如何避免string.intern()争用并保持较低的内存占用?

时间:2011-07-28 09:48:37

标签: java string memory contention string-interning

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

几个月前,我们在此服务器上发现了堆内存问题。我们可以通过实现参数键和值(大多数非常冗余)来实现解决,从而将内存占用量从150 MB减少到20 MB。

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

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

是否有人关注数字以确定是否值得将String.intern放弃以支持其他解决方案?

所以问题是?如何才能在这些问题上获得尽可能低的争用?

谢谢, 斯蒂芬

4 个答案:

答案 0 :(得分:3)

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

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

答案 1 :(得分: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 

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

你真的需要节省130 MB吗?我知道这听起来很棒,但是记忆是否会用于别的东西呢?

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

答案 2 :(得分:0)

我们遇到一个问题,即String被解析为经过验证的'Name'对象。 这在应用程序中的所有位置都已完成,需要在内存和速度方面进行优化。

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

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

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

答案 3 :(得分: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的概念相同,以节省第一个用户损失,但它增加了另一个构建步骤,并且如果存在配置文件更改(无论如何应该进行控制),它将成为部署。