4

我正在解析一个相当大 (200 MB) 的 XML 文件,该文件会生成一个对象树,每个对象都定义了一堆参数 (key=value)。此数据结构在 Tomcat webapp 中运行并用于查找这些参数。

几个月前,我们在这台服务器上发现了一个堆内存问题。我们可以通过实习参数键和值(其中大多数是非常冗余的)来解决它,这将内存占用从超过 150 MB 减少到低至 20 MB。

今天我正在重新访问服务器,因为人们抱怨启动时间。我正在分析服务器并看到使用 XPP3 解析 XML 需要 40 秒,其中 String.intern() 需要 30 多秒。

我知道这是一个权衡。我知道我可以自己做实习。由于解析 XML 是单线程的,因此简单的 HashMap 也可以完成这项工作。但你知道,这感觉有点奇怪。

是否有人计算过这些数字,看看是否值得放弃 String.intern 以支持不同的解决方案?

那么问题来了?我怎样才能使此类问题的争用尽可能低?

谢谢,斯特凡

4

4 回答 4

3

添加一个额外的间接步骤:拥有第二个 HashMap 来保存键,并在将键插入内存结构之前先在那里查找键。这将为您提供比 String#intern() 更大的灵活性。

但是,如果您需要在每次启动 tomcat 时解析那个 200MB 的 XML 文件,而额外的 10 秒让人们抱怨(他们是否经常重新启动 tomcat?) - 这会使标志弹出(您是否考虑过使用数据库,甚至是 Apache德比,保留解析的数据?)。

于 2011-07-28T10:06:15.010 回答
1

当您添加更多字符串时,String.intern() 似乎不能很好地扩展。池中字符串的数量似乎为 O(n)。

Random rand = new Random();
for(int i=0;i<100;i++) {
    long start = System.nanoTime();
    for(int j=0;j<100000;j++)
        Long.toString(rand.nextLong()).toString().intern();
    long time = System.nanoTime() - start;
    System.out.printf("Took %,d ns on average to intern() a random string%n", time/100000);
}

印刷

Took 1,586 ns on average to intern() a random string
Took 3,843 ns on average to intern() a random string
Took 7,551 ns on average to intern() a random string
Took 13,436 ns on average to intern() a random string
Took 20,226 ns on average to intern() a random string
Took 27,609 ns on average to intern() a random string
Took 35,098 ns on average to intern() a random string
Took 42,439 ns on average to intern() a random string
Took 50,801 ns on average to intern() a random string
Took 20,975 ns on average to intern() a random string
Took 4,634 ns on average to intern() a random string
Took 10,512 ns on average to intern() a random string
Took 16,914 ns on average to intern() a random string
Took 23,601 ns on average to intern() a random string
Took 30,230 ns on average to intern() a random string
Took 36,184 ns on average to intern() a random string
Took 43,266 ns on average to intern() a random string

相反,我使用数组作为字符串池。

private static void testHashArray(String[] strings2, int size) {
    String[] pool = new String[size];
    int hit=0, miss=0;
    long start2 = System.nanoTime();
    for (String s : strings2) {
        int hash = (s.hashCode() & 0x7fffffff) % pool.length;
        String s2 = pool[hash];
        if (s.equals(s2)) {
            hit++;
        } else {
            miss++;
        }
        if (s2 != s)
            pool[hash] = s;
    }
    long time2 = System.nanoTime() - start2;
    System.out.printf("Hash size: %,d took %.3f second. Hit/miss %,d/%,d %n", size, time2 / 1e9, hit, miss);
}

public static void main(String... args) {
    Random rand = new Random();

    // a million unique strings.
    String[] strings = new String[1000 * 1000];
    for (int i = 0; i < strings.length; i++)
        strings[i] = String.valueOf(rand.nextLong());
    // random selection of Strings
    String[] strings2 = new String[10 * 1000 * 1000];
    int totalSize = 0;
    for (int i = 0; i < strings2.length; i++) {
        int idx = (int) Math.pow(strings.length, rand.nextFloat());
        String s = strings[idx];
        strings2[i] = s;
        totalSize += s.length() + 16; // with overhead
    }
    System.out.printf("Original size %,d%n", totalSize);

    Set<String> uniqueStrings = Collections.newSetFromMap(new IdentityHashMap<String, Boolean>());
    uniqueStrings.addAll(Arrays.asList(strings2));
    System.out.printf("Unique strings %,d%n", uniqueStrings.size());

    long start = System.nanoTime();
    HashMap<String,String> map = new HashMap();
    for(String s: strings2)
        map.put(s,s);
    long time = System.nanoTime() - start;
    System.out.printf("Took %.3f second to map strings%n", time/1e9);

    testHashArray(strings2, 10192);
    testHashArray(strings2, 101929);
    testHashArray(strings2, 1019291);
}

印刷

Original size 353,293,201
Unique strings 766,222
Took 0.979 second to map strings
Hash size: 10,192 took 0.357 second. Hit/miss 5,213,210/4,786,790 
Hash size: 101,929 took 0.309 second. Hit/miss 7,202,094/2,797,906 
Hash size: 1,019,291 took 0.254 second. Hit/miss 8,789,382/1,210,618 

如果做实习生很慢,那么在后台线程中加载之后执行它怎么样。加载服务器后,您可以在找到重复项时对字符串进行 intern()。

你真的需要节省 130 MB 吗?我知道这听起来不错,但内存会用于其他用途吗?

如果您想在 intern() 上使用更快的形式,您可以使用固定大小的数组。

于 2011-07-28T12:00:53.410 回答
0

我们在将字符串解析为经过验证的“名称”对象时遇到问题。这是在应用程序的所有地方完成的,需要在内存和速度方面进行优化。

经过几次测试运行,我们最终得到了一个处理 char 数组的解决方案,无论是在解析期间还是在 Name 的实现中。

String.toCharArray()来检索字符串的数组,或者可以使用String.charAt(pos)。为了在数组之间快速复制,我们使用了System.arrayCopy

解析实际上比使用缓存进行查找要快。

于 2011-07-28T10:11:36.187 回答
0

这是另一个想法,虽然它可能听起来有点古怪。您是否想过编写一个代码生成器,它只解析您的 XML 文件并吐出 Java 代码,该代码使用实际字符串填充映射(那些在编译时被实习)

像这样的东西

public final class ConfigurationData {
  public static String get(String key) {
    return map.get(key);
  }
  private static final Map<String,String> MAP;
  static {
    MAP = new HashMap<String,String>([[[ number of records to load up ]]]);
    MAP.put([[[key 1]]], [[[ value 1 ]]]);
    MAP.put([[[key 2]]], [[[ value 2 ]]]);
    ...
  }
}

这遵循与预编译 JSP 相同的概念,以节省第一次用户损失,但它增加了另一个构建步骤,如果配置文件发生更改(无论如何都应该控制),它就会成为部署。

于 2011-11-02T05:42:28.000 回答