实战:Eclipse运行速度调优

很多Java开发人员都有一种错觉,认为系统调优的工作都是针对服务端应用的,规模越大的系统,就需要越专业的调优运维团队参与。这个观点不能说不对,只是有点狭隘了。上一节中笔者所列举的案例确实大多是服务端运维、调优的例子,但不只服务端需要调优,其他应用类型也是需要的,作为一个普通的Java开发人员,学习到的各种虚拟机的原理和最佳实践方法距离我们并不遥远,开发者身边就有很多场景可以使用上这些知识。下面就通过一个普通程序员日常工作中可以随时接触到的开发工具开始这次实战[1]。

1、调优前的程序运行状态

笔者使用Eclipse作为日常工作中的主要IDE工具,由于安装的插件比较大(如Kloc-work、ClearCase LT等)、代码也很多,启动Eclipse直到所有项目编译完成需要四五分钟。一直对开发环境的速度感觉到不满意,趁着编写这本书的机会,决定对Eclipse进行“动刀”调优。

笔者机器的Eclipse运行平台是32位Windows 7系统,虚拟机为HotSpot 1.5 b64。硬件为ThinkPad X201,Intel i5 CPU,4GB物理内存。在初始的配置文件eclipse.ini中,除了指定JDK的路径、设置最大堆为512MB以及开启了JMX管理(需要在VisualVM中收集原始数据)外,未作任何改动,原始配置内容如代码清单5-3所示。

代码清单5-3 Eclipse 3.5初始配置

-vm 
D:/_DevSpace/jdk1.5.0/bin/javaw.exe 
-startup 
plugins/org.eclipse.equinox.launcher_1.0.201.R35x_v20090715.jar 
--launcher.library 
plugins/org.eclipse.equinox.launcher.win32.win32.x86_1.0.200.v20090519 
-product org.eclipse.epp.package.jee.product 
--launcher.XXMaxPermSize 
256M 
-showsplash 
org.eclipse.platform 
-vmargs 
-Dosgi.requiredJavaVersion=1.5 
-Xmx512m 
-Dcom.sun.management.jmxremote 

为了与调优后的结果进行量化对比,调优开始前笔者先做了一次初始数据测试。测试用例很简单,就是收集从Eclipse启动开始,直到所有插件加载完成为止的总耗时以及运行状态数据,虚拟机的运行数据通过VisualVM及其扩展插件VisualGC进行采集。测试过程中反复启动数次Eclipse直到测试结果稳定后,取最后一次运行的结果作为数据样本(为了避免操作系统未能及时进行磁盘缓存而产生的影响),数据样本如图5-2所示。
图5-2 Eclipse原始运行数据
Eclipse启动的总耗时没有办法从监控工具中直接获得,因为VisualVM不可能知道Eclipse运行到什么阶段算是启动完成。为了测试的准确性,笔者写了一个简单的Eclipse插件,用于统计Eclipse的启动耗时。由于代码十分简单,且本书并不是Eclipse RCP的开发教程,所以只列出代码清单5-4供读者参考,不再延伸。如果读者需要这个插件,可以使用下面的代码自己编译即可。

代码清单5-4 Eclipse启动耗时统计插件

ShowTime.java代码: 
import org.eclipse.jface.dialogs.MessageDialog; 
import org.eclipse.swt.widgets.Display; 
import org.eclipse.swt.widgets.Shell; 
import org.eclipse.ui.IStartup; 
/*** 统计Eclipse启动耗时 * @author zzm */ 
public class ShowTime implements IStartup { 
	public void earlyStartup() { 
		Display.getDefault().syncExec(new Runnable() { 
			public void run() { 
				long eclipseStartTime = Long.parseLong(System.getProperty("eclipse.startTime")); 
				long costTime = System.currentTimeMillis() - eclipseStartTime; 
				Shell shell = Display.getDefault().getActiveShell(); 
				String message = "Eclipse启动耗时:" + costTime + "ms"; 
				MessageDialog.openInformation(shell, "Information", message); 
			}
		}); 
	}
}

plugin.xml代码:

<?xml version="1.0" encoding="UTF-8"?> 
<?eclipse version="3.4"?> 
<plugin> 
	<extension point="org.eclipse.ui.startup"> 
		<startup class="eclipsestarttime.actions.ShowTime"/> 
	</extension> 
</plugin> 

上述代码打包成JAR后放到Eclipse的plugins目录,反复启动几次后,插件显示的平均时间稳定在 15秒左右,如图5-3所示。
在这里插入图片描述
根据VisualGC和Eclipse插件收集到的信息,总结原始配置下的测试结果如下:

  • 整个启动过程平均耗时约15秒。
  • 最后一次启动的数据样本中,垃圾收集总耗时4.149秒,其中:
    ■Full GC被触发了19次,共耗时3.166秒;
    ■Minor GC被触发了378次,共耗时0.983秒。
  • 加载类9115个,耗时4.114秒。
  • 即时编译时间1.999秒。
  • 交给虚拟机的512MB堆内存被分配为40MB的新生代(31.5MB的Eden空间和2个4MB的Survivor 空间)以及472MB的老年代。

客观地说,考虑到该机器硬件的条件,15秒的启动时间其实还在可接受范围以内,但是从 VisualGC中反映的数据上看,存在的问题是非用户程序时间(图5-2中的Compile Time、Class Load Time、GC Time)占比非常之高,占了整个启动过程耗时的一半以上(这里存在少许夸张成分,因为如即时编译等动作是在后台线程完成的,用户程序在此期间也正常并发执行,最多就是速度变慢,所
以并没有占用一半以上的绝对时间)。虚拟机后台占用太多时间也直接导致Eclipse在启动后的使用过程中经常有卡顿的感觉,进行调优还是有较大价值的。

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

对Eclipse进行调优的第一步就是先对虚拟机的版本进行升级,希望能先从虚拟机版本身上得到一些“免费的”性能提升。把JDK版本升级到 JDK 6 Update 21,这次升级到JDK 6之后,性能有什么变化先暂且不谈,在使用几分钟之后,笔者的Eclipse就和前面几个服务端的案例一样非常“不负众望”地发生了内存溢出,如图5-5所示。
图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大小”的曲线不能向上发展,说明这次内存溢出很明显是永久代导致的内存溢出。
图5-6 Java堆监视曲线
图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运行期参数

-Dcom.sun.management.jmxremote 
-Dosgi.requiredJavaVersion=1.5 
-Xmx512m 

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

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

-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这个参数,问题随即解决。

[1] 版本升级也有不少性能倒退的案例,受程序、第三方包兼容性以及中间件限制,在企业应用中升级 JDK版本是一件需要慎重考虑的事情。
[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。

3、编译时间和类加载时间的优化

从Eclipse启动时间来看,升级到JDK 6所带来的性能提升是……嗯?基本上没有提升。多次测试的平均值与JDK 5的差距完全在实验误差范围之内。 各位读者不必失望,Sun公司给的JDK 6性能白皮书[1]描述的众多相对于JDK 5的提升并不至于全部是广告词,尽管总启动时间并没有减少,但在查看运行细节的时候,却发现了一件很令人玩味的事情:在JDK 6中启动完Eclipse所消耗的类加载时间比JDK 5长了接近一倍,读者注意不要看反了,这里写的是JDK 6的类加载比JDK 5慢一倍,测试结果见代码清单5-7,反复测试多次仍然是相似的结果。

代码清单5-7 JDK 5、JDK 6中的类加载时间对比

使用JDK 6的类加载时间: 
C:\Users\IcyFenix>jps 
3552 
6372 org.eclipse.equinox.launcher_1.0.201.R35x_v20090715.jar 
6900 Jps C:\Users\IcyFenix>jstat -class 6372 Loaded Bytes Unloaded Bytes Time 7917 10190.3 0 0.0 8.18 

使用JDK 5类加载时间: 
C:\Users\IcyFenix>jps 
3552 
7272 Jps 
7216 org.eclipse.equinox.launcher_1.0.201.R35x_v20090715.jar 
C:\Users\IcyFenix>jstat -class 7216 
Loaded Bytes      Unloaded   Bytes   Time 
7902     9691.2    3                2.6        4.34 

在本例中类加载时间上的差距并不能作为一个具有普适性的测试结论去说明JDK 6的类加载必然 比JDK 5慢,笔者测试了自己机器上的Tomcat和GlassFish启动过程,并没有出现类似的差距。在国内 最大的Java社区中,笔者发起过关于此问题的讨论[2]。从参与者反馈的测试结果来看,此问题只在一 部分机器上存在,而且在JDK 6的各个更新包之间,测试结果也存在很大差异。

经多轮试验后,发现在笔者机器上两个JDK进行类加载时,字节码验证部分耗时差距尤其严重,暂且认为是JDK 6中新加入类型检查验证器时,可能在某些机器上会影响到以前类型检查验证器的工作[3]。考虑到实际情况,Eclipse使用者甚多,它的编译代码我们可以认为是安全可靠的,可以不需要在加载的时候再进行字节码验证,因此通过参数-Xverify:none禁止掉字节码验证过程也可作为一项优化措施。加入这个参数后,两个版本的JDK类加载速度都有所提高,此时JDK 6的类加载速度仍然比 JDK 5要慢,但是两者的耗时已经接近了很多,测试结果如代码清单5-8所示。

代码清单5-8 JDK 1.5、1.6中取消字节码验证后的类加载时间对比

使用JDK 1.6的类加载时间:
C:\Users\IcyFenix>jps 
5512 org.eclipse.equinox.launcher_1.0.201.R35x_v20090715.jar 
5596 Jps 
C:\Users\IcyFenix>jstat -class 5512 
Loaded Bytes     Unloaded Bytes Time 
6749     8837.0   0              0.0      3.94 

使用JDK 1.5的类加载时间: 
C:\Users\IcyFenix>jps 
4724 org.eclipse.equinox.launcher_1.0.201.R35x_v20090715.jar 
5412 Jps 
C:\Users\IcyFenix>jstat -class 4724 
Loaded   Bytes    Unloaded   Bytes  Time 
6885       9109.7  3                2.6       3.10 

关于类与类加载的话题,譬如刚刚提到的字节码验证是怎么回事,本书专门规划了两个章节进行详细讲解,在此暂不再展开了。 在取消字节码验证之后,JDK 5的平均启动下降到了13秒,而在JDK 6的测试数据平均比JDK 5快 了1秒左右,下降到平均12秒,如图5-8所示。在类加载时间仍然落后的情况下,依然可以看到JDK 6在 性能上确实比JDK 5略有优势,说明至少在Eclipse启动这个测试用例上,升级JDK版本确实能带来一 些“免费的”性能提升。
图5-8 运行在JDK 6下取消字节码验证的启动时间
前面提到过,除了类加载时间以外,在VisualGC中监视曲线中显示了两项很大的非用户程序耗 时:编译时间(Compile Time)和垃圾收集时间(GC Time)。垃圾收集时间读者应该非常清楚了,而 编译时间是什么东西?程序在运行之前不是已经编译了吗?

虚拟机的即时编译与垃圾收集一样,是本书的一个重点部分,后面有专门章节讲解,这里先简要介绍一下:编译时间是指虚拟机的即时编译器(Just In Time Compiler)编译热点代码(Hot Spot Code)的耗时。我们知道Java语言为了实现跨平台的特性,Java代码编译出来后形成Class文件中储存的是字节码(Byte Code),虚拟机通过解释方式执行字节码命令,比起C/C++编译成本地二进制代码来说,速度要慢不少。为了解决程序解释执行的速度问题,JDK 1.2以后,HotSpot虚拟机内置了两个即时编译器[4],如果一段Java方法被调用次数到达一定程度,就会被判定为热代码交给即时编译器即时编译为本地代码,提高运行速度(这就是HotSpot虚拟机名字的来由)。而且完全有可能在运行期动态编译比C/C++的编译期静态编译出来的结果要更加优秀,因为运行期的编译器可以收集很多静态编译器无法得知的信息,也可以采用一些激进的优化手段,针对“大多数情况”而忽略“极端情况”进行假
设优化,当优化条件不成立的时候再逆优化退回到解释状态或者重新编译执行。所以Java程序只要代码编写没有问题(典型的是各种泄漏问题,如内存泄漏、连接泄漏),随着运行时间增长,代码被编译得越来越彻底,运行速度应当是越运行越快的。不过,Java的运行期编译的一大缺点就是它进行编译需要消耗机器的计算资源,影响程序正常的运行时间,这也就是上面所说的“编译时间”。

HotSpot虚拟机提供了一个参数-Xint来禁止编译器运作,强制虚拟机对字节码采用纯解释方式执行。如果读者想使用这个参数省下Eclipse启动中那2秒的编译时间获得一个哪怕只是“更好看”的启动成绩的话,那恐怕要大失所望了,加上这个参数之后虽然编译时间确实下降到零,但Eclipse启动的总时间却剧增到27秒,就是因为没有即时编译的支持,执行速度大幅下降了。现在这个参数最大的作用,除了某些场景调试上的需求外,似乎就剩下让用户缅怀一下JDK 1.2之前Java语言那令人心酸心碎的运行速度了。

与解释执行相对应的另一方面,HotSpot虚拟机还有另一个力度更强的即时编译器:当虚拟机运行在客户端模式的时候,使用的是一个代号为C1的轻量级编译器,另外还有一个代号为C2的相对重量级的服务端编译器能提供更多的优化措施。由于本次实战所采用的HotSpot版本还不支持多层编译,所以虚拟机只会单独使用其中一种即时编译器,如果使用客户端模式的虚拟机启动Eclipse将会使用到C2编译器,这时从VisualGC可以看到启动过程中虚拟机使用了超过15秒的时间去进行代码编译。如果读者的工作习惯是长时间不会关闭Eclipse的话,服务端编译器所消耗的额外编译时间最终是会在运行速度的提升上“赚”回来的,这样使用服务端模式是一个相当不错的选择。不过至少在本次实战中,我们还是继续选用客户端虚拟机来运行Eclipse。

[1] 白皮书:http://java.sun.com/performance/reference/whitepapers/6_performance.html。
[2] 笔者发起的关于JDK 6与JDK 5在Eclipse启动时类加载速度差异的讨论: http://www.javaeye.com/topic/826542。
[3] 这部分内容可常见第7章关于类加载过程的介绍。
[4] JDK 1.2之前也可以使用外挂JIT编译器进行本地编译,但只能与解释器二选其一,不能同时工作。

4、调整内存设置控制垃圾收集频率

三大块非用户程序时间中,还剩下“GC时间”没有调整,而“GC时间”却又是其中最重要的一块, 并不单单因为它是耗时最长的一块,更因为它是一个稳定持续的消耗。由于我们做的测试是在测程序 的启动时间,类加载和编译时间的影响力在这项测试里被大幅放大了。在绝大多数的应用中,都不可 能出现持续不断的类被加载和卸载。在程序运行一段时间后,随着热点方法被不断编译,新的热点方 法数量也总会下降,这都会让类加载和即时编译的影响随运行时间增长而下降,但是垃圾收集则是随 着程序运行而持续运作的,所以它对性能的影响才显得最为重要。

在Eclipse启动的原始数据样本中,短短15秒,类共发生了19次Full GC和378次Minor GC,一共397 次GC共造成了超过4秒的停顿,也就是超过1/4的时间都是在做垃圾收集,这样的运行数据看起来实在 太糟糕了。

首先来解决新生代中的Minor GC,尽管垃圾收集的总时间只有不到1秒,但却发生了378次之多。 从VisualGC的线程监视中看到Eclipse启动期间一共发起了超过70条线程,同时在运行的线程数超过25 条,每当发生一次垃圾收集,所有用户线程[1]都必须跑到最近的一个安全点然后挂起线程来等待垃圾 回收。这样过于频繁的垃圾收集就会导致很多没有必要的线程挂起及恢复动作。

新生代垃圾收集频繁发生,很明显是由于虚拟机分配给新生代的空间太小导致,Eden区加上一个 Survivor区的总大小还不到35MB。所以完全有必要使用-Xmn参数手工调整新生代的大小。

再来看一看那19次Full GC,看起来19次相对于378次Minor GC来说并“不多”,但总耗时有3.166 秒,占了绝大部分的垃圾收集时间,降低垃圾收集停顿时间的主要目标就是要降低Full GC这部分时 间。从VisualGC的曲线图上看得不够精确,这次直接从收集器日志[2]中分析一下这些Full GC是如何产 生的,代码清单5-9中是启动最开始的2.5秒内发生的10次Full GC记录。

代码清单5-9 Full GC记录
在这里插入图片描述
括号中加粗的数字代表着老年代的容量,这组GC日志显示,10次Full GC发生的原因全部都是老 年代空间耗尽,每发生一次Full GC都伴随着一次老年代空间扩容:1536KB→1664KB→2684KB→… →42056KB→46828KB。10次GC以后老年代容量从起始的1536KB扩大到46828KB,当15秒后Eclipse启 动完成时,老年代容量扩大到了103428KB,代码编译开始后,老年代容量到达顶峰473MB,整个Java堆到达最大容量512MB。

日志还显示有些时候内存回收状况很不理想,空间扩容成为获取可用内存的最主要手段,譬如这一句:
Tenured: 25092K->24656K(25108K) , 0.1112429 secs

代表老年代当前容量为25108KB,内存使用到25092KB的时候发生了Full GC,花费0.11秒把内存使用降低到24656KB,只回收了不到500KB的内存,这次垃圾收集基本没有什么回收效果,仅仅做了扩容,扩容过程相比起回收过程可以看作是基本不需要花费时间的,所以说这0.11秒几乎是平白浪费了。

由上述分析可以得出结论:Eclipse启动时Full GC大多数是由于老年代容量扩展而导致的,由永久代空间扩展而导致的也有一部分。为了避免这些扩展所带来的性能浪费,我们可以把-Xms和-XX:PermSize参数值设置为-Xmx和-XX:MaxPermSize参数值一样,这样就强制虚拟机在启动的时候就把老年代和永久代的容量固定下来,避免运行时自动扩展[3]。

根据以上分析,优化计划确定为:把新生代容量提升到128MB,避免新生代频繁发生Minor GC; 把Java堆、永久代的容量分别固定为512MB和96MB[4],避免内存扩展。这几个数值都是根据机器硬件和Eclipse插件、工程数量决定,读者实战的时候应依据VisualGC和日志里收集到的实际数据进行设置。改动后的eclipse.ini配置如代码清单5-10所示。

代码清单5-10 内存调整后的Eclipse配置文件

-vm 
D:/_DevSpace/jdk1.6.0_21/bin/javaw.exe 
-startup 
plugins/org.eclipse.equinox.launcher_1.0.201.R35x_v20090715.jar 
--launcher.library plugins/org.eclipse.equinox.launcher.win32.win32.x86_1.0.200.v20090519 
-product 
org.eclipse.epp.package.jee.product 
-showsplash 
org.eclipse.platform 
-vmargs 
-Dosgi.requiredJavaVersion=1.5 
-Xverify:none 
-Xmx512m 
-Xms512m 
-Xmn128m 
-XX:PermSize=96m 
-XX:MaxPermSize=96m 

现在这个配置之下,垃圾收集的次数已经大幅度降低,图5-9是Eclipse启动后一分钟的监视曲线, 只发生了8次Minor GC和4次Full GC,总耗时为1.928秒。
图5-9 GC调整后的运行数据
这个结果已经算是基本正常,但是还存在一点瑕疵:从Old Gen的曲线上看,老年代直接固定在 384MB,而内存使用量只有66MB,并且一直很平滑,完全不应该发生Full GC才对,那4次Full GC是 怎么来的?使用jstat-gccause查询一下最近一次GC的原因,见代码清单5-11。

代码清单5-11 查询GC原因
在这里插入图片描述
从LGCC(Last GC Cause)中看到原来是代码调用System.gc()显式触发的垃圾收集,在内存设置调整后,这种显式垃圾收集不符合我们的期望,因此在eclipse.ini中加入参数-XX:+DisableExplicitGC屏 蔽掉System.gc()。再次测试发现启动期间的Full GC已经完全没有了,只发生了6次Minor GC,总共耗 时417毫秒,与调优前4.149秒的测试结果相比,正好是十分之一。进行GC调优后Eclipse的启动时间下降非常明显,比整个垃圾收集时间降低的绝对值还大,现在启动只需要7秒多,如图5-10所示。
图5-10 Eclipse启动时间

[1] 严格来说,不包括正在执行native代码的用户线程,因为native代码一般不会改变Java对象的引用关系,所以没有必要挂起它们来等待垃圾回收。
[2] 可以通过以下几个参数要求虚拟机生成GC日志:-XX:+PrintGCTimeStamps(打印GC停顿时
间)、-XX:+PrintGCDetails(打印GC详细信息)、-verbose:gc(打印GC信息,输出内容已被前一个参数包括,可以不写)、-Xloggc:gc.log。
[3] 需要说明一点,虚拟机启动的时候就会把参数中所设定的内存全部划为私有,即使扩容前有一部分内存不会被用户代码用到,这部分内存也不会交给其他进程使用。这部分内存在虚拟机中被标识 为“Virtual”内存。
[4] 512MB和96MB两个数值对于笔者的应用情况来说依然偏少,但由于笔者需要同时开VMware虚拟机工作,所以需要预留较多内存,读者在实际调优时不妨再设置大一些。

5、选择收集器降低延迟

现在Eclipse启动已经比较迅速了,但我们的调优实战还没有结束,毕竟Eclipse是拿来写程序用 的,不是拿来测试启动速度的。我们不妨再在Eclipse中进行一个非常常用但又比较耗时的操作:代码 编译。图5-11是当前配置下,Eclipse进行代码编译时的运行数据,从图中可以看到,新生代每次回收耗时约65毫秒,老年代每次回收耗时约725毫秒。对于用户来说,新生代垃圾收集的耗时也还好,65毫秒的停顿在使用中基本无法察觉到,而老年代每次垃圾收集要停顿接近1秒钟,虽然较长时间才会出现一次,但这样的停顿已经是可以被人感知了,会影响到体验。

再注意看一下编译期间的处理器资源使用状况,图5-12是Eclipse在编译期间的处理器使用率曲线图,整个编译过程中平均只使用了不到30%的处理器资源,垃圾收集的处理器使用率曲线更是几乎与坐标横轴紧贴在一起,这说明处理器资源还有很多可利用的余地。
图5-11 编译期间运行数据
图5-12 编译期间CPU曲线
列举垃圾收集的停顿时间、处理器资源富余的目的,都是为了给接下来替换掉客户端模式的虚拟机中默认的新生代、老年代串行收集器做个铺垫。

Eclipse应当算是与使用者交互非常频繁的应用程序,由于代码太多,笔者习惯在做全量编译或者清理动作的时候,使用“Run in Background”功能一边编译一边继续工作。回顾一下在第3章提到的几种收集器,很容易想到在JDK 6版本下提供的收集器里,CMS是最符合这类场景的选择。我们在 eclipse.ini中再加入这两个参数,-XX:+UseConc-MarkSweepGC和-XX:+UseParNewGC(ParNew是使用CMS收集器后的默认新生代收集器,写上仅是为了配置更加清晰),要求虚拟机在新生代和老年代分别使用ParNew和CMS收集器进行垃圾回收。指定收集器之后,再次测试的结果如图5-13所示,与原来使用串行收集器对比,新生代停顿从每次65毫秒下降到了每次53毫秒,而老年代的停顿时间更是从725毫秒大幅下降到了36毫秒。
图5-13 指定ParNew和CMS收集器后的GC数据
当然,由于CMS的停顿时间只是整个收集过程中的一小部分,大部分收集行为是与用户程序并发进行的,所以并不是真的把垃圾收集时间从725毫秒直接缩短到36毫秒了。在收集器日志中可以看到 CMS与程序并发的时间约为400毫秒,这样收集器的运行结果就比较令人满意了。

到这里为止,对于虚拟机内存的调优基本就结束了,这次实战可以看作一次简化的服务端调优过程,服务端调优有可能还会在更多方面,如数据库、资源池、磁盘I/O等,但对于虚拟机内存部分的优化,与这次实战中的思路没有什么太大差别。即使读者实际工作中不接触到服务器,根据自己工作环境做一些试验,总结几个参数让自己日常工作环境速度有较大幅度提升也是很能提升工作幸福感的。 最终eclipse.ini的配置如代码清单5-12所示。

代码清单5-12 修改收集器配置后的Eclipse配置

-vm 
D:/_DevSpace/jdk1.6.0_21/bin/javaw.exe 
-startup plugins/org.eclipse.equinox.launcher_1.0.201.R35x_v20090715.jar 
--launcher.library plugins/org.eclipse.equinox.launcher.win32.win32.x86_1.0.200.v20090519 
-product org.eclipse.epp.package.jee.product 
-showsplash org.eclipse.platform
-vmargs 
-Dcom.sun.management.jmxremote 
-Dosgi.requiredJavaVersion=1.5 
-Xverify:none 
-Xmx512m
-Xms512m 
-Xmn128m 
-XX:PermSize=96m 
-XX:MaxPermSize=96m 
-XX:+DisableExplicitGC 
-Xnoclassgc 
-XX:+UseParNewGC 
-XX:+UseConcMarkSweepGC 
-XX:CMSInitiatingOccupancyFraction=85
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
提高 JAVA IDE 的性能的JVM开关 Submitted by 小天蝎 on 2005, August 18, 2:45 PM. integration 我的本本是p4 1.8G的dell c640 内存1G,eclipse 3.1 + myeclipse 4.0m2 速度还不错。 运行参数如下:eclipse.exe -vmargs -Xverify:none -XX:+UseParallelGC -XX:PermSize=20M -------------- JVM 提供了各种用于调整内存分配和垃圾回收行为的标准开关和非标准开关。其中一些设置可以提高 JAVA IDE 的性能。 注意,由于 -X (尤其是 -XX JVM)开关通常是 JVMJVM 供应商特定的,本部分介绍的开关可用于 Sun Microsystems J2SE 1.4.2。 以下设置在大多数系统上将产生比工厂更好的设置性能。 -vmargs - 表示将后面的所有参数直接传递到所指示的 Java VM。 -Xverify:none - 此开关关闭Java字节码验证,从而加快了类装入的速度,并使得在仅为验证目的而启动的过程中无需装入类。此开关缩短了启动时间,因此没有理由不使用它。 -Xms24m - 此设置指示 Java 虚拟机将其初始堆大小设置为 24 MB。通过指示 JVM 最初应分配给堆的内存数量,可以使 JVM 不必在 IDE 占用较多内存时增加堆大小。 -Xmx96m - 此设置指定 Java 虚拟机应对堆使用的最大内存数量。为此数量设置上限表示 Java 进程消耗的内存数量不得超过可用的物理内存数量。对于具有更多内存的系统可以增加此限制,96 MB 设置有助于确保 IDE 在内存量为 128MB 到 256MB 的系统上能够可靠地执行操作。注意:不要将该值设置为接近或大于系统的物理内存量,否则将在主要回收过程中导致频繁的交换操作。 -XX:PermSize=20m - 此 JVM 开关不仅功能更为强大,而且能够缩短启动时间。该设置用于调整内存"永久区域"(类保存在该区域中)的大小。因此我们向 JVM 提示它将需要的内存量。该设置消除了许多系统启动过程中的主要垃圾收集事件。SunONE Studio 或其它包含更多模块的 IDE 的用户可能希望将该数值设置得更高。 下面列出了其它一些可能对 ECLIPSE 在某些系统(不是所有系统)上的性能产生轻微或明显影响的 JVM 开关。尽管使用它们会产生一定的影响,但仍值得一试。 -XX:CompileThreshold=100 - 此开关将降低启动速度,原因是与不使用此开关相比,HotSpot 能够更快地将更多的方法编译为本地代码。其结果是提高了 IDE 运行时的性能,这是因为更多的 UI 代码将被编译而不是被解释。该值表示方法在被编译前必须被调用的次数。 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC - 如果垃圾回收频繁中断,则请尝试使用这些开关。此开关导致 JVM 对主要垃圾回收事件(如果在多处理器工作站上运行,则也适用于次要回收事件)使用不同的算法,这些算法不会影响整个垃圾回收进程。注意:目前尚不确定此收集器是提高还是降低单处理器计算机的性能。 -XX:+UseParallelGC - 某些测试表明,至少在内存配置相当良好的单处理器系统中,使用此回收算法可以将次要垃圾回收的持续时间减半。注意,这是一个矛盾的问题,事实上此回收器主要适用于具有千兆字节堆的多处理器。尚无可用数据表明它对主要垃圾回收的影响。注意:此回收器与 -XX:+UseConcMarkSweepGC 是互斥的。=====================================================================================================================建议启动参数:c:\eclipse\eclipse.exe -vmargs -Xverify:none -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值