JVM性能优化

273 阅读8分钟

这是我参与更文挑战的第23天,活动详情查看: 更文挑战

JVM性能优化到底在优化什么

基于JVM运行的系统最怕什么

我们都知道JVM运行的时候,最核心的内存区域就是堆内存,这里会存放我们创建出来的各种对象。堆内存又会被分为新生代和老年代。对象一般来说在创建的是时候都会首先被分配到新生代,随着系统运行,新生代放不下后,就会触发新生代的GC,新生代的GC一定会停止系统程序的运行,进入“stop the world”状态。对象在新生代触发各种规则之后就会进入老年代,随着系统运行,老年代放不下后,就会触发老年代的GC,老年代的GC我们之前了解过CMS和G1垃圾收集器都会有两个阶段使程序进入“stop the world”状态,而与工作线程并行的阶段也会占用CPU资源,影响服务性能。到这里我们能够明白其实JVM运行的系统最害怕的问题就是:垃圾回收时的系统卡顿“stop the world”。

年轻代GC对系统的影响

通常年轻代的GC机会是没什么好调优的,因为它采用的复制算法效率极高,而且新生代里的存活对象一般很少,所以只需要迅速标记出这少量的存活对象,一定到一个空的Survivor,然后全部回收掉Eden和另一个Survivor中的对象即可,速度是很快的。 一般来说,对新生代的GC调优,核心在于新生代内存大小的分配,内存合理的话,通常可以使系统在低峰期几个小时才有一次新生代GC,高峰期几分钟一次新生代GC。另外,新生代的GC通常都在几毫秒或几十毫秒之间,这么短时间的卡顿对系统几乎是没有影响的。

什么时候新生代GC对系统影响会很大

当你的系统内存很大的时候,比如32核64G的机器,新生代分配20~30G的内存,这个时候因为内存很大,存在的对象更多,即使是新生代在追踪存活对象和垃圾回收的时候也会消耗过多的时间。那么如何解决呢?答案就是使用G1垃圾回收器,G1的特点就在于可以设置预期停顿时间,比如我们设置20ms,那么G1就会根据预期停顿时间,选择部分Region进行回收,最大程度的回收垃圾,而且降低了系统停顿时间。 G1垃圾回收器,天生就适合大内存机器的JVM运行,可以完美解决大内存垃圾回收时间过长的问题。

要命的频繁老年代GC

综上所述,新生代GC一般问题不会太大,真正问题最大的就在于频繁的老年代GC。这里我们首先回顾一下对象进入老年代的几个条件:1、对象年龄太大;2、动态年龄判定规则;3、新生代垃圾回收过后,存活对象太多,无法放入Survivor,直接进入老年代。第2和第3个条件都很关键,通常如果新生代中Survivor区域内存过小,就会导致这两个条件频繁发生,导致对象频繁进入老年代而频繁触发老年代GC。老年代的GC是很耗时的,无论是CMS还是G1,都要经历复杂的过程。通常老年代GC会比新生代GC慢10倍以上,所以一定要做好内存区域的合理分配。

JVM优化到底在优化什么

通过上面的描述应该可以明白了,系统正真最大的问题,就是因为内存分配、参数设置不合理,导致你的对象频繁进入老年代,然后频繁触发老年代GC,导致系统频繁卡顿。JVM优化就是合理分配内存、合理配置参数,让对象尽可能都在新生代创建和销毁,尽最大可能的不要触发老年代GC,降低垃圾回收导致的系统停顿。

总结JVM GC相关的参数

经过之前的介绍,我们了解了JVM的运行原理,JVM GC的工作原理,JVM垃圾回收器的工作原理,JVM优化具体在做什么。知道了JVM优化,就是对内存的合理分配和参数的合理配置,这里我们就来汇总一下JVM GC相关的各种参数。

  1. -Xms:Java堆内存初始大小;

  2. -Xmx:Java堆内存最大大小;

a. Xms和Xmx通常都会设置成相同堆值,这样避免堆内存动态伸缩时消耗资源。

  1. -Xmn:Java堆内存中新生代的大小,扣除新生代剩余的就是老年代的大小;
  2. -XX:NewRatio:设置新生代和老年代的大小比例,默认值2,新生代:老年代=1:2;
  3. -XX:SurvivorRatio:设置新生代Eden区和Survivor区的占比; a. 假设SurvivorRatio=8,Eden:S1:S2=8:1:1。
  4. -XX:MaxTenuringThreshold:新生代对象成长为多少岁的时候会进入老年代,默认是15;
  5. -XX:PretenureSizeThreshold:超过该设置的大对象会直接进入老年代;
  6. -XX:PermSize:永久代初始大小;
  7. -XX:MaxPermSize:永久代最大大小;

a. PermSize和MaxPermSize通常也会设置成相同的值,避免内存动态伸缩时消耗资源。

  1. -XX:MetaspaceSize:JDK1.8以后的永久代初始大小;
  2. -XX:MaxMetaspaceSize:JDK1.8以后的永久代最大大小;

a. MetaspaceSize和MaxMetaspaceSize通常也要设置相同的值,避免内存动态伸缩。

  1. -Xss:每个线程的栈内存大小,通常是1MB,这个参数一般不需要自行设置;
  2. -XX:+UseParNewGC:选择新生代使用ParNew垃圾回收器;
  3. -XX:ParallelGCThreads:设置ParNew进行垃圾回收时工作的线程数;
  4. -XX:+UseConcMarkSweepGC:选择老年代使用CMS垃圾收集器;

a. CMS默认启动的垃圾回收线程数是:(CPU核数+3)/4。

  1. -XX:CMSInitiatingOccupancyFaction:该参数是设置老年代占用达到多少百分比的时候就会触发Old GC,JDK1.6默认是92%;

a. 因为我们在CMS的最后阶段“并发清理”时,是和工作线程并发执行的,这时会存在有对象进来,而此时很可能我们的垃圾对象还没有被清理,所以我们最好预留一定的空间来做缓冲。

  1. -XX:+UseCMSCompactAtFullCollection:默认是打开的,意思是在CMS垃圾回收会,系统要再次进入“stop the world”然后进行碎片整理;

  2. -XX:CMSFullGCsBeforeCompaction:意思是执行多少次Old GC之后才执行一次内存碎片整理,默认是0,就是每次Old GC之后都会进行内存整理; a. 这里设置次数,是看似减少“stop the world”的次数,但是Old GC之后如果不整理碎片就会导致可用内存变少,下次触发Old GC的时间间隔就会变短,所以最好在每次Old GC之后都进行碎片整理。

  3. -XX:HandlePromotionFailure:这个参数是用来开启对象从新生代进入老年代是否要进行2次判断的,不过这个参数在JDK1.6之后就被废弃了,所以我们也不用过多的考虑; a. 新生代在发生Minor GC的时候,会判断1、老年代可用空间>新生代对象总和;2、老年代可用空间>历次Minor GC升入老年代对象的平均大小。两者满足一个就可以进行Minor GC,否则就要提前触发Full GC。该参数在JDK1.6之前需要设置后才会进行第二部的判断。

  4. -XX:+UseG1GC:选择老年代使用G1垃圾收集器;

  5. -XX:G1HeapRegionSize:手动指定G1中每个Region的大小;

  6. -XX:G1NewSizePercent:设置新生代对堆内存的初始占比,默认5%;

  7. -XX:G1MaxNewSizePercent:设置新生代对堆内存的最大占比,最多不会超过60%; a. -XX:G1HeapRegionSize、-XX:G1NewSizePercent、-XX:G1MaxNewSizePercent 一般都保持默认即可。

  8. -XX:MaxG1GCPauseMills:设置G1垃圾回收的预期停顿时间,默认是200ms;

  9. -XX:InitiatingHeapOccupancyPercent:默认值是45%,它的意思是老年代占据堆内存的多大比例时,会尝试触发一次新生代+老年代的混合回收;

  10. -XX:G1MixedGCCountTarget:默认是8,它的意思是在G1的最后阶段,执行几次混合回收;

  11. -XX:G1HeapWastePercent:默认是5%,它的意思是在混合回收的时候,对Region的回收都是基于复制算法进行的,这样回收过程中就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,就会立即停止混合回收;

  12. -XX:G1MixedGCLiveThresholdPercent:默认是85%,它的意思是确定要回收Region时,必须是存活对象低于85%的Region才可以进行回收; a. 否则要是一个Region的存活对象多余85%,那么回收它成本会比较高。

  13. -XX:+PrintGCDetils:打印详细的GC日志;

  14. -XX:+PrintGCTimeStamps:打印出来每次GC发生的时间;

  15. -Xloggc:gc.log:设置将GC写入的文件。