5.3.2 升级JDK版本的性能变化及兼容问题

5.3.2 升级JDK版本的性能变化及兼容问题

对Eclipse进行调优的第一步就是先对虚拟机的版本进行升级,希望能先从虚拟机版本身上得到一些“免费的”性能提升。

每次JDK的大版本发布时,发行商通常都会宣称虚拟机的运行速度比上一版本有了多少比例的提高,这虽然是个广告性质的宣言,常被使用者从更新列表或者技术白皮书中直接忽略,但技术进步确实会促使性能改进,从国内外的第三方评测数据来看,版本升级至少在某些方面确实带来了一定性能改善^1。以下是一个第三方网站对JDK 5、6、7三个版本做的性能评测,分别测试了以下4个用例[^2]。

1)生成500万个字符串。
2)500万次ArrayList<String>数据插入,使用第一点生成的数据。
3)生成500万个HashMap<String,Integer>,每个键-值对通过并发线程计算,测试并发能力。
4)打印500万个ArrayList<String>中的值到文件,并重读回内存。

三个版本的JDK分别运行这4个用例的测试程序,测试结果如图5-4所示。

image-20210919152917030

图5-4 JDK横向性能对比

从这4个用例的测试结果来看,在每一个测试场景中新版的JDK性能都有改进,譬如JDK 6比JDK 5有大约15%的平均性能提升。尽管对JDK仅测试这四个用例并不能说明什么问题,甚至要通过测试数据来量化描述一个JDK比旧版提升了多少本身就是很难做到特别科学准确的(要做稍微靠谱一点的测试,可以使用SPECjvm 2015[^3]之类的软件来完成,或者把相应版本的TCK[^4]中数万个测试用例的性能数据对比一下可能稍有说服力),但笔者还是选择相信这次“软广告”性质的测试,把JDK版本升级到JDK 6 Update 21,升级没有选择JDK 7或者其他版本的最主要理由是:本书后续故事剧情发展需要。

与所有小说作者(嗯……知道,本书不是小说)设计的故事情节一样,获得最后的胜利之前总是要经历各种各样的挫折,这次升级到JDK 6之后,性能有什么变化先暂且不谈,在使用几分钟之后, 笔者的Eclipse就和前面几个服务端的案例一样非常“不负众望”地发生了内存溢出,如图5-5所示。

image-20210919153051781
图5-5 Eclipse OutOfMemoryError

这次内存溢出开始是完全出乎笔者意料的:决定对Eclipse做调优是因为速度慢,但笔者的开发环境一直都很稳定,至少没有出现过内存溢出的问题,而这次升级除了修改了eclipse.ini中的Java虚拟机路径之外,还未进行任何运行参数的调整,Eclipse居然进去主界面之后随便开了几个文件就抛出内存溢出异常了,难道JDK 6 Update21有哪个类库的API出现了严重的泄漏问题吗?

事实上并不是JDK 6出现了什么问题,否则以Java的影响力,它早就上新闻了。根据前面三章中介绍讲解的原理和工具,我们要查明这个异常的原因并且解决它一点也不困难。打开VisualVM,监视页签中的内存曲线部分如图5-6、图5-7所示。

在Java堆中监视曲线里,“堆大小”的曲线与“使用的堆”的曲线一直都有很大的间隔距离,每当两条曲线开始出现互相靠近的趋势时,“堆大小”的曲线就会快速向上转向,而“使用的堆”的曲线会向下转向。“堆大小”的曲线向上代表的是虚拟机内部在进行堆扩容,因为运行参数中并没有指定最小堆(-Xms)的值与最大堆(-Xmx)相等,所以堆容量一开始并没有扩展到最大值,而是根据使用情况进行伸缩扩展。“使用的堆”的曲线向下是因为虚拟机内部触发了一次垃圾收集,一些废弃对象的空间被回收后,内存用量相应减少。从图形上看,Java堆运作是完全正常的。但永久代的监视曲线就很明显有问题了,“PermGen大小”的曲线与“使用的PermGen”的曲线几乎完全重合在一起,这说明永久代中已经没有可回收的资源了,所以“使用的PermGen”的曲线不会向下发展,并且永久代中也没有空间可以扩展了,所以“PermGen大小”的曲线不能向上发展,说明这次内存溢出很明显是永久代导致的内存溢出。

image-20210919153527309

图5-6 Java堆监视曲线

image-20210919153227432
图5-7 永久代监视曲线

再注意到图5-7中永久代的最大容量“67108864字节”,也就是64MB,这恰好是JDK在未使用- XX:MaxPermSize参数明确指定永久代最大容量时的默认值,无论JDK 5还是JDK 6,这个默认值都是64MB。对于Eclipse这种规模的Java程序来说,64MB的永久代内存空间显然是不够的,内存溢出是肯定的,但为何在JDK 5中没有发生过溢出呢?

在VisualVM的“概述>JVM参数”页签中,分别检查使用JDK 5和JDK 6运行Eclipse时的Java虚拟机启动参数,发现使用JDK 6时,只有三个启动参数,如代码清单5-5所示。

代码清单5-5 JDK 1.6的Eclipse运行期参数

1
2
3
-Dcom.sun.management.jmxremote 
-Dosgi.requiredJavaVersion=1.5
-Xmx512m

而使用JDK 5运行时,就有四个启动参数,其中多出来的一个正好就是设置永久代最大容量的- XX:MaxPermSize=256M,如代码清单5-6所示。
代码清单5-6 JDK 1.5的Eclipse运行期参数

1
2
3
4
-Dcom.sun.management.jmxremote 
-Dosgi.requiredJavaVersion=1.5
-Xmx512m
-XX:MaxPermSize=256M

为什么会这样呢?笔者从Eclipse的Bug List网站[^5]上找到答案:使用JDK 5时之所以有永久代容量这个参数,是因为在eclipse.ini中存在“–launcher.XXMaxPermSize 256M”这项设置,当launcher——也就是Windows下的可执行程序eclipse.exe,检测到Eclipse是运行在Sun公司的虚拟机上的话,就会把参数值转化为-XX:MaxPermSize传递给虚拟机进程。因为世界三大商用虚拟机中只有Sun公司的虚拟机才有永久代的概念,也就是只有JDK 8以前的HotSpot虚拟机才需要设置这个参数,JRockit虚拟机和J9虚拟机都是不需要设置的,所以这个参数才会有检测虚拟机后进行设置的过程。

2010年4月10日,Oracle正式完成对Sun公司的收购,此后无论是网页还是具体程序产品,提供商都从Sun变为了Oracle,而eclipse.exe就是根据程序提供商来判断是否Sun公司的虚拟机的,当JDK 1.6 Update 21中java.exe、javaw.exe的“Company”属性从“Sun Microsystems Inc.”变为“Oracle Corporation”后,Eclipse就不再认识这个虚拟机了,因此没有把最大永久代的参数传递过去。

查明了原因,解决方案就简单了,launcher不认识就只好由人来告诉它,在eclipse.ini中明确指定- XX:MaxPermSize=256M这个参数,问题随即解决。

[^2]: 测试用例、数据及图片来源于http://www.taranfx.com/java-7-whats-new-performance-benchmark-1-5-1- 6-1-7。
[^3]: 官方网站:http://www.spec.org/jvm2008/docs/UserGuide.html。
[^4]: TCK(Technology Compatibility Kit)是一套由一组测试用例和相应的测试工具组成的工具包,用于保证一个使用Java技术的实现能够完全遵守其适用的Java平台规范,并且符合相应的参考实现。
[^5]: https://bugs.eclipse.org/bugs/show_bug.cgi?id=319514。