指标

YoungGC  几分钟或几十分钟一次 几十毫秒的gc是比较好的

大内存机器使用G1垃圾回收器比较好,否则大机器内存大垃圾回收时间较长


 

通用调优

-XX:+DisableExplicitGC      手动调用System.gc()就不会生效,减少人为FullGC, 拓展: 有些NIO框架比如Netty框架经常会使用DirectByteBuffer来分配堆外内存,,在分配之前会显式的调用System.gc(),如果开启了DisableExplicitGC这个参数,会导致System.gc()调用变成一个空调用,没有任何作用,反而会导致Netty框架无法申请到足够的堆外内存,从而产生java.lang.OutOfMemoryError: Direct buffer memory,既然不推荐使用DisableExplicitGC这个参数,那有没有什么办法能尽量减少显式调用System.gc()带来的GC停顿呢,JVM提供了ExplicitGCInvokesConcurrent和ExplicitGCInvokesConcurrentAndUnloadsClasses这两个参数来保证显式调用System.gc()触发的是一个并发GC周期而不是Full GC,这两个参数只能配合CMS使用(-XX:+UseConcMarkSweepGC),CMS GC周期内也会做reference-processing,因此也能够触发对DirectByteBuffer内存的回收,减少了Full GC带来的长时间停顿.

尽量让每次YoungGC能放入Survivor,避免直接进入老年代,使老年代对象增多发生FULLGC

G1回收器调优

年轻代 调整-XX:MaxGCPauseMillis  让系统的gc频率别太高,同时每次gc停顿时间也别太长,达到一个合理值。

年轻代 也是调整-XX:MaxGCPauseMillis 在保证他的新生代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc。

-XX:G1MaxNewSizePercent 默认值:60% 限定了新生代最多占用堆内存 60% 的空间

-XX:MaxGCPauseMillis 默认值是 200 ms  意味着每次 进行垃圾回收,最长的停顿时间不超过 200ms。这也是为什么 G1 号称它造成的 STW 是停顿可控的

-XX:PretenureSizeThreshold 来控制多大的对象才算大, 大对象直接送到老年代  单位为Byte

-XX:InitiatingHeapOccupancyPercent 默认值是 45%  如果 老年代 占据了堆内存的 45% 的 Region 的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收

-XX:ParallelGCThreads: 在 stw 阶段工作的 GC 线程数 , 可以根据当前机器 CPU 核数来设置 , 建议核心数 -1

-XX:ConcGCThreads: 在非 stw 阶段工作的 GC 线程数 , 会影响系统的吞吐量 , 毕竟是要跟用户线程抢 CPU 资源,系统如果是计算密集型的建议是 CPU 核数的 1/4 ~ 1/3 , iO 密集型建议是 1/2

-XX:G1ReservePercent: G1为分配担保预留的空间比例 也就是老年代留多少空间给 新生代来晋升 , 默认是 10%,如果晋升失败会触发单线程的 old gc 非常恐怖 , 建议高并发系统加大机器内存 提高这个参数的比例

-XX:MaxMetaspaceSize: 元空间最大大小 , 在高并发且机器内存够的情况 建议增大元空间的大小,稍微大的点系统都会有很多依赖的组件,这些组件底层都有可能会用到一些反射 或者 字节码框架 , 会生成一些你看不懂类名的类,一旦第三方框架出现问题 , 你的系统很有可能也会受影响,调大元空间 , 有监控系统的设置报警机制 , 给自己系统争取一些缓冲时间也是有必要的

-XX:TraceClassLoading -XX:TraceClassUnloading  追踪类加载和类卸载的情况 , 可以在 Tomcat 的 catalina.out 日志文件中,打印出来JVM中加载了哪些类,卸载了哪些类

-XX:SoftRefLRUPolicyMSPerMB: JVM 可以忍受多久 软引用不被回收,如果是 0 则每次都会把软引用回收掉释放内存,有一个情况是反射在 15 次后会动态生成一些软引用类来提高反射的效率 , 当 ygc 的时候把这些软应用给回收了,但是它们的类加载器或者一些奇怪名字的类还在元空间 , 那下次要用这个反射对象的时候又得重新创建,就造成了元空间慢慢无限增大从而触发 OOM , 建议这个参数设置 2000 - 5000 单位是: ms

-XX:+DisableExplicitGC: 关闭显示的调用 System.gc() , System.gc() 是触发类似 full gc 的操作 
    开启 or 关闭 有两个情况:
        关闭: 防止 team 里有刚入职的小天才写完一个业务逻辑就给你来一个 System.gc() 来优化内存 (别问 问那个小天才就是我)
        开启: 项目里面有 Nio 相关的操作会用到直接内存 , 在 Java 中是 DirecByteBuffer 对象来申请的
    在某些不合理的情况下导致控制这块区域的 DirecByteBuffer 会晋升到老年代, Nio 在申请堆外内存空间不足的时候会手动调用 System.gc() 去回收 DirecByteBuffer 堆外内存,有用到 Nio 的系统把这个参数关掉是有一定概率发生 Direct buffer memory 的,关闭还是打开取决于你自己的系统 , 以及能不能做到 code review 不让程序员自己去显示的调用 System.gc()

-XX:G1MixedGCCountTarget: 设置垃圾回收混合回收阶段,默认8 最多可以拆成几次回收,G1 的垃圾回收是分为 初始标记、并发标记、最终标记、混合回收 这几个阶段的,其中混合回收是可以并发的反复回收多次 , 这样的好处是避免单次停顿回收 stw 时间太长,停止系统一会儿 , 回收掉一些 Region , 再让系统运行一会儿 , 然后再次停止系统一会儿 , 再次回收掉一些 Region,这样可以尽可能让系统不要停顿时间过长 , 可以在多次回收的间隙 , 也运行一下

备选:

-XX:+UseGCLogFileRotation 开或关闭GC日志滚动记录功能

-XX:NumberOfGCLogFiles=5 设置滚动日志文件的个数

-XX:GCLogFileSize=32M 设置滚动日志文件的大小,当前写日志文件大小超过该参数值时,日志将写入下一个文件

G1 尽量避免对象过快进入老年代 ,尽量避免频繁触发 mixed gc , 就可以做到根本上优化 mixed gc 了,Mixed gc 优化的核心还是 -XX:MaxGCPauseMills 这个参数 核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值 , 在保证新生代 gc 别太频繁的同时 , 还得考虑每次 gc 过后的存活对象有多少

FullGC

FullGC  gc发生异常情况,元空间、直接内存这些区域快满了都会触发FullGC

FullGC 使用-XX:+UseSerialGC使用单线程的进行 gc 这个过程是极慢极慢的

CMS调优

-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=5"两个参数的含义,就是在5次Full GC之后会触发一次Compaction操作,也就是压缩操作,如果频繁FullGC,可能是内存碎片严重 建议调低此值,多进行数据压缩处理

虚拟机参数模板推荐配置

机器4c 8g

export BASE_DIR=$(dirname $0)/..
export CUR_DATETIME=`date +"%Y-%m-%d-%H:%M:%S"`

JAVA_OPTS="${JAVA_OPTS} -Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92"

JAVA_OPTS="${JAVA_OPTS} -XX:+ UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParllnitialMarkEnabled -XX:+CMSScavengeBeforeRemark"

JAVA_OPTS="${JAVA_OPTS} -Xloggc:${BASE_DIR}/gc_logs/tomcat_gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps"

JAVA_OPTS="${JAVA_OPTS} -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${BASE_DIR}/heapdump/heapdump-${CUR_DATETIME}.hprof"

-XX:+CMSParllnitialMarkEnabled  这个参数会在CMS垃圾回收器的“初始标记”阶段开启多线程并发执行。

-XX:+CMSScavengeBeforeRemark 这个参数会在CMS的重新标记阶段之前,先尽量执行一次Young GC。

-XX:CMSInitiatingOccupancyFaction=92  老年代最大使用率,超过这个值进行垃圾回收

-XX:+UseCMSCompactAtFullCollection  -XX:CMSFullGCsBeforeCompaction=0  执行FullGC几次后,会进行内存压缩

备选:

-XX:+UseGCLogFileRotation 开或关闭GC日志滚动记录功能

-XX:NumberOfGCLogFiles=5 设置滚动日志文件的个数

-XX:GCLogFileSize=32M 设置滚动日志文件的大小,当前写日志文件大小超过该参数值时,日志将写入下一个文件

JVM优化思路

估算访问量,每次创建对象大小,估算回收对象大小,剩余对象大小,确定Survivor大小,Survivor大于剩余对象,防止回收后进入老年代。

估算完对程序进行压测,使用jstat -gc PID 查看系统gc情况确定实际的内存大小 。

年轻代频繁gc   调说明年轻代Eden可能有些小了,调大年轻代的内存大小或者Eden内存大小,需要整体查看。如果每次YoungGC几毫秒,不怎么发生OldGC,也是比较好的情况。

老年代频繁gc  说明YoungGC进入老年代的存活对象太多了,需要调大Survivor, -XX:SurvivorRatio=2"表明,Eden:Survivor:Survivor的比例为2:1:1。同时有需要也可以调大年轻代的内存大小

垃圾回收触发机制

1. Young GC的触发时机
    其实之前几周的文章里,我们已经分析的很清楚了,Young GC其实- -般就是 在新生代的Eden区域满了之后就会触发,采用复制算法来回收新生代的垃圾
 

2.Old GC和Full GC的触发时机
其实之前的文章里也对Old GC的触发时机说的很清晰了,简而言之就是下面几种情况:

(1)发生Young GC之前进行检查,如果“老年代可用的连续内存空间" <“新生代历次Young GC后升入老年代的对象总和的平均大小”,说明本次Young GC后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间此时必须先触发-次Old GC给老年代腾出更多的空间,然后再执行Young GC

(2)执行Young GC之后有一批对象需要 放入老年代,此时老年代就是没有足够的内存空间存放这些对象了,此时必须立即触发一次Old GC

(3)老年代内存使用率超过了92%,也要直接触发Old GC,当然这个比例是可以通过参数调整的其实说白了,

上述三个条件你概括成-句话,就是老年代空间也不够了,没法放入更多对象了,这个时候务必执行OldGC对老年代进行垃圾回收。

3.永久代满了之后怎么办?
大家现在既然都知道了,Full GC有上述几个触发条件,同时触发Full GC的时候其实会带上针对新生代的Young GC,也会有针对老年代的Full GC,还会有针对永久代的GC。所以假如存放类信息、常量池的永久代满了之后,就会触发一次Full GC。这样Full GC执行的时候,就会顺带把永久代中的垃圾给回收了,但是永久代中的垃圾一般是很少的,因为里面存放的都是一些类,还有常量池之类的东西,这些东西通常来说是不需要回收的。如果永久代真的放满了,回收之后发现没腾出来更多的地方,此时只能抛出内存不够的异常了。
 

新生代晋升老年代

对象优先在Eden分配,且新生代对象晋升到老年代有多种情况,

现在做一个总结:

(1)、 空间分配担保

      Eden区满时,进行Minor GC,当Eden和一个Survivor区中依然存活的对象无法放入到Survivor中,则通过分配担保机制提前转移到老年代中。 是把当前需要回收的Eden和Survivor的存活对象放入老年代。
      在发生Minor GC之前, 虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间, 如果这个条件成立, 那这一次Minor GC可以确保是安全的。 如果不成立, 则虚拟机会先查看-XX: HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure) ; 如果允许, 那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小, 如果大于, 将尝试进行一次Minor GC, 尽管这次Minor GC是有风险的; 如果小于, 或者-XX:HandlePromotionFailure设置不允许冒险, 那这时就要改为进行一次Full GC。( JDK 6 Update 24之前)  

        在JDK 6 Update 24之后, 这个测试结果就有了差异, -XX: HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略, 观察OpenJDK中的源码变化(见代码清单3-12) , 虽然源码中还定义了-XX: HandlePromotionFailure参数, 但是在实际虚拟机中已经不会再使用它。 JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小, 就会进行Minor GC, 否则将进行Full GC。

(2)、大对象直接进入老年代

        若对象体积太大, 新生代无法容纳这个对象,  HotSpot虚拟机提供了-XX: PretenureSizeThreshold参数, 指定大于该设置值的对象直接在老年代分配, 这样做的目的就是避免在Eden区及两个Survivor区之间来回复制, 产生大量的内存复制操作。 此参数只对Serial及ParNew两款收集器有效。

(3)、长期存活的对象将进入老年代。

        虚拟机给每个对象定义了一个对象年龄(Age) 计数器, 存储在对象头中(详见第2章) 。 对象通常在Eden区里诞生, 如果经过第一次Minor GC后仍然存活, 并且能被Survivor容纳的话, 该对象会被移动到Survivor空间中, 并且将其对象年龄设为1岁。 对象在Survivor区中每熬过一次Minor GC, 年龄就增加1岁, 当它的年龄增加到一定程度(默认为15) , 就会被晋升到老年代中。 对象晋升老年代的年龄阈值, 可以通过参数-XX:MaxTenuringThreshold设置。

(4)、动态对象年龄判定。

       描述1: HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX: MaxTenuringThreshold才能晋升老年代, 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代, 无须等到-XX:MaxTenuringThreshold中要求的年龄。(深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) 周志明 )

       描述2:动态年龄判定规则,如果一次新生代gc过后, 发现Survivor区域中的几个年龄的对象加起来超过了Survivor区域的50%,比如说年龄1+年龄2+年龄3的对象大小总和,超过了Survivor区域的50%,此时就会把年龄3以上的对象都放入老年代。
(从 0 开始带你成为JVM实战高手 救火队队长描述

        描述3:假设

                MaxTenuringThreshold为15
                年龄1的对象占用了33%
                年龄2的对象占用33%
                年龄3的对象占用34%。

          -XX:TargetSurvivorRatio 目标存活率,默认为50%  也就是上面说的50%

           结果 : 年龄1的占用了33%,年龄2的占用了33%,累加和超过默认的TargetSurvivorRatio(50%),年龄2和年龄3的对象都要晋升。

           结论: 动态对象年龄判断,主要是被TargetSurvivorRatio这个参数来控制。而且算的是年龄从小到大的累加和,而不是某个年龄段对象的大小。

      (jvm误区--动态对象年龄判定  https://my.oschina.net/xpbob/blog/2221709  描述

使用场景

CMSConcurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。 希望系统停顿时间尽可能短, 以给用户带来良好的交互体验。 缺点:产生大量空间碎片、并发阶段会降低吞吐量

G1 适合大内存机器,可控范围的低停顿
 

03-13 09:27