Tomcat有关JVM的部分优化及原因

      之前在网上有看到过很多关于Tomcat启动参数的设置,有不少类似于标准的设置参考,但是很多解释比较模糊,在阅读过《深入理解java虚拟机》一书之后,其中主要涉及的JVM优化想跟大家分享一下,如有错误还请指正。

      一般优化就是在catalina.bat或者catalina.sh的首行之前加入JAVA_OPTS="",其中catalina.bat可以不用引号,catalina.sh必须加上引号,否则不生效也没有任何提示。目前我的环境主要是是4G内存的windows server2008和CentOS5.9的64系统,然后是JDK1.7_45的64位虚拟机(也就是HotSpot),tomcat版本为7.0.42。大概是为了实现每秒2000次响应,其中算法和数据库时间都在100ms以下,而且都能够承受如此频率的响应。我的配置开始也是照抄的网上大神们的,后面了解之后稍微做了一些修改如下:

set JAVA_OPTS="-server -Xms2048M -Xmx2048M -Xss256k -XX:+AggressiveOpts -XX:+UseBiasedLocking -XX:PermSize=128M -XX:MaxPermSize=256M -XX:+DisableExplicitGC -XX:MaxTenuringThreshold=31 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=96m -XX:-UseFastAccessorMethods -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSInitiatingOccupancyOnly -verbose:gc -Xloggc:../logs/gc.log -XX:+PrintGCDetails -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true"

 下面介绍以下每一项的意思,可能不是特别全面而且用词可能不是特别准确,主要是根据书中所说进行的简单总结。

 1."-server":启动虚拟机运行在server模式下,默认是在client模式下。这两个模式大致有两个方面的区别默认垃圾收集器和编译行为。垃圾收集器,client模式下默认为新生代Serial收集器+老年代Serial Old收集器,首先这两个收集器都是单线程的,既是只会用一个CPU或一条收集线程去完成垃圾收集工作。而且这两个垃圾收集器都会有"Stop The World"的不良用户体验,既是在用户不可见的情况下吧用户正常工作的线程全部停掉,当然新生代还是采取的复制算法,老年代为标记整理算法,如下图
Tomcat有关JVM的部分优化及原因_第1张图片
 
    而在server模式下新生代默认采用的Parallel Scavenge收集器,老年代采用的Serial Old收集器(因为没有其他老年代收集器能与Parallel Scavenge收集器配合)。其中Parallel Scavenge收集器是可以达到一个可控的吞吐量的收集器,可以使用配置"-XX:MaxGCPauseMillis"参数和"-XX:GCTimeRatio"参数来指定最长停顿时间和吞吐量大小,吞吐量即是GC时间占总CPU时间的比值,比如将GCTimeRatio配置为19,那么允许的最大GC时间就占总时间的5%(1/(1+19))。

    关于编译,HotSpot虚拟机中内置了两个即时编译器,分别为Client Compiler(C1编译器)和Server Compiler(C2编译器),他们都是虚拟机的后端运行期编译器(JIT编译器,Just In Time Compiler)把字节码转变成机器码的过程。因为最初Java程序是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认作”热点代码“(Hot Spot Code),为了提高执行效率,运行时虚拟机会将这些代码编译为与本地平台有关的机器码,并进行各种优化,完成这项任务的编译器就是JIT编译器。大致可以简单的认为C1的编译速度较快,C2的编译质量较高(优化更多),但其实无论在server还是client模式下都可以采用分层编译(Tiered Compilation),其中jdk7的server模式分层编译默认开启。也可以通过"-XX:+TieredCompilation"手动开启。这个参数有助于在程序启动响应速度和运行效率之间达到平衡。server模式和client模式在Hot Spot Code的判定上面也有所不同,判定方法是否为Hot Spot Code见后文关于"-XX: CompileThreshold"参数介绍,在判定代码块(循环语句等)时,server的默认值会比client更宽松,也就是server会做更多的编译工作。在其他方面server模式会进行一些更复杂和激进的编译优化,本人才疏学浅不作介绍,可以参见Sun官方Wiki百科。

 

2."-Xms2048m":设置JVM堆的最小大小为2048m,一般跟最大大小设置为一样,避免动态分配影响性能。

 

3."-Xmx2048m":设置JVM堆的最大大小为2048m,需要注意的是本地方法栈、方法区和直接内存都不在这个范围之中,所以需要考虑到这部分内存加上堆内存才是整个应用所使用的内存。而且32位虚拟机实际支持的内存为1.5~2G,64位无限制。

 

4."-Xss256k":设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

 

5."-XX:+AggressiveOpts":使用激进的优化特性,主要是在编译期间,jdk6就已经默认开启了,可以不指定,但需要测试效果,因为可能激进优化的假设不成立,优化后导致比如加载新类后类型继承结构变化、出现罕见陷阱等情况时(此处优化是由C2编译器进行),又会通过逆优化退回到解释转台继续执行(或者是C1编译器编译),反而降低了性能。

 

6."-XX:+UseBiasedLocking":使用偏向锁,也是jdk6的默认值,他的目的是消除数据在无竞争的情况下的同步原语,就是说把不需要同步的同步语句去掉。实现主要是偏向第一个使用该同步资源的线程,所以叫偏向锁。但这也是一个带有效益权衡性质的优化,如果代码中大多数同步语句都是数据有竞争的情况下,禁止("-XX:-UseBiasedLocking")反而可以提升性能。

 

7."-XX:PermSize=128M -XX:MaxPermSize=256M":分别设置永久代初始大小,和永久代最大大小。永久代也就是方法区,虽然是永久代但其实也并不是永远不变,因为会涉及到类型的卸载,不过卸载类的要求相当严格。

 

8."-XX:+DisableExplicitGC":忽略用户代码中手动调用GC(system.gc())。

 

 9."-XX:MaxTenuringThreshold=31":新生代中对象的年龄到达多少岁时晋升到老年代(一次minorGC年龄加1),该默认值为15。需要注意的是JVM为了适应不同程序的内存状况并不是一定要求只有年龄达到MaxTenuringThreshold才能晋升到老年代,如果在Survicor空间中相同年龄所有对象大小的和大于Survivor空间的一半,年龄大于或等于该年龄的对象就会直接进入老年代,这称为动态对象年龄判定。例如,在上述配置条件下,新生代默认占对内存的1/3(其余为老年代,可以通过"-Xmn768m"来指定新生代大小为768m),则默认的Survivor区占新生代的1/10大致为68m,如果此时执行minorGC时,Eden区域存活的对象中10岁的对象的大小的和大于68M,那么这次minorGC之后,Eden区域中存活的大于或等于10岁的对象就都会直接进入老年代,而不是等到31岁。

    注:因为新生代的对象大多具备朝生夕死的特点,新生代一般采取复制算法,这样效率会很快,具体为将新生代分为三块区域,Eden区和两块Survivor区比例为8:1(可以通过配置"-XX:SurvivorRatio=8"指定新生代中Eden:Survivor为8:1,默认比例),每次只使用Eden区和一块Survivor区(即使用新生代的90%,只会浪费10%的空间,而且据IBM公司的研究,新生代中98%的对象都是朝生夕死),剩下一块Survivor区用作minorGC时将原来Eden区和另一块Survivor区存活的对象复制过去,而此时如果这块Survivor区不够用就需要向老年代分配(比如上述的动态对象年龄判定条件满足时)。

 

10."XX:+UseConcMarkSweepGC":使用Concurrent Mark Sweep收集器(CMS收集器)。其主要特点是获取最短回收停顿时间的收集器,其收集步骤分为4步:初始标记(CMS initial mark),并发标记(CMS concurrent mark),重新标记(CMS remark),并发清除(CMS concurrent sweep)。其中初始标记和重新标记仍需要Stop The World,但初始标记只是标记GC Roots能直接关联到的对象(采用的垃圾收集算法为根搜索算法),并发标记就是进行GC Roots搜索确定对象是否“已死”,而重新标记是为了修正并发标记时程序继续运作造成的改变,重新标记需要的时间远低于并发标记,所以整个收集过程停顿时间会很短,如下图:
Tomcat有关JVM的部分优化及原因_第2张图片
 

 11."-XX:+UseParNewGC":新生代采用ParNew收集器。ParNew收集器除了多线程收集之外其他和Serial收集器类似,以致在单cpu情况下效果不如Serial,甚至双cpu情况下由于线程交互开销也不能完全保证比Serial更优秀。但是,采用它的原因是只有它能与CMS收集器配合工作。其收集过程如下图:

Tomcat有关JVM的部分优化及原因_第3张图片
 

12."-XX:+CMSParallelRemarkEnabled":使CMS垃圾收集时,第二次标记(前文描述CMS中的重新标记)采用多线程方式,本人没有测试默认值是否开启。

 

13."-XX:+UseCMSCompactAtFullCollection":设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理,默认开启,关闭可能会导致老年代碎片过多分配大对象失败。

 

14."-XX:LargePageSizeInBytes=96m":设置java进程大内存页每页大小,需要操作系统支持。详细请参见博客:http://kenwublog.com/tune-large-page-for-jvm-optimization

 

15."-XX:-UseFastAccessorMethods":设置关闭快速调用成员方法,这里表述可能不是太准确。首先说明一下什么方法叫做AccessorMethods,①必须是成员方法,静态方法不行,②返回值类型必须是引用类型或者int,其它都不算,③方法体的代码必须满足aload_0; getfield #index; areturn或ireturn这样的模式,方法名是什么都没关系,是不是get、is、has开头都不重要。 因为此类方法方法体很简单,而且没有方法计数器,开启此设置后可以跳过对该类方法的编译,详细参见http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2011-March/005057.html该设置默认在jdk6中是开启的,但由于jdk7的server模式默认开启了多层编译,此时在这种多台方法调用时甚至会导致性能下降,所以设置关闭(jdk7是否默认关闭不太清楚),但仍需要根据自己项目测试。

 

16."-XX:CMSInitiatingOccupancyFraction=70":CMS垃圾收集老年代时触发条件,当老年代使用的比例占用到整个老年代的70%时,执行一次Full GC(major GC),默认值90%,如有错误还请留言指正。

注:本人没有自己编译过JDK,网上资料主要有两种说法,90%和68%,经本人在上述环境测试,没有设置改制时应用运行一段时间会自己挂掉,没有输出任何日志,gc日志显示正在执行minor GC时突然挂掉,配置为70%后没有出现过这种情况,所以我偏向认为默认值为90%。

 

17."-XX:+UseCMSInitiatingOccupancyOnly":CMS GC触发的条件为旧生代已使用的空间达到设定的CMSInitiatingOccupancyFraction百分比,例如默认CMSInitiatingOccupancyFraction为90%,如旧生代空间为1 000MB,那么当旧生代已使用的空间达到900MB时,CMS GC即开始执行;另外一种触发方式是JVM自动触发,JVM基于之前GC的频率及旧生代的增长趋势来评估决定什么时候执行CMS GC,如果不希望JVM自行触发,可设置该参数。

 

18."-verbose:gc":输出GC前后内存情况,但并不详细,如,

[Full GC 168K->97K(1984K), 0.0253873 secs]

 

19."-Xloggc:../logs/gc.log":将GC日志输出到gc.log文件中,此处是相对路径,Tomcat则是在TOMCAT_HOME/logs/gc.log。

 

20."-XX:+PrintGCDetails":输出GC的详细信息,如,

33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]

 其中33.125指的是JVM启动之后经过的时间,GC指的就是minor GC,DefNew指的是新生代(默认的收集器),3324K(GC前)->152K(GC后)(3712K(总大小)),0.0025925花费的时间3324K->152K(11904K)指的是整个JVM堆的大小变化,后面的Full GC中Tenured既是老年代,此处应该是新生代对象晋升到老年代所以导致老年代大小反而增加。Perm既是永久代,后面的user,sys,real和Linux的time命令输出时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU时间以及操作从开始到结束所经过的真实时间。

 

21."-Djava.awt.headless=true": Headless模式是系统的一种配置模式。在该模式下,系统缺少了显示设备、键盘或鼠标。 Headless模式虽然不是我们愿意见到的,但事实上我们却常常需要在该模式下工作,尤其是服务器端程序开发者。因为服务器(如提供Web服务的主机)往往可能缺少前述设备,但又需要使用他们提供的功能,生成相应的数据,以提供给客户端(如浏览器所在的配有相关的显示设备、键盘和鼠标的主机)。 一般是在程序开始激活headless模式,告诉程序,现在你要工作在Headless mode下,如果服务程序并不需要图像处理可以不设置。

 

22."-Djava.net.preferIPv4Stack=true":禁用IPv6,因为本人应用中会用到JGroups作为分布式内存数据库,而在使用IPv6的地址会无法识别加入到集群,所以禁用掉了,大家可以根据自己应用情况决定。

你可能感兴趣的:(jvm,tomcat,优化,虚拟机,GC)