最全面的JVM优化经验总结
开始之前
Java虚拟机有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
注意:本文仅针对JDK7、HotSPOTJava虚拟机,对于JDK8引入的JVM新特性及其他Java虚拟机,本文不予关注。
我们以一个例子开始这篇文章。假设你是一个普通的Java对象,你出生在Eden区,在Eden区有许多和你差不多的小兄弟、小姐妹,可以把Eden区当成幼儿园,在这个幼儿园里大家玩了很长时间。Eden区不能无休止地放你们在里面,所以当年纪稍大,你就要被送到学校去上学,这里假设从小学到高中都称为Survivor区。开始的时候你在Survivor区里面划分出来的的“From”区,读到高年级了,就进了Survivor区的“To”区,中间由于学习成绩不稳定,还经常来回折腾。直到你18岁的时候,高中毕业了,该去社会上闯闯了。于是你就去了年老代,年老代里面人也很多。在年老代里,你生活了20年(每次GC加一岁),最后寿终正寝,被GC回收。有一点没有提,你在年老代遇到了一个同学,他的名字叫爱德华(慕光之城里的帅哥吸血鬼),他以及他的家族永远不会死,那么他们就生活在永生代。
本文主要讲讲如何运用这些区域,为系统性能提供更好的帮助。本文不再重复这些概念,直接进入主题。
如何将新对象预留在年轻代
众所周知,由于FullGC的成本远远高于MinorGC,因此某些情况下需要尽可能将对象分配在年轻代,这在很多情况下是一个明智的选择。虽然在大部分情况下,JVM会尝试在Eden区分配对象,但是由于空间紧张等问题,很可能不得不将部分年轻对象提前向年老代压缩。因此,在JVM参数调优时可以为应用程序分配一个合理的年轻代空间,以最大限度避免新对象直接进入年老代的情况发生。清单1所示代码尝试分配4MB内存空间,观察一下它的内存使用情况。
清单1.相同大小内存分配
publicclassPutInEden{ publicstaticvoidmain(String[]args){ byte[]b1,b2,b3,b4;//定义变量 b1=newbyte[1024*1024];//分配1MB堆空间,考察堆空间的使用情况 b2=newbyte[1024*1024]; b3=newbyte[1024*1024]; b4=newbyte[1024*1024]; } }
使用JVM参数-XX:+PrintGCDetails-Xmx20M-Xms20M运行清单1所示代码,输出如清单2所示。
清单2.清单1运行输出
[GC[DefNew:5504K->640K(6144K),0.0114236secs]5504K->5352K(19840K), 0.0114595secs][Times:user=0.02sys=0.00,real=0.02secs] [GC[DefNew:6144K->640K(6144K),0.0131261secs]10856K->10782K(19840K), 0.0131612secs][Times:user=0.02sys=0.00,real=0.02secs] [GC[DefNew:6144K->6144K(6144K),0.0000170secs][Tenured:10142K->13695K(13696K), 0.1069249secs]16286K->15966K(19840K),[Perm:376K->376K(12288K)], 0.1070058secs][Times:user=0.03sys=0.00,real=0.11secs] [FullGC[Tenured:13695K->13695K(13696K),0.0302067secs]19839K->19595K(19840K), [Perm:376K->376K(12288K)],0.0302635secs][Times:user=0.03sys=0.00,real=0.03secs] [FullGC[Tenured:13695K->13695K(13696K),0.0311986secs]19839K->19839K(19840K), [Perm:376K->376K(12288K)],0.0312515secs][Times:user=0.03sys=0.00,real=0.03secs] [FullGC[Tenured:13695K->13695K(13696K),0.0358821secs]19839K->19825K(19840K), [Perm:376K->371K(12288K)],0.0359315secs][Times:user=0.05sys=0.00,real=0.05secs] [FullGC[Tenured:13695K->13695K(13696K),0.0283080secs]19839K->19839K(19840K), [Perm:371K->371K(12288K)],0.0283723secs][Times:user=0.02sys=0.00,real=0.01secs] [FullGC[Tenured:13695K->13695K(13696K),0.0284469secs]19839K->19839K(19840K), [Perm:371K->371K(12288K)],0.0284990secs][Times:user=0.03sys=0.00,real=0.03secs] [FullGC[Tenured:13695K->13695K(13696K),0.0283005secs]19839K->19839K(19840K), [Perm:371K->371K(12288K)],0.0283475secs][Times:user=0.03sys=0.00,real=0.03secs] [FullGC[Tenured:13695K->13695K(13696K),0.0287757secs]19839K->19839K(19840K), [Perm:371K->371K(12288K)],0.0288294secs][Times:user=0.03sys=0.00,real=0.03secs] [FullGC[Tenured:13695K->13695K(13696K),0.0288219secs]19839K->19839K(19840K), [Perm:371K->371K(12288K)],0.0288709secs][Times:user=0.03sys=0.00,real=0.03secs] [FullGC[Tenured:13695K->13695K(13696K),0.0293071secs]19839K->19839K(19840K), [Perm:371K->371K(12288K)],0.0293607secs][Times:user=0.03sys=0.00,real=0.03secs] [FullGC[Tenured:13695K->13695K(13696K),0.0356141secs]19839K->19838K(19840K), [Perm:371K->371K(12288K)],0.0356654secs][Times:user=0.01sys=0.00,real=0.03secs] Heap defnewgenerationtotal6144K,used6143K[0x35c10000,0x362b0000,0x362b0000) edenspace5504K,100%used[0x35c10000,0x36170000,0x36170000) fromspace640K,99%used[0x36170000,0x3620fc80,0x36210000) tospace640K,0%used[0x36210000,0x36210000,0x362b0000) tenuredgenerationtotal13696K,used13695K[0x362b0000,0x37010000,0x37010000) thespace13696K,99%used[0x362b0000,0x3700fff8,0x37010000,0x37010000) compactingpermgentotal12288K,used371K[0x37010000,0x37c10000,0x3b010000) thespace12288K,3%used[0x37010000,0x3706cd20,0x3706ce00,0x37c10000) rospace10240K,51%used[0x3b010000,0x3b543000,0x3b543000,0x3ba10000) rwspace12288K,55%used[0x3ba10000,0x3c0ae4f8,0x3c0ae600,0x3c610000)
清单2所示的日志输出显示年轻代Eden的大小有5MB左右。分配足够大的年轻代空间,使用JVM参数-XX:+PrintGCDetails-Xmx20M-Xms20M-Xmn6M运行清单1所示代码,输出如清单3所示。
清单3.增大Eden大小后清单1运行输出
[GC[DefNew:4992K->576K(5568K),0.0116036secs]4992K->4829K(19904K), 0.0116439secs][Times:user=0.02sys=0.00,real=0.02secs] [GC[DefNew:5568K->576K(5568K),0.0130929secs]9821K->9653K(19904K), 0.0131336secs][Times:user=0.02sys=0.00,real=0.02secs] [GC[DefNew:5568K->575K(5568K),0.0154148secs]14645K->14500K(19904K), 0.0154531secs][Times:user=0.00sys=0.01,real=0.01secs] [GC[DefNew:5567K->5567K(5568K),0.0000197secs][Tenured:13924K->14335K(14336K), 0.0330724secs]19492K->19265K(19904K),[Perm:376K->376K(12288K)], 0.0331624secs][Times:user=0.03sys=0.00,real=0.03secs] [FullGC[Tenured:14335K->14335K(14336K),0.0292459secs]19903K->19902K(19904K), [Perm:376K->376K(12288K)],0.0293000secs][Times:user=0.03sys=0.00,real=0.03secs] [FullGC[Tenured:14335K->14335K(14336K),0.0278675secs]19903K->19903K(19904K), [Perm:376K->376K(12288K)],0.0279215secs][Times:user=0.03sys=0.00,real=0.03secs] [FullGC[Tenured:14335K->14335K(14336K),0.0348408secs]19903K->19889K(19904K), [Perm:376K->371K(12288K)],0.0348945secs][Times:user=0.05sys=0.00,real=0.05secs] [FullGC[Tenured:14335K->14335K(14336K),0.0299813secs]19903K->19903K(19904K), [Perm:371K->371K(12288K)],0.0300349secs][Times:user=0.01sys=0.00,real=0.02secs] [FullGC[Tenured:14335K->14335K(14336K),0.0298178secs]19903K->19903K(19904K), [Perm:371K->371K(12288K)],0.0298688secs][Times:user=0.03sys=0.00,real=0.03secs] Exceptioninthread"main"java.lang.OutOfMemoryError:Javaheapspace[FullGC[Tenured: 14335K->14335K(14336K),0.0294953secs]19903K->19903K(19904K), [Perm:371K->371K(12288K)],0.0295474secs][Times:user=0.03sys=0.00,real=0.03secs] [FullGC[Tenured :14335K->14335K(14336K),0.0287742secs]19903K->19903K(19904K), [Perm:371K->371K(12288K)],0.0288239secs][Times:user=0.03sys=0.00,real=0.03secs] [FullGC[TenuredatGCTimeTest.main(GCTimeTest.java:16) :14335K->14335K(14336K),0.0287102secs]19903K->19903K(19904K), [Perm:371K->371K(12288K)],0.0287627secs][Times:user=0.03sys=0.00,real=0.03secs] Heap defnewgenerationtotal5568K,used5567K[0x35c10000,0x36210000,0x36210000) edenspace4992K,100%used[0x35c10000,0x360f0000,0x360f0000) fromspace576K,99%used[0x36180000,0x3620ffe8,0x36210000) tospace576K,0%used[0x360f0000,0x360f0000,0x36180000) tenuredgenerationtotal14336K,used14335K[0x36210000,0x37010000,0x37010000) thespace14336K,99%used[0x36210000,0x3700ffd8,0x37010000,0x37010000) compactingpermgentotal12288K,used371K[0x37010000,0x37c10000,0x3b010000) thespace12288K,3%used[0x37010000,0x3706ce28,0x3706d000,0x37c10000) rospace10240K,51%used[0x3b010000,0x3b543000,0x3b543000,0x3ba10000) rwspace12288K,55%used[0x3ba10000,0x3c0ae4f8,0x3c0ae600,0x3c610000)
通过清单2和清单3对比,可以发现通过设置一个较大的年轻代预留新对象,设置合理的Survivor区并且提供Survivor区的使用率,可以将年轻对象保存在年轻代。
一般来说,Survivor区的空间不够,或者占用量达到50%时,就会使对象进入年老代(不管它的年龄有多大)。清单4创建了3个对象,分别分配一定的内存空间。
清单4.不同大小内存分配
publicclassPutInEden2{ publicstaticvoidmain(String[]args){ byte[]b1,b2,b3; b1=newbyte[1024*512];//分配0.5MB堆空间 b2=newbyte[1024*1024*4];//分配4MB堆空间 b3=newbyte[1024*1024*4]; b3=null;//使b3可以被回收 b3=newbyte[1024*1024*4];//分配4MB堆空间 } }
使用参数-XX:+PrintGCDetails-Xmx1000M-Xms500M-Xmn100M-XX:SurvivorRatio=8运行清单4所示代码,输出如清单5所示。
清单5.清单4运行输出
Heap defnewgenerationtotal92160K,used11878K[0x0f010000,0x15410000,0x15410000) edenspace81920K,2%used[0x0f010000,0x0f1a9a20,0x14010000) fromspace10240K,99%used[0x14a10000,0x1540fff8,0x15410000) tospace10240K,0%used[0x14010000,0x14010000,0x14a10000) tenuredgenerationtotal409600K,used86434K[0x15410000,0x2e410000,0x4d810000) thespace409600K,21%used[0x15410000,0x1a878b18,0x1a878c00,0x2e410000) compactingpermgentotal12288K,used2062K[0x4d810000,0x4e410000,0x51810000) thespace12288K,16%used[0x4d810000,0x4da13b18,0x4da13c00,0x4e410000) Nosharedspacesconfigured.
清单5输出的日志显示,年轻代分配了8M,年老代也分配了8M。我们可以尝试加上-XX:TargetSurvivorRatio=90参数,这样可以提高from区的利用率,使from区使用到90%时,再将对象送入年老代,运行清单4代码,输出如清单6所示。
清单6.修改运行参数后清单4输出
Heap defnewgenerationtotal9216K,used9215K[0x35c10000,0x36610000,0x36610000) edenspace8192K,100%used[0x35c10000,0x36410000,0x36410000) fromspace1024K,99%used[0x36510000,0x3660fc50,0x36610000) tospace1024K,0%used[0x36410000,0x36410000,0x36510000) tenuredgenerationtotal10240K,used10239K[0x36610000,0x37010000,0x37010000) thespace10240K,99%used[0x36610000,0x3700ff70,0x37010000,0x37010000) compactingpermgentotal12288K,used371K[0x37010000,0x37c10000,0x3b010000) thespace12288K,3%used[0x37010000,0x3706cd90,0x3706ce00,0x37c10000) rospace10240K,51%used[0x3b010000,0x3b543000,0x3b543000,0x3ba10000) rwspace12288K,55%used[0x3ba10000,0x3c0ae4f8,0x3c0ae600,0x3c610000)
如果将SurvivorRatio设置为2,将b1对象预存在年轻代。输出如清单7所示。
清单7.再次修改运行参数后清单4输出
Heap defnewgenerationtotal7680K,used7679K[0x35c10000,0x36610000,0x36610000) edenspace5120K,100%used[0x35c10000,0x36110000,0x36110000) fromspace2560K,99%used[0x36110000,0x3638fff0,0x36390000) tospace2560K,0%used[0x36390000,0x36390000,0x36610000) tenuredgenerationtotal10240K,used10239K[0x36610000,0x37010000,0x37010000) thespace10240K,99%used[0x36610000,0x3700fff0,0x37010000,0x37010000) compactingpermgentotal12288K,used371K[0x37010000,0x37c10000,0x3b010000) thespace12288K,3%used[0x37010000,0x3706ce28,0x3706d000,0x37c10000) rospace10240K,51%used[0x3b010000,0x3b543000,0x3b543000,0x3ba10000) rwspace12288K,55%used[0x3ba10000,0x3c0ae4f8,0x3c0ae600,0x3c610000)
如何让大对象进入年老代
我们在大部分情况下都会选择将对象分配在年轻代。但是,对于占用内存较多的大对象而言,它的选择可能就不是这样的。因为大对象出现在年轻代很可能扰乱年轻代GC,并破坏年轻代原有的对象结构。因为尝试在年轻代分配大对象,很可能导致空间不足,为了有足够的空间容纳大对象,JVM不得不将年轻代中的年轻对象挪到年老代。
因为大对象占用空间多,所以可能需要移动大量小的年轻对象进入年老代,这对GC相当不利。基于以上原因,可以将大对象直接分配到年老代,保持年轻代对象结构的完整性,这样可以提高GC的效率。如果一个大对象同时又是一个短命的对象,假设这种情况出现很频繁,那对于GC来说会是一场灾难。
原本应该用于存放永久对象的年老代,被短命的对象塞满,这也意味着对堆空间进行了洗牌,扰乱了分代内存回收的基本思路。因此,在软件开发过程中,应该尽可能避免使用短命的大对象。
可以使用参数-XX:PetenureSizeThreshold设置大对象直接进入年老代的阈值。当对象的大小超过这个值时,将直接在年老代分配。参数-XX:PetenureSizeThreshold只对串行收集器和年轻代并行收集器有效,并行回收收集器不识别这个参数。
清单8.创建一个大对象
publicclassBigObj2Old{ publicstaticvoidmain(String[]args){ byte[]b; b=newbyte[1024*1024];//分配一个1MB的对象 } }
使用JVM参数-XX:+PrintGCDetails–Xmx20M–Xms20MB运行,可以得到清单9所示日志输出。
清单9.清单8运行输出
Heap defnewgenerationtotal6144K,used1378K[0x35c10000,0x362b0000,0x362b0000) edenspace5504K,25%used[0x35c10000,0x35d689e8,0x36170000) fromspace640K,0%used[0x36170000,0x36170000,0x36210000) tospace640K,0%used[0x36210000,0x36210000,0x362b0000) tenuredgenerationtotal13696K,used0K[0x362b0000,0x37010000,0x37010000) thespace13696K,0%used[0x362b0000,0x362b0000,0x362b0200,0x37010000) compactingpermgentotal12288K,used374K[0x37010000,0x37c10000,0x3b010000) thespace12288K,3%used[0x37010000,0x3706dac8,0x3706dc00,0x37c10000) rospace10240K,51%used[0x3b010000,0x3b543000,0x3b543000,0x3ba10000) rwspace12288K,55%used[0x3ba10000,0x3c0ae4f8,0x3c0ae600,0x3c610000)
可以看到该对象被分配在了年轻代,占用了25%的空间。如果需要将1MB以上的对象直接在年老代分配,设置-XX:PetenureSizeThreshold=1000000,程序运行后输出如清单10所示。
清单10.修改运行参数后清单8输出
Heap defnewgenerationtotal6144K,used354K[0x35c10000,0x362b0000,0x362b0000) edenspace5504K,6%used[0x35c10000,0x35c689d8,0x36170000) fromspace640K,0%used[0x36170000,0x36170000,0x36210000) tospace640K,0%used[0x36210000,0x36210000,0x362b0000) tenuredgenerationtotal13696K,used1024K[0x362b0000,0x37010000,0x37010000) thespace13696K,7%used[0x362b0000,0x363b0010,0x363b0200,0x37010000) compactingpermgentotal12288K,used374K[0x37010000,0x37c10000,0x3b010000) thespace12288K,3%used[0x37010000,0x3706dac8,0x3706dc00,0x37c10000) rospace10240K,51%used[0x3b010000,0x3b543000,0x3b543000,0x3ba10000) rwspace12288K,55%used[0x3ba10000,0x3c0ae4f8,0x3c0ae600,0x3c610000)
清单10里面可以看到当满1MB时进入到了年老代。
如何设置对象进入年老代的年龄
堆中的每一个对象都有自己的年龄。一般情况下,年轻对象存放在年轻代,年老对象存放在年老代。为了做到这点,虚拟机为每个对象都维护一个年龄。如果对象在Eden区,经过一次GC后依然存活,则被移动到Survivor区中,对象年龄加1。以后,如果对象每经过一次GC依然存活,则年龄再加1。
当对象年龄达到阈值时,就移入年老代,成为老年对象。这个阈值的最大值可以通过参数-XX:MaxTenuringThreshold来设置,默认值是15。虽然-XX:MaxTenuringThreshold的值可能是15或者更大,但这不意味着新对象非要达到这个年龄才能进入年老代。
事实上,对象实际进入年老代的年龄是虚拟机在运行时根据内存使用情况动态计算的,这个参数指定的是阈值年龄的最大值。即,实际晋升年老代年龄等于动态计算所得的年龄与-XX:MaxTenuringThreshold中较小的那个。清单11所示代码为3个对象申请了若干内存。
清单11.申请内存
publicclassMaxTenuringThreshold{ publicstaticvoidmain(Stringargs[]){ byte[]b1,b2,b3; b1=newbyte[1024*512]; b2=newbyte[1024*1024*2]; b3=newbyte[1024*1024*4]; b3=null; b3=newbyte[1024*1024*4]; } }
参数设置为:-XX:+PrintGCDetails-Xmx20M-Xms20M-Xmn10M-XX:SurvivorRatio=2
运行清单11所示代码,输出如清单12所示。
清单12.清单11运行输出
[GC[DefNew:2986K->690K(7680K),0.0246816secs]2986K->2738K(17920K), 0.0247226secs][Times:user=0.00sys=0.02,real=0.03secs] [GC[DefNew:4786K->690K(7680K),0.0016073secs]6834K->2738K(17920K), 0.0016436secs][Times:user=0.00sys=0.00,real=0.00secs] Heap defnewgenerationtotal7680K,used4888K[0x35c10000,0x36610000,0x36610000) edenspace5120K,82%used[0x35c10000,0x36029a18,0x36110000) fromspace2560K,26%used[0x36110000,0x361bc950,0x36390000) tospace2560K,0%used[0x36390000,0x36390000,0x36610000) tenuredgenerationtotal10240K,used2048K[0x36610000,0x37010000,0x37010000) thespace10240K,20%used[0x36610000,0x36810010,0x36810200,0x37010000) compactingpermgentotal12288K,used374K[0x37010000,0x37c10000,0x3b010000) thespace12288K,3%used[0x37010000,0x3706db50,0x3706dc00,0x37c10000) rospace10240K,51%used[0x3b010000,0x3b543000,0x3b543000,0x3ba10000) rwspace12288K,55%used[0x3ba10000,0x3c0ae4f8,0x3c0ae600,0x3c610000)
更改参数为-XX:+PrintGCDetails-Xmx20M-Xms20M-Xmn10M-XX:SurvivorRatio=2-XX:MaxTenuringThreshold=1,运行清单11所示代码,输出如清单13所示。
清单13.修改运行参数后清单11输出
[GC[DefNew:2986K->690K(7680K),0.0047778secs]2986K->2738K(17920K), 0.0048161secs][Times:user=0.00sys=0.00,real=0.00secs] [GC[DefNew:4888K->0K(7680K),0.0016271secs]6936K->2738K(17920K), 0.0016630secs][Times:user=0.00sys=0.00,real=0.00secs] Heap defnewgenerationtotal7680K,used4198K[0x35c10000,0x36610000,0x36610000) edenspace5120K,82%used[0x35c10000,0x36029a18,0x36110000) fromspace2560K,0%used[0x36110000,0x36110088,0x36390000) tospace2560K,0%used[0x36390000,0x36390000,0x36610000) tenuredgenerationtotal10240K,used2738K[0x36610000,0x37010000,0x37010000) thespace10240K,26%used[0x36610000,0x368bc890,0x368bca00,0x37010000) compactingpermgentotal12288K,used374K[0x37010000,0x37c10000,0x3b010000) thespace12288K,3%used[0x37010000,0x3706db50,0x3706dc00,0x37c10000) rospace10240K,51%used[0x3b010000,0x3b543000,0x3b543000,0x3ba10000) rwspace12288K,55%used[0x3ba10000,0x3c0ae4f8,0x3c0ae600,0x3c610000)
清单13所示,第一次运行时b1对象在程序结束后依然保存在年轻代。第二次运行前,我们减小了对象晋升年老代的年龄,设置为1。即,所有经过一次GC的对象都可以直接进入年老代。
程序运行后,可以发现b1对象已经被分配到年老代。如果希望对象尽可能长时间地停留在年轻代,可以设置一个较大的阈值。
稳定的Java堆VS动荡的Java堆
一般来说,稳定的堆大小对垃圾回收是有利的。获得一个稳定的堆大小的方法是使-Xms和-Xmx的大小一致,即最大堆和最小堆(初始堆)一样。如果这样设置,系统在运行时堆大小理论上是恒定的,稳定的堆空间可以减少GC的次数。因此,很多服务端应用都会将最大堆和最小堆设置为相同的数值。
但是,一个不稳定的堆并非毫无用处。稳定的堆大小虽然可以减少GC次数,但同时也增加了每次GC的时间。让堆大小在一个区间中震荡,在系统不需要使用大内存时,压缩堆空间,使GC应对一个较小的堆,可以加快单次GC的速度。基于这样的考虑,JVM还提供了两个参数用于压缩和扩展堆空间。
-XX:MinHeapFreeRatio参数用来设置堆空间最小空闲比例,默认值是40。当堆空间的空闲内存小于这个数值时,JVM便会扩展堆空间。
-XX:MaxHeapFreeRatio参数用来设置堆空间最大空闲比例,默认值是70。当堆空间的空闲内存大于这个数值时,便会压缩堆空间,得到一个较小的堆。
当-Xmx和-Xms相等时,-XX:MinHeapFreeRatio和-XX:MaxHeapFreeRatio两个参数无效。
清单14.堆大小设置
importjava.util.Vector; publicclassHeapSize{ publicstaticvoidmain(Stringargs[])throwsInterruptedException{ Vectorv=newVector(); while(true){ byte[]b=newbyte[1024*1024]; v.add(b); if(v.size()==10){ v=newVector(); } Thread.sleep(1); } } }
清单14所示代码是测试-XX:MinHeapFreeRatio和-XX:MaxHeapFreeRatio的作用,设置运行参数为-XX:+PrintGCDetails-Xms10M-Xmx40M-XX:MinHeapFreeRatio=40-XX:MaxHeapFreeRatio=50时,输出如清单15所示。
清单15.修改运行参数后清单14输出
[GC[DefNew:2418K->178K(3072K),0.0034827secs]2418K->2226K(9920K), 0.0035249secs][Times:user=0.00sys=0.00,real=0.03secs] [GC[DefNew:2312K->0K(3072K),0.0028263secs]4360K->4274K(9920K), 0.0029905secs][Times:user=0.00sys=0.00,real=0.03secs] [GC[DefNew:2068K->0K(3072K),0.0024363secs]6342K->6322K(9920K), 0.0024836secs][Times:user=0.00sys=0.00,real=0.03secs] [GC[DefNew:2061K->0K(3072K),0.0017376secs][Tenured:8370K->8370K(8904K), 0.1392692secs]8384K->8370K(11976K),[Perm:374K->374K(12288K)], 0.1411363secs][Times:user=0.00sys=0.02,real=0.16secs] [GC[DefNew:5138K->0K(6336K),0.0038237secs]13508K->13490K(20288K), 0.0038632secs][Times:user=0.00sys=0.00,real=0.03secs]
改用参数:-XX:+PrintGCDetails-Xms40M-Xmx40M-XX:MinHeapFreeRatio=40-XX:MaxHeapFreeRatio=50,运行输出如清单16所示。
清单16.再次修改运行参数后清单14输出
[GC[DefNew:10678K->178K(12288K),0.0019448secs]10678K->178K(39616K), 0.0019851secs][Times:user=0.00sys=0.00,real=0.03secs] [GC[DefNew:10751K->178K(12288K),0.0010295secs]10751K->178K(39616K), 0.0010697secs][Times:user=0.00sys=0.00,real=0.02secs] [GC[DefNew:10493K->178K(12288K),0.0008301secs]10493K->178K(39616K), 0.0008672secs][Times:user=0.00sys=0.00,real=0.02secs] [GC[DefNew:10467K->178K(12288K),0.0008522secs]10467K->178K(39616K), 0.0008905secs][Times:user=0.00sys=0.00,real=0.02secs] [GC[DefNew:10450K->178K(12288K),0.0008964secs]10450K->178K(39616K), 0.0009339secs][Times:user=0.00sys=0.00,real=0.01secs] [GC[DefNew:10439K->178K(12288K),0.0009876secs]10439K->178K(39616K), 0.0010279secs][Times:user=0.00sys=0.00,real=0.02secs]
从清单16可以看出,此时堆空间的垃圾回收稳定在一个固定的范围。在一个稳定的堆中,堆空间大小始终不变,每次GC时,都要应对一个40MB的空间。因此,虽然GC次数减小了,但是单次GC速度不如一个震荡的堆。
增大吞吐量提升系统性能
吞吐量优先的方案将会尽可能减少系统执行垃圾回收的总时间,故可以考虑关注系统吞吐量的并行回收收集器。在拥有高性能的计算机上,进行吞吐量优先优化,可以使用参数:
java–Xmx3800m–Xms3800m–Xmn2G–Xss128k–XX:+UseParallelGC –XX:ParallelGC-Threads=20–XX:+UseParallelOldGC
–Xmx380m–Xms3800m:设置Java堆的最大值和初始值。一般情况下,为了避免堆内存的频繁震荡,导致系统性能下降,我们的做法是设置最大堆等于最小堆。假设这里把最小堆减少为最大堆的一半,即1900m,那么JVM会尽可能在1900MB堆空间中运行,如果这样,发生GC的可能性就会比较高;
-Xss128k:减少线程栈的大小,这样可以使剩余的系统内存支持更多的线程;
-Xmn2g:设置年轻代区域大小为2GB;
–XX:+UseParallelGC:年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器,可以尽可能地减少GC时间。
–XX:ParallelGC-Threads:设置用于垃圾回收的线程数,通常情况下,可以设置和CPU数量相等。但在CPU数量比较多的情况下,设置相对较小的数值也是合理的;
–XX:+UseParallelOldGC:设置年老代使用并行回收收集器。
尝试使用大的内存分页
CPU是通过寻址来访问内存的。32位CPU的寻址宽度是0~0xFFFFFFFF,计算后得到的大小是4G,也就是说可支持的物理内存最大是4G。但在实践过程中,碰到了这样的问题,程序需要使用4G内存,而可用物理内存小于4G,导致程序不得不降低内存占用。
为了解决此类问题,现代CPU引入了MMU(MemoryManagementUnit内存管理单元)。MMU的核心思想是利用虚拟地址替代物理地址,即CPU寻址时使用虚址,由MMU负责将虚址映射为物理地址。
MMU的引入,解决了对物理内存的限制,对程序来说,就像自己在使用4G内存一样。内存分页(Paging)是在使用MMU的基础上,提出的一种内存管理机制。它将虚拟地址和物理地址按固定大小(4K)分割成页(page)和页帧(pageframe),并保证页与页帧的大小相同。
这种机制,从数据结构上,保证了访问内存的高效,并使OS能支持非连续性的内存分配。在程序内存不够用时,还可以将不常用的物理内存页转移到其他存储设备上,比如磁盘,这就是大家耳熟能详的虚拟内存。
在Solaris系统中,JVM可以支持LargePageSize的使用。使用大的内存分页可以增强CPU的内存寻址能力,从而提升系统的性能。
java–Xmx2506m–Xms2506m–Xmn1536m–Xss128k–XX:++UseParallelGC –XX:ParallelGCThreads=20–XX:+UseParallelOldGC–XX:+LargePageSizeInBytes=256m
–XX:+LargePageSizeInBytes:设置大页的大小。
过大的内存分页会导致JVM在计算Heap内部分区(perm,new,old)内存占用比例时,会出现超出正常值的划分,最坏情况下某个区会多占用一个页的大小。
使用非占有的垃圾回收器
为降低应用软件的垃圾回收时的停顿,首先考虑的是使用关注系统停顿的CMS回收器,其次,为了减少FullGC次数,应尽可能将对象预留在年轻代,因为年轻代MinorGC的成本远远小于年老代的FullGC。
java–Xmx3550m–Xms3550m–Xmn2g–Xss128k–XX:ParallelGCThreads=20 –XX:+UseConcMarkSweepGC–XX:+UseParNewGC–XX:+SurvivorRatio=8–XX:TargetSurvivorRatio=90 –XX:MaxTenuringThreshold=31
–XX:ParallelGCThreads=20:设置20个线程进行垃圾回收;
–XX:+UseParNewGC:年轻代使用并行回收器;
–XX:+UseConcMarkSweepGC:年老代使用CMS收集器降低停顿;
–XX:+SurvivorRatio:设置Eden区和Survivor区的比例为8:1。稍大的Survivor空间可以提高在年轻代回收生命周期较短的对象的可能性,如果Survivor不够大,一些短命的对象可能直接进入年老代,这对系统来说是不利的。
–XX:TargetSurvivorRatio=90:设置Survivor区的可使用率。这里设置为90%,则允许90%的Survivor空间被使用。默认值是50%。故该设置提高了Survivor区的使用率。当存放的对象超过这个百分比,则对象会向年老代压缩。因此,这个选项更有助于将对象留在年轻代。
–XX:MaxTenuringThreshold:设置年轻对象晋升到年老代的年龄。默认值是15次,即对象经过15次MinorGC依然存活,则进入年老代。这里设置为31,目的是让对象尽可能地保存在年轻代区域。
结束语
通过本文的学习,读者了解了如何将新对象预留在年轻代、如何让大对象进入年老代、如何设置对象进入年老代的年龄、稳定的Java堆VS动荡的Java堆、增大吞吐量提升系统性能、尝试使用大的内存分页、使用非占有的垃圾回收器等主题,通过实例及对应输出解释的形式让读者对于JVM优化有一个初步认识。
如其他文章相同的观点,没有哪一条优化是固定不变的,读者需要自己判断、实践后才能找到正确的道路。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。