我正在解析一个相当大的(200 MB)XML文件,该文件导致一个对象树,每个对象都定义了一堆参数(key = value)。此数据结构在Tomcat webapp中运行,用于查找这些参数。
几个月前,我们在此服务器上发现了堆内存问题。我们可以通过实现参数键和值(大多数非常冗余)来实现解决,从而将内存占用量从150 MB减少到20 MB。
今天我正在重新访问服务器,因为人们抱怨启动时间。我正在分析服务器并发现使用XPP3解析XML需要40秒,其中String.intern()需要超过30秒。
我知道这是一个权衡。我知道我可以自己做实习。由于解析XML是单线程的,因为简单的HashMap也可以完成这项工作。但是你知道,这有点奇怪。
是否有人关注数字以确定是否值得将String.intern放弃以支持其他解决方案?
所以问题是?如何才能在这些问题上获得尽可能低的争用?
谢谢, 斯蒂芬
答案 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的概念相同,以节省第一个用户损失,但它增加了另一个构建步骤,并且如果存在配置文件更改(无论如何应该进行控制),它将成为部署。