提高Java代码性能的各种技巧
作者:网络转载 发布时间:[ 2015/5/7 11:18:54 ] 推荐标签:编程语言
Java 6,7,8中的String.intern–字符串池
这篇文章将要讨论Java 6中是如何实现String.intern方法的,以及这个方法在Java 7以及Java 8中做了哪些调整。
字符串池
字符串池(有名字符串标准化)是通过使用的共享String对象来使用相同的值不同的地址表示字符串的过程。你可以使用自己定义的Map<String,String>(根据需要使用weak引用或者soft引用)并使用map中的值作为标准值来实现这个目标,或者你也可以使用JDK提供的String.intern()。
很多标准禁止在Java 6中使用String.intern()因为如果频繁使用池会市区控制,有很大的几率触发OutOfMemoryException。Oracle Java 7对字符串池做了很多改进,你可以通过以下地址进行了解http://bugs.sun.com/view_bug.do?bug_id=6962931以及http://bugs.sun.com/view_bug.do?bug_id=6962930
Java 6中的String.intern()
在美好的过去所有共享的String对象都存储在PermGen中—堆中固定大小的部分主要用于存储加载的类对象和字符串池。除了明确的共享字符串,PermGen字符串池还包含所有程序中使用过的字符串(这里要注意是使用过的字符串,如果类或者方法从未加载或者被条用,在其中定义的任何常量都不会被加载)
Java 6中字符串池的大问题是它的位置—PermGen。PermGen的大小是固定的并且在运行时是无法扩展的。你可以使用-XX:MaxPermSize=N配置来调整它的大小。据我了解,对于不同的平台默认的PermGen大小在32M到96M之间。你可以扩展它的大小,不过大小使用都是固定的。这个限制需要你在使用String.intern时需要非常小心—你好不要使用这个方法intern任何无法控制的用户输入。这是为什么在JAVA6中大部分使用手动管理Map来实现字符串池
Java 7中的String.intern()
Java 7中Oracle的工程师对字符串池的逻辑做了很大的改变—字符串池的位置被调整到heap中了。这意味着你再也不会被固定的内存空间限制了。所有的字符串都保存在堆(heap)中同其他普通对象一样,这使得你在调优应用时仅需要调整堆大小。这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用String.intern()。
字符串池中的数据会被垃圾收集
没错,在JVM字符串池中的所有字符串会被垃圾收集,如果这些值在应用中没有任何引用。这是用于所有版本的Java,这意味着如果interned的字符串在作用域外并且没有任何引用—它将会从JVM的字符串池中被垃圾收集掉。
因为被重新定位到堆中以及会被垃圾收集,JVM的字符串池看上去是存放字符串的合适位置,是吗?理论上是—违背使用的字符串会从池中收集掉,当外部输入一个字符传且池中存在时可以节省内存。看起来是一个完美的节省内存的策略?在你回答这个之前,可以肯定的是你需要知道字符串池是如何实现的。
在Java 6,7,8中JVM字符串池的实现
字符串池是使用一个拥有固定容量的HashMap每个元素包含具有相同hash值的字符串列表。一些实现的细节可以从Java bug报告中获得http://bugs.sun.com/view_bug.do?bug_id=6962930
默认的池大小是1009(出现在上面提及的bug报告的源码中,在Java7u40中增加了)。在JAVA 6早期版本中是一个常量,在随后的java6u30至java6u41中调整为可配置的。而在java 7中一开始是可以配置的(至少在java7u02中是可以配置的)。你需要指定参数-XX:StringTableSize=N,N是字符串池Map的大小。确保它是为性能调优而预先准备的大小。
在Java 6中这个参数没有太多帮助,因为你仍任被限制在固定的PermGen内存大小中。后续的讨论将直接忽略Java 6
Java 7(直至Java7u40)
在Java7中,换句话说,你被限制在一个更大的堆内存中。这意味着你可以预先设置好String池的大小(这个值取决于你的应用程序需求)。通常说来,一旦程序开始内存消耗,内存都是成百兆的增长,在这种情况下,给一个拥有100万字符串对象的字符串池分配8-16M的内存看起来是比较适合的(不要使用1,000,000作为-XX:StringTaleSize的值–它不是质数;使用1,000,003代替)
你可能期待关于String在Map中的分配—可以阅读我之前关于HashCode方法调优的经验。
你必须设置一个更大的-XX:StringTalbeSize值(相比较默认的1009),如果你希望更多的使用String.intern()—否则这个方法将很快递减到0(池大小)。
我没有注意到在intern小于100字符的字符串时的依赖情况(我认为在一个包含50个重复字符的字符串与现实数据并不相似,因此100个字符看上去是一个很好的测试限制)
下面是默认池大小的应用程序日志:第一列是已经intern的字符串数量,第二列intern 10,000个字符串所有的时间(秒)
0;time=0.0 sec
50000;time=0.03 sec
100000;time=0.073 sec
150000;time=0.13 sec
200000;time=0.196 sec
250000;time=0.279 sec
300000;time=0.376 sec
350000;time=0.471 sec
400000;time=0.574 sec
450000;time=0.666 sec
500000;time=0.755 sec
550000;time=0.854 sec
600000;time=0.916 sec
650000;time=1.006 sec
700000;time=1.095 sec
750000;time=1.273 sec
800000;time=1.248 sec
850000;time=1.446 sec
900000;time=1.585 sec
950000;time=1.635 sec
1000000;time=1.913 sec
测试是在Core i5-3317U@1.7Ghz CPU设备上进行的。你可以看到,它成线性增长,并且在JVM字符串池包含一百万个字符串时,我仍然可以近似每秒intern 5000个字符串,这对于在内存中处理大量数据的应用程序来说太慢了。
现在,调整-XX:StringTableSize=100003参数来重新运行测试:
50000;time=0.017 sec
100000;time=0.009 sec
150000;time=0.01 sec
200000;time=0.009 sec
250000;time=0.007 sec
300000;time=0.008 sec
350000;time=0.009 sec
400000;time=0.009 sec
450000;time=0.01 sec
500000;time=0.013 sec
550000;time=0.011 sec
600000;time=0.012 sec
650000;time=0.015 sec
700000;time=0.015 sec
750000;time=0.01 sec
800000;time=0.01 sec
850000;time=0.011 sec
900000;time=0.011 sec
950000;time=0.012 sec
1000000;time=0.012 sec
可以看到,这时插入字符串的时间近似于常量(在Map的字符串列表中平均字符串个数不超过10个),下面是相同设置的结果,不过这次我们将向池中插入1000万个字符串(这意味着Map中的字符串列表平均包含100个字符串)
2000000;time=0.024 sec
3000000;time=0.028 sec
4000000;time=0.053 sec
5000000;time=0.051 sec
6000000;time=0.034 sec
7000000;time=0.041 sec
8000000;time=0.089 sec
9000000;time=0.111 sec
10000000;time=0.123 sec
现在让我们将吃的大小增加到100万(精确的说是1,000,003)
1000000;time=0.005 sec
2000000;time=0.005 sec
3000000;time=0.005 sec
4000000;time=0.004 sec
5000000;time=0.004 sec
6000000;time=0.009 sec
7000000;time=0.01 sec
8000000;time=0.009 sec
9000000;time=0.009 sec
10000000;time=0.009 sec
如你所看到的,时间非常平均,并且与“0到100万”的表没有太大差别。甚至在池大小足够大的情况下,我的笔记本也能每秒添加1,000,000个字符对象。
我们还需要手工管理字符串池吗?
现在我们需要对比JVM字符串池和WeakHashMap<String,WeakReference<String>>它可以用来模拟JVM字符串池。下面的方法用来替换String.intern:
private static final WeakHashMap<String,WeakReference<String>>s_manualCache=
new WeakHashMap<String,WeakReference<String>>(100000);
private static String manualIntern(final String str)
{
final WeakReference<String>cached=s_manualCache.get(str);
if(cached!=null)
{
final String value=cached.get();
if(value!=null)
return value;
}
s_manualCache.put(str,new WeakReference<String>(str));
return str;
}

sales@spasvo.com