提高 Java 代码性能的各种技巧Word文档格式.docx
《提高 Java 代码性能的各种技巧Word文档格式.docx》由会员分享,可在线阅读,更多相关《提高 Java 代码性能的各种技巧Word文档格式.docx(13页珍藏版)》请在冰点文库上搜索。
![提高 Java 代码性能的各种技巧Word文档格式.docx](https://file1.bingdoc.com/fileroot1/2023-5/4/8fef485a-3ff7-46a6-9909-3507f17242dc/8fef485a-3ff7-46a6-9909-3507f17242dc1.gif)
除了明确的共享字符串,PermGen字符串池还包含所有程序中使用过的字符串(这里要注意是使用过的字符串,如果类或者方法从未加载或者被条用,在其中定义的任何常量都不会被加载)
Java6中字符串池的最大问题是它的位置—PermGen。
PermGen的大小是固定的并且在运行时是无法扩展的。
你可以使用
-XX:
MaxPermSize=N
配置来调整它的大小。
据我了解,对于不同的平台默认的PermGen大小在32M到96M之间。
你可以扩展它的大小,不过大小使用都是固定的。
这个限制需要你在使用
时需要非常小心—你最好不要使用这个方法intern任何无法控制的用户输入。
这是为什么在JAVA6中大部分使用手动管理
Map
来实现字符串池
Java7中的String.intern()
Java7中Oracle的工程师对字符串池的逻辑做了很大的改变—字符串池的位置被调整到heap中了。
这意味着你再也不会被固定的内存空间限制了。
所有的字符串都保存在堆(heap)中同其他普通对象一样,这使得你在调优应用时仅需要调整堆大小。
这个改动使得我们有足够的理由让我们重新考虑在Java7中使用String.intern()。
字符串池中的数据会被垃圾收集
没错,在JVM字符串池中的所有字符串会被垃圾收集,如果这些值在应用中没有任何引用。
这是用于所有版本的Java,这意味着如果
interned的字符串在作用域外并且没有任何引用—它将会从JVM的字符串池中被垃圾收集掉。
因为被重新定位到堆中以及会被垃圾收集,JVM的字符串池看上去是存放字符串的合适位置,是吗?
理论上是—违背使用的字符串会从池中收集掉,当外部输入一个字符传且池中存在时可以节省内存。
看起来是一个完美的节省内存的策略?
在你回答这个之前,可以肯定的是你需要知道字符串池是如何实现的。
在Java6,7,8中JVM字符串池的实现
字符串池是使用一个拥有固定容量的
HashMap
每个元素包含具有相同hash值的字符串列表。
一些实现的细节可以从Javabug报告中获得
默认的池大小是1009(出现在上面提及的bug报告的源码中,在Java7u40中增加了)。
在JAVA6早期版本中是一个常量,在随后的
java6u30至java6u41中调整为可配置的。
而在java7中一开始就是可以配置的(至少在java7u02中是可以配置的)。
你需要指定参数
StringTableSize=N,
N是字符串池
的大小。
确保它是为性能调优而预先准备的大小。
在Java6中这个参数没有太多帮助,因为你仍任被限制在固定的PermGen内存大小中。
后续的讨论将直接忽略Java6
Java7(直至Java7u40)
在Java7中,换句话说,你被限制在一个更大的堆内存中。
这意味着你可以预先设置好String池的大小(这个值取决于你的应用程序需求)。
通常说来,一旦程序开始内存消耗,内存都是成百兆的增长,在这种情况下,给一个拥有100万字符串对象的字符串池分配8-16M的内存看起来是比较适合的(不要使用1,000,000作为
StringTaleSize
的值–它不是质数;
使用
1,000,003代替)
你可能期待关于String在Map中的分配—可以阅读我之前关于HashCode方法调优的经验。
你必须设置一个更大的
StringTalbeSize
值(相比较默认的1009),如果你希望更多的使用String.intern()—否则这个方法将很快递减到0(池大小)。
我没有注意到在intern小于100字符的字符串时的依赖情况(我认为在一个包含50个重复字符的字符串与现实数据并不相似,因此100个字符看上去是一个很好的测试限制)
下面是默认池大小的应用程序日志:
第一列是已经intern的字符串数量,第二列intern10,000个字符串所有的时间(秒)
0;
time=0.0sec
50000;
time=0.03sec
100000;
time=0.073sec
150000;
time=0.13sec
200000;
time=0.196sec
250000;
time=0.279sec
300000;
time=0.376sec
350000;
time=0.471sec
400000;
time=0.574sec
450000;
time=0.666sec
500000;
time=0.755sec
550000;
time=0.854sec
600000;
time=0.916sec
650000;
time=1.006sec
700000;
time=1.095sec
750000;
time=1.273sec
800000;
time=1.248sec
850000;
time=1.446sec
900000;
time=1.585sec
950000;
time=1.635sec
1000000;
time=1.913sec
测试是在Corei5-3317U@1.7GhzCPU设备上进行的。
你可以看到,它成线性增长,并且在JVM字符串池包含一百万个字符串时,我仍然可以近似每秒
intern
5000个字符串,这对于在内存中处理大量数据的应用程序来说太慢了。
现在,调整
StringTableSize=100003
参数来重新运行测试:
time=0.017sec
time=0.009sec
time=0.01sec
time=0.007sec
time=0.008sec
time=0.013sec
time=0.011sec
time=0.012sec
time=0.015sec
可以看到,这时插入字符串的时间近似于常量(在Map的字符串列表中平均字符串个数不超过10个),下面是相同设置的结果,不过这次我们将向池中插入1000万个字符串(这意味着Map中的字符串列表平均包含100个字符串)
2000000;
time=0.024sec
3000000;
time=0.028sec
4000000;
time=0.053sec
5000000;
time=0.051sec
6000000;
time=0.034sec
7000000;
time=0.041sec
8000000;
time=0.089sec
9000000;
time=0.111sec
10000000;
time=0.123sec
现在让我们将吃的大小增加到100万(精确的说是1,000,003)
time=0.005sec
time=0.004sec
如你所看到的,时间非常平均,并且与“0到100万”的表没有太大差别。
甚至在池大小足够大的情况下,我的笔记本也能每秒添加1,000,000个字符对象。
我们还需要手工管理字符串池吗?
现在我们需要对比JVM字符串池和
WeakHashMap<
String,WeakReference<
String>
>
它可以用来模拟JVM字符串池。
下面的方法用来替换
String.intern:
privatestaticfinalWeakHashMap<
s_manualCache=
newWeakHashMap<
(100000);
privatestaticStringmanualIntern(finalStringstr)
{
finalWeakReference<
cached=s_manualCache.get(str);
if(cached!
=null)
{
finalStringvalue=cached.get();
if(value!
returnvalue;
}
s_manualCache.put(str,newWeakReference<
(str));
returnstr;
}
下面针对手工池的相同测试:
manualtime=0.001sec
manualtime=0.03sec
manualtime=0.034sec
manualtime=0.008sec
manualtime=0.019sec
manualtime=0.011sec
manualtime=0.027sec
manualtime=0.009sec
manualtime=0.007sec
当JVM有足够内存时,手工编写的池提供了良好的性能。
不过不幸的是,我的测试(保留
String.valueOf(0<
N<
1,000,000,000))保留非常短的字符串,在使用
-Xmx1280M
参数时它允许我保留月为2.5M的这类字符串。
JVM字符串池(size=1,000,003)从另一方面讲在JVM内存足够时提供了相同的性能特性,知道JVM字符串池包含12.72M的字符串并消耗掉所有内存(5倍多)。
我认为,这非常值得你在你的应用中去掉所有手工字符串池。
在Java7u40+以及Java8中的String.intern()
Java7u40版本扩展了字符串池的大小(这是组要的性能更新)到60013.这个值允许你在池中包含大约30000个独立的字符串。
通常来说,这对于需要保存的数据来说已经足够了,你可以通过
+PrintFlagsFinal
JVM参数获得这个值。
我尝试在原始发布的Java8中运行相同的测试,Java8仍然支持
StringTableSize
参数来兼容Java7特性。
主要的区别在于Java8中默认的池大小增加到60013:
time=0.019sec
time=0.014sec
time=0.018sec
time=0.029sec
time=0.02sec
time=0.021sec
测试代码
这篇文章的测试代码很简单,一个方法中循环创建并保留新字符串。
你可以测量它保留10000个字符串所需要的时间。
最好配合
-verbose:
gc
JVM参数来运行这个测试,这样可以查看垃圾收集是何时以及如何发生的。
另外最好使用
-Xmx
参数来执行堆的最大值。
这里有两个测试:
testStringPoolGarbageCollection
将显示JVM字符串池被垃圾收集—检查垃圾收集日志消息。
在Java6的默认PermGen大小配置上,这个测试会失败,因此最好增加这个值,或者更新测试方法,或者使用Java7.
第二个测试显示内存中保留了多少字符串。
在Java6中执行需要两个不同的内存配置比如:
-Xmx128M
以及
(10倍以上)。
你可能发现这个值不会影响放入池中字符串的数量。
另一方面,在Java7中你能够在堆中填满你的字符串。
/**
-TestingString.intern.
*
-Runthisclassatleastwith-verbose:
gcJVMparameter.
*/
publicclassInternTest{
publicstaticvoidmain(String[]args){
testStringPoolGarbageCollection();
testLongLoop();
/**
-Usethismethodtoseewhereinternedstringsarestored
-andhowmanyofthemcanyoufitforthegivenheapsize.
privatestaticvoidtestLongLoop()
test(1000*1000*1000);
//uncommentthefollowinglinetoseethehand-writtencacheperformance
//testManual(1000*1000*1000);
-Usethismethodtocheckthatnotusedinternedstringsaregarbagecollected.
privatestaticvoidtestStringPoolGarbageCollection()
//firstmethodcall-useitasareference
test(1000*1000);
//wearegoingtocleanthecachehere.
System.gc();
//checkthememoryconsumptionandhowlongdoesittaketointernstrings
//inthesecondmethodcall.
privatestaticvoidtest(finalintcnt)
finalList<
lst=newArrayList<
(100);
longstart=System.currentTimeMillis();
for(inti=0;
i<
cnt;
++i)
finalStringstr="
Verylongteststring,whichtellsyouaboutsomething"
+
"
very-veryimportant,definitelydeservingtobeinterned#"
+i;
//uncommentthefollowinglinetotestdependencyfromstringlength
//finalStringstr=Integer.toString(i);
lst.add(str.intern());
if(i%10000==0)
System.out.println(i+"
;
time="
+(System.currentTimeMillis()-start)/1000.0+"
sec"
);
start=System.currentTimeMillis();
System.out.println("
Totallength="
+lst.size());
privatestaticfinalWeakHashMap<
s_manualCache=
privatestaticStringmanualIntern(finalStringstr)
privatestaticvoidtestManual(finalintcnt)
lst.add(manualIntern(str));
manualtime="
+