由于 Full GC 的成本要远远高于 Minor GC ,因此尽可能将对象分配在新生代,在JVM 调优中,可以为应用程序分配一个合理的新生代空间,以最大限度避免新对象直接进去老年代。
注意:由于新生代垃圾回收的速度高于老年代回收,因此,将年轻对象预留在新生代有利于提高整体的 GC 效率
大对象占用空间多,直接放入新生代中会扰乱新生代GC,新生代空间不足将会把大量的较小的年轻代对象移入到老年代中,这对GC来说是相当不利的。如果有短命大对象,对GC来说将会是一场灾难,原本存放于老年代的永久对象,被短命大对象塞满,扰乱了分代内存回收的基本思路,因此,在开发过程中,尽可能避免使用短命的大对象。使用参数 -XX:PretenureSizeThreshold 设置大对象直接进入老年代的阀值,当对象超过这个阀值时,将直接在老年代中分配。其中, -XX:PretenureSizeThreshold 只对串行收集器和新生代并行收集器有效,并行回收收集器不识别这个参数。
注意:短命的大对象对垃圾回收是一场灾难,目前木有一种特别好的回收方法处理这个问题,因此尽可能避免使用短命的大对象。
在堆中每个对象都有自己的年龄,如果对象在 eden 区,经过一次 GC 后还存活,则被移动到 survivor 区中,对象年龄加 1,以后每经过一次 GC 依然存活的,对象年龄就加 1。当对象年龄达到阀值时,就移动到老年代,这个阀值用以下参数设置:
-XX:MaxTenuringThreshold:默认值是15,这个参数是指定进入老年代的最大年龄值,对象实际进入老年代的年龄是 JVM 在运行时根据内存使用情况动态计算的。
如果希望对象尽可能长地留在新生代中,可以设置一个较大的阀值。
稳定的堆大小对垃圾回收是有利的,获得一个稳定堆大小的方法就是设置 -Xmx 和 -Xms 一样的值。不稳定的堆也不是木有用处,让堆大小在一个区间内震荡,在系统不需要使用大内存时压缩堆空间,使 GC 应对一个较小的堆,可以加快单次 GC 的速度。基于这种思想,JVM 提供了两个参数用于压缩和扩展堆空间,参数如下:
-XX:MinHeapFreeRatio:设置堆空间最小空闲比例,默认是 40 ,当堆空间的空闲比例小于这个值时,JVM 便会扩展堆空间
-XX:MaxHeapFreeRatio:设置堆空间的最大空闲比例,默认是 70,当堆空间的空闲比例大于这个值时,JVM 便会压缩堆空间,得到一个较小的堆
注意:当 -Xms 和 -Xmx 相等时,-XX:MinHeapFreeRatio 和 -XX:MaxHeapFreeRatio 这两个参数无效
机器配置是 4G 内存 和 32 核 CPU,配置参数如下:
-Xms3800m -Xmx3800m(堆的初始值和最大值一样)
-Xss128k(线程栈大小,减少它使剩余的系统内存支持更多的线程)
-XX:+UseParallelGC(新生代使用并行回收收集器)
-XX:ParallelGCThreads=20(垃圾回收的线程数)
-XX:+UseParallelOldGC (老年代使用并行回收收集器)
使用大的内存分页可以增强 CPU 的内存寻址能力,从而提高系统的性能,参数设置如下:
-XX:LargePageSizeInBytes:设置大页的大小
为了降低应用软件在垃圾回收时的停顿,首先考虑的使用关注系统停顿的 CMS 回收器,为了减少 Full GC 的次数,应尽可能将对象预留在新生代,新生代 Minor GC 的成本远远小于老年代的 Full GC
-Xms3800m -Xmx3800m(堆的初始值和最大值一样)
-Xss128k(线程栈大小,减少它使剩余的系统内存支持更多的线程)
-XX:ParallelGCThreads=20(垃圾回收的线程数)
-XX:+UseConcMarkSweepGC(老年代使用 CMS 收集器)
-XX:SurvivorRatio=8(设置 eden : survivor = 8 : 1)
-XX:TargetSurvivorRatio(设置 survivor 的使用率为 90%,默认是50%,提高了survivor 区的使用率,当存放的对象超过这个数值,则对象会向老年代压缩)
-XX:MaxTenuringThreshold=31(设置年轻对象晋升到老年代的最大年龄是31,默认是15,设为31是尽可能地将对象留在新生代)
JVM 的 JIT(Just-In-Time)编译器,可以在运行时将字节码编译成本地代码,从而提高函数的执行效率。参数设置如下
-XX:CompileThreshold:JIT 编译的阀值,当函数的调用超过这个值时,JIT 就将字节码编译成本地机器码。在 client 模式下,取值是 1500,在 server 模式下,取值是 10000
在性能问题排查中,分析堆快照(Dump)是必不可少的一环。获取程序的堆快照文件有多种方法,下面介绍一种常用的方法,即使用 -XX:+HeapDumpOnOutOfMemoryError 参数在程序发生 OOM 时,导出应用程序的当前堆快照。这是一种非常有效的方法,因为当程序发生 OOM 退出系统时,一些瞬时信息都随着程序的终止而消失,而重现 OOM 问题往往比较困难或者耗时,因此当发生 OOM 时,将堆信息保存到文件中是至关重要的,通过下面的参数设置:
-XX:+HeapDumpOnOutOfMemoryError(开启堆快照)
-XX:HeapDumpPath=C:/m.hprof(保存文件到哪个目录)
导出的 Dump 文件可以通过 Visual VM 等多种工具查看分析,进而定位问题,如下图所示:
系统发生 OOM 错误时,JVM 在错误发生时运行一段第三方脚本,重置系统,设置参数如下:
-XX:OnOutOfMemoryError=C:\reset.bat
获取 GC 信息是 java 应用程序调优最重要的一环,下面介绍一些常用的设置参数:
-XX:+PrintGC(-verbose:gc):输出打印简要的 GC 信息,包括 GC 前的堆栈情况和 GC 后的堆栈大小和堆栈的总大小
-XX:+PrintGCDetails:输出打印详细的 GC 信息,不仅包括基本信息,还给出了新生代、老年代和永久区各自的 GC 信息
-XX:+PrintGCTimeStamps:额外输出 GC 发生的时间,可以推断出 GC 的频率和间隔
-XX:+PrintTenuringDistribution -XX:MaxTenuringThreshold=18:查看新生代晋升老年代的实际阀值(-XX:+PrintTenuringDistribution),设置的最大年龄为18( -XX:MaxTenuringThreshold=18)
-XX:+PrintHeapAtGC:每次 GC 时都会打印堆的详细使用情况,输出量很巨大。它分为两个部分:GC 前的堆信息和 GC 后的堆信息,这里包含了新生代、老年代和永久区的使用大小和使用率,还包括了新生代中 eden 区和 survivor 区的使用情况
-XX:+PrintGCApplicationStoppedTime:应用程序在 GC 发生时的停顿时间
-XX:+PrintGCApplicationConcurrentTime:应用程序在 GC 停顿期间的执行时间
-Xloggc:C:/gc.log:将 GC 日志信息输出到具体位置的文件中,便于日后日志分析
注意:详细的 GC 信息是进行 JVM 调优的重要参考信息,可以根据 GC 日志,设置合理的堆大小及相关垃圾回收器的参数
JVM 提供了一组参数,用于获取系统运行时加载、卸载类的信息,参数设置如下:
-XX:TraceClassUnloading:跟踪类的卸载情况
-verbose:class:相当于同时设置 -XX:TraceClassLoading 和 -XX:TraceClassUnloading 两个参数
-XX:+PrintClassHistogram:打印运行时实例的信息,当设置此参数后,使用 Ctrl + Break 会输出系统内类的统计信息,从左到右依次显示了序号、实例数量、总大小和类名等信息
-XX:DisableExplicitGC:禁止 GC 操作,即禁止在程序中使用 System.gc() 触发 Full GC
-Xingc:增量式的 GC ,增量式的 GC 使用特定的算法让 GC 线程和应用程序线程交叉执行,从而减少应用程序因 GC 产生的停顿时间
-XX:+UseLargePages:启用大页,使用大页后,内存分页的表项就会减少,从而提升CPU从虚拟内存地址映射到物理内存地址的能力
-XX:LargePageSizeInBytes:指定大页的大小
三、触发gc的条件和解决方案
1、System.gc()方法的调用
system.gc(), 此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。
解决方案:强烈建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
2、老年代代空间(old/Tenured)不足
老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的Full GC
解决方案:调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组
3、永久区(perm)或者元数据区(matespace)空间不足(jdk<=7 ,在jdk8里面是metaspace)
JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space
解决方案:为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC
4、CMS GC时出现promotion failed和concurrent mode failure
对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。
解决方案:增大survivor space、老年代空间或调低触发并发GC的比率(-XX:CMSInitiatingOccupancyFraction=70,预留空间为70%),但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX: CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
5、统计得到的Minor GC晋升到旧生代(Eden到S2和S1到S2的和)的平均大小大于老年代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。
解决方案:通过在启动时通过- java -Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
6、堆中分配很大的对象
所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。
解决方案:为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,空间碎片问题没有了,但停顿时间不得不变长了,JVM设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的
参考:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BABFAFAE