《深入理解Java虚拟机》——实战:eclipse优化

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

实战:eclipse运行速度调优

很多Java开发人员都有这样一个观念:系统调优的工作都是针对服务端应用而言的,规模越大的系统,需要越专业的调优运维团队参与。这个观念不能说不对,但是并不说明其他应用就不需要了。

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

笔者使用Spring Tool Suite(以下简称STS,Spring提供的带Spring插件的eclipse)作为日常主要IDE工具,由于插件较多、代码也较多,启动STS直到所有项目编译完成需要四五分钟。

笔者机器使用64位Windows 7系统,虚拟机为HotSpot 64-Bit Servier VM 1.7.0-67,Intel i5的CPU,8GB的物理内存。在初始的配置文件sts.ini中未作任何改动,初始配置内容如下:

-startup
plugins/org.eclipse.equinox.launcher_1.3.0.v20130327-1440.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.200.v20130521-0416
-product
org.springsource.sts.ide
--launcher.defaultAction
openFile
--launcher.XXMaxPermSize
256M
-vmargs
-Dosgi.requiredJavaVersion=1.6
-Xms40m
-Xmx768m
-XX:MaxPermSize=256m

为了与调优后的结果进行量化对比,调优开始前做一次初始数据测试。测试用例很简单,就是收集从STS启动开始,直到所有插件加载完成为止的总耗时及运行状态数据,虚拟机的运行数据通过VisualVM及其扩展插件VisualGC进行采集。测试过程中反复启动STS数次直到测试结果稳定后,取最后一次运行结果作为数据样本。样本如下:

《深入理解Java虚拟机》——实战:eclipse优化_第1张图片

STS启动的总耗时没有办法从监控工具中直接获得,因为VisualVM不可能只奥STS运行到什么阶段算是启动完成。为了保证测试的准确性,作者写了一个简单的eclipse插件,用于统计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启动耗时
 */
public class ShowTime implements IStartup {
public void earlyStartup(){
    Display.getDefault().syncExec(new Runnable(){
        public void run(){
            long startTime= Long.parseLong(System.getProperty("eclipse.startTime"));
            lont costTime = System.currentTimeMillis() - startTime;
            Shell shell = Display.getDefault().getActiveShell();
            String message = "Eclipse startup cost tiem : "+costTime +"ms";
            MessageDialog.openInformation(shell,"Infomation",message);
        }
    });
}
}

plugin.xml代码:



    
        
    

上述代码打包成jar后,放到STS的plugins目录下,反复启动几次后,插件显示的平均时间稳定在15秒左右,如下图所示:

《深入理解Java虚拟机》——实战:eclipse优化_第2张图片

根据VisualGC和Eclipse插件收集到的信息,总结原始配置下的测试结果如下:

  • 整个启动过程平均耗时约15秒。

  • 最后一次的数据样本中,垃圾收集总耗时1.084s,其中:

  • Full GC被触发了3次,共耗时763.908ms。

  • Minor GC被触发了16次,共耗时319.876ms。

  • 加载类13849个,耗时11.149秒。

  • JIT编译时间为19.669秒。

  • 虚拟机937MB的堆内存被分配给425MB的新生代(255MB的Eden空间和2个85M的Survivor空间)及512MB的老年代。

有个疑惑,明明我的虚拟机配的“-Xmx768m”怎么一算会有937的堆内存……当然因为我截图速度慢了点,估计这就是JIT编译时间超15s的原因。

不过从上述数据可以看出非用户程序时间(Compile Time、Class Loader Time、 GC Time)非常高,JIT应该是因为和用户程序同步进行,但是占用的时间也是比较大的。

作者在著书时,从JDK1.5升级到1.6妄图从版本升级中享受到性能提升,这部分我就略过了,因为我直接用的64位JDK1.7作为我的本机环境。

2. 编译时间和类加载时间的优化

eclipse使用者众多,并且暂时也没有能力去更改eclipse,所以将它的编译代码认为是可靠的,需要在加载的时候再进行字节码验证,通过参数-Xverify:none禁止掉字节码验证过程作为一项优化措施。

在取消字节码验证之后,平均启动降到了14秒左右。前面说过,除了类加载时间之外,在VisualGC的监视曲线中显示了两项很大的非用户程序耗时:编译时间(Compile Time)和垃圾收集时间(GC Time)。编译时间是什么?编译时间是指虚拟机的JIT编译器(Just In Time Compiler)编译热点代码(Hot Spot Code)的耗时。Java为了实现跨平台的特性,Java代码编译出的Class文件中储存的是字节码(ByteCode),虚拟机通过解释方式执行字节码命令,比起C/C++编译成本地二进制代码来说,速度要慢不少。为了解决程序解释执行的速度问题,JDK1.2以后,虚拟机内置了两个运行时编译器,如果一段Java方法被调用的次数达到一定程度,就会被判定为热代码,从而交给JIT编译器即时编译为本地代码,以提高运行速度(这就是HotSpot虚拟机名字的由来)。甚至有可能在运行期动态编译比C/C++编译成本地二进制代码更优秀,因为运行期可以收集很多编译器无法知道的信息,甚至可以采用一些很激进的优化手段,在优化条件不成立的时候再逆优化退回来。所以Java程序只要代码没有问题(主要是泄露问题,如内存泄露、连接泄露),随着代码被编译得越来越彻底,运行速度应当是越来越快的。Java运行期编译最大的缺点就是编译所需小号程序的正常运行时间,也就是上面说的“编译时间”。

虚拟机提供了一个参数-Xint禁止编译器运作,强制虚拟机对字节码采用纯解释方式执行。测试一下,果然如作者所说,这个参数无法优化启动速度,反而变成了42秒……

与解释执行相对应的另一方面,虚拟机还有力度更强的编译器:当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器,另外还有一个代号为C2的相对重量级的编译器,它能提供更多的优化措施。如果使用-server模式的虚拟机启动Eclipse将会使用到C2编译器,这时从VisualGC可以看到启动过程中虚拟机使用了超过15秒的时间去进行代码编译。如果工作习惯长时间不关闭Eclipse的话,C2编译器所消耗的额外编译时间最终还是会在运行速度的提升中赚回来,使用-server模式也是一个不错的选择。实际上我突然才发现我一直用的是-server虚拟机(通过java -version查看)。

3. 调整内存设置控制垃圾收集频率

添加-XX:+PrintGCTimeStamps、-XX:+PrintGCDetails、-verbose:gc、-Xloggc:gc.log参数,再启动STS,查看STS安装路径下的gc.log:

0.618: [GC [PSYoungGen: 10752K->1528K(12288K)] 10752K->2469K(39936K), 0.0034314 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.889: [GC [PSYoungGen: 12280K->1528K(12288K)] 13221K->5404K(39936K), 0.0045233 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
1.258: [GC [PSYoungGen: 12280K->1536K(12288K)] 16156K->8429K(39936K), 0.0047783 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
1.461: [GC [PSYoungGen: 12288K->1536K(23040K)] 19181K->11169K(50688K), 0.0049524 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
1.724: [GC [PSYoungGen: 23040K->1536K(23040K)] 32673K->16800K(50688K), 0.0051438 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
1.985: [GC [PSYoungGen: 23040K->7671K(49152K)] 38304K->23101K(76800K), 0.0061606 secs] [Times: user=0.06 sys=0.00, real=0.01 secs] 
2.453: [GC [PSYoungGen: 49143K->9197K(50688K)] 64573K->26409K(78336K), 0.0095502 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
3.888: [GC [PSYoungGen: 50669K->11232K(90112K)] 67881K->34014K(117760K), 0.0185893 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] 
3.906: [Full GC [PSYoungGen: 11232K->3013K(90112K)] [ParOldGen: 22782K->27420K(52224K)] 34014K->30433K(142336K) [PSPermGen: 16999K->16995K(34304K)], 0.1309900 secs] [Times: user=0.30 sys=0.00, real=0.13 secs] 
5.190: [GC [PSYoungGen: 81861K->15329K(94208K)] 109281K->47036K(146432K), 0.0266980 secs] [Times: user=0.06 sys=0.00, real=0.03 secs] 
5.505: [GC [PSYoungGen: 94177K->13867K(114176K)] 125884K->47392K(166400K), 0.0136559 secs] [Times: user=0.06 sys=0.00, real=0.01 secs] 
5.786: [GC [PSYoungGen: 108587K->14107K(115200K)] 142112K->47632K(167424K), 0.0105256 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]

6秒内发生了12次GC。对比发现后面几次发生GC后几乎都有扩容的现象,原因是新生代空间耗尽。而每次Full GC都是因为老年代空间耗尽。

空间扩容是获取可用内存的最主要的手段,譬如“ParOldGen: 103279K->103070K(167424K)”代表老年代的当前容量为167424KB,内存使用到103279KB的时候发生Full GC,回收后内存使用降到了103070KB,回收了100KB左右内存,这次GC对老年代基本没有效果,继续查看后面时间为0.6秒,这个0.6秒基本就是白用功了。

结论:Eclipse启动时Full GC大多数是由于老年代和永久代(这里我的日志显示永久代也随着Full GC而每次扩容)容量扩展而导致的。为了避免这些扩容所带来的性能浪费,我们可以把-Xms和-XX:PermSize参数分别设置为-Xmx和-XX:MaxPermSize参数值,强制虚拟机在启动的时候就把老年代和永久代的容量固定下来,避免运行时自动扩展。调整后,配置文件为:

-startup
plugins/org.eclipse.equinox.launcher_1.3.0.v20130327-1440.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.200.v20130521-0416
-product
org.springsource.sts.ide
--launcher.defaultAction
openFile
-vmargs
-Dosgi.requiredJavaVersion=1.6
-Xms1024m
-Xmx1024m
-XX:PermSize=512m
-XX:MaxPermSize=512m
-Xverify:none

现在这个配置下GC次数大幅度降低,再查看

C:\Users\Administrator>jps
8008 Jps
6724 org.eclipse.equinox.launcher_1.3.0.v20130327-1440.jar
5848 Main
C:\Users\Administrator>jstat -gccause 6724
  S0    S1    E     O      P     YGC     YGCT    FGC    FGCT     GCT    LGCC GCC
  0.00  0.00  1.03  11.65  14.17  57  1.247 46   15.016   16.263 System.gc() No GC

从LGCC(Last GC Cause)总看到原来是代码调用System.gc()显示触发的GC,从内存设置调整后,这种显示GC不符合我们的希望,因此在sts.ini中加入参数-XX:+DisableExplicitGC屏蔽掉System.gc()。再次测试发现启动期间的Full GC已经完全没有了,只有4次的Minor GC,耗时300毫秒左右,和调优前有所降低。只是最后我STS启动时间还是在14、5秒左右,和作者的调优效果天差地远,毕竟我的硬件环境和软件环境都有所不同。还有作者写书时间为2010年,而我是在2014做的操作……

4.选择收集器降低延迟

启动时间调优结束,但是Eclipse是用来写程序的,不是用来启动着玩的,所以,再测试一个常用但耗时的操作:代码编译。我从VisualVM里看到每次新生代回收耗时大约60ms左右,老年代则压根没有回收操作(好像我应该把老年代内存调小,给新生代让位了……)。

《深入理解Java虚拟机》——实战:eclipse优化_第3张图片

再来看下编译期的CPU资源使用情况,下图是CPU使用率曲线图,整个编译过程中(下午4:20之后)平均使用了不到40%的CPU资源,垃圾收集的CPU使用曲线基本就是和坐标横轴贴在一起,这说明CPU资源还有很多可利用的余地。

《深入理解Java虚拟机》——实战:eclipse优化_第4张图片

列举GC停顿时间、CPU资源富余的目的,都是为了替换Client模式的虚拟机中默认的心声带和老年代串行收集器做铺垫(我已经是Server模式了……)。大家应该都是习惯后台编译的,这样能一边编译一边继续工作。所以CMS收集器是最符合该场景的收集器。在ini文件中加入两个参数-XX:+UseConcMarkSweepGC和-XX:+UseParNewGC(ParNew收集器是使用CMS收集器后默认的新生代收集器,写上仅是为了配置更加清晰),要求虚拟机在新生代和老年代分别使用ParNew和CMS收集器进行垃圾回收。结果显示新生代停顿从60ms涨到了80ms,说明原来默认的收集器比后来配置的好(我用的是64位JDK1.7)。而老年代也进行了2次回收,并耗时114.9ms,结果令人不满意,回退优化。

优化结束。

转载于:https://my.oschina.net/daidetian/blog/305209

你可能感兴趣的:(《深入理解Java虚拟机》——实战:eclipse优化)