一  问题描述

1  频繁Full GC  

  现象描述:堆内存占用较快,运行到3天时就达到100%,并触发了老年代GC

  

JVM知识回顾:
  Java的堆内存由新生代(New or Young)和老年代(Old)组成。新生代进一步划分为一个Eden空间和两个Survivor空间S0、S1(也称为From、To)。Eden空间是对象被创建时的地方,经过新生代GC后存活的对象存放到Survivor空间,经过几轮新生代GC仍然存活会存放到Old区。

  在JVM默认参数下,Survivor空间太小。导致在GC清理后,Eden区很多对象无法存放到Survivor区。因此,这些对象被过早地保存在老年代中,这会导致老年代占用增长很快。通过jmap -heap pid查看堆内存分布情况如下:

  

  上图是设置了SurviorRatio=6时(Eden区与Survivor区比例为6:1),按照计算发现Survior区比设置的小很多。这个数值还是动态变化的,有时候十几兆,有时候几兆。原因是:JDK8使用ParallelGC作为默认GC算法,在这个算法下使用了“AdaptiveSizePolicy”策略,它会动态调整Eden和Survivor的空间。如果开启 AdaptiveSizePolicy,则每次 GC 后会重新计算 Eden、From 和 To 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量。

  关闭AdaptiveSizePolicy有两种办法:

    1. 通过JMV参数关闭:-XX:-UseAdaptiveSizePolicy

    2. 老年代使用CMS回收器,CMS不会开启UseAdaptiveSizePolicy。开启CMS回收器,新生代会默认使用ParNew GC回收器(并发收集器)。 

-XX:SurvivorRatio=6 -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSScavengeBeforeRemark -XX:NativeMemoryTracking=detail
-XX:+UseConcMarkSweepGC  使用CMS收集器

-XX:+ UseCMSCompactAtFullCollection  Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长

-XX:+CMSFullGCsBeforeCompaction  设置进行几次Full GC后,进行一次碎片整理

-XX:ParallelCMSThreads  设定CMS的线程数量(一般情况约等于可用CPU数量)

-XX:+CMSScavengeBeforeRemark  在remark之前强制进行一次Young GC。

  修改参数优化后:

  

 优化后效果:

  

2  对堆外内存估算少

  Java程序占用内存分为堆内存和非堆内存,堆内存里分新生代和老年代,非堆内存包含方法区、jdk8的MetaSpace、栈空间、程序计数器等。我们经常关注的是堆内存,而较少关注非堆内存。酒店静态外网接口应用缩容时,容器内存是2G,并修改了堆内存最大限制为1.5G(-Xms1536m -Xmx1536m)。也就是说java进程的非堆内存+操作系统的其他进程占用的内存如果不超过500M,就不会引起oom问题。通过使用Native Memory Tracking (NMT) 来分析JVM内部内存使用情况,可以发现非堆内存占用达到800~900M。因此系统长时间运行后,内存占用可能会超出2G,因此宕机。

   Native Memory Tracking (NMT) 是Hotspot VM用来分析VM内部内存使用情况的一个功能。在JVM参数中加入-XX:NativeMemoryTracking=detail,打开NMT。然后登录堡垒机,通过jcmd VM.native_memory summary命令,可以查看内存占用情况。

  下面是对一个2G内存容器,Xmx为1G的应用的统计。每一项含义说明,见官方说明

  其中第一项Java Heap是堆内存。其他就可以理解外非堆内存的占用,共700M。其中Thread项是线程占用的内存,默认每个线程1M,一共303个线程占用了300M。

Native Memory Tracking:

Total: reserved=3000668KB, committed=1755712KB
-                 Java Heap (reserved=1048576KB, committed=1048576KB)
                            (mmap: reserved=1048576KB, committed=1048576KB) 
 
-                     Class (reserved=1126128KB, committed=85464KB)
                            (classes #13164)
                            (malloc=1776KB #25469) 
                            (mmap: reserved=1124352KB, committed=83688KB) 
 
-                    Thread (reserved=311789KB, committed=311789KB)
                            (thread #303)
                            (stack: reserved=310456KB, committed=310456KB)
                            (malloc=979KB #1567) 
                            (arena=355KB #605)
 
-                      Code (reserved=258501KB, committed=54209KB)
                            (malloc=8901KB #11187) 
                            (mmap: reserved=249600KB, committed=45308KB) 
 
-                        GC (reserved=101204KB, committed=101204KB)
                            (malloc=97784KB #800) 
                            (mmap: reserved=3420KB, committed=3420KB) 
 
-                  Compiler (reserved=456KB, committed=456KB)
                            (malloc=325KB #600) 
                            (arena=131KB #3)
 
-                  Internal (reserved=87776KB, committed=87776KB)
                            (malloc=87744KB #19745) 
                            (mmap: reserved=32KB, committed=32KB) 
 
-                    Symbol (reserved=18473KB, committed=18473KB)
                            (malloc=14533KB #158025) 
                            (arena=3940KB #1)
 
-    Native Memory Tracking (reserved=3674KB, committed=3674KB)
                            (malloc=215KB #3261) 
                            (tracking overhead=3459KB)
 
-               Arena Chunk (reserved=226KB, committed=226KB)
                            (malloc=226KB) 
 
-                   Unknown (reserved=43864KB, committed=43864KB)
                            (mmap: reserved=43864KB, committed=43864KB)

解决方案建议

  1、设置Xmx和Xms时要为堆外内存留出足够的空间,建议大于1G,如果内存资源允许大于2G。

  2、优化JVM参数。
    a) 对应接口类型的应用,使用CMS回收器,减少停顿时间,保证接口性能。

-XX:SurvivorRatio=6-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0-XX:+CMSScavengeBeforeRemark -XX:NativeMemoryTracking=detail

  b) 对应后台定时任务,MQ消费监听等类型的应用,使用默认的Parallel Scavenge+Parallel Old回收器,优先保证吞吐量。并关闭UseAdaptiveSizePolicy参数。

-XX:SurvivorRatio=6-XX:-UseAdaptiveSizePolicy

二  JVM GC调优

1  GC回收的区域

  JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。

2  如何判断对象是否可被回收

  (1)判断对象是否存活

  a)引用计数算法:

    在对象头维护着一个 counter 计数器,对象被引用一次则计数器 +1;若引用失效则计数器 -1。当计数器为 0 时,就认为该对象无效了。任何时刻计数器为0的对象就是不可能再被使用的。

  优点:实现简单,判定效率高效,被actionscript3和python中广泛应用。
  缺点:无法解决对象之间的相互引用问题。java没有采纳

    主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

public class ReferenceCountingGC {

    public Object instance;
    public ReferenceCountingGC(String name){}
}

public static void testGC(){

    ReferenceCountingGC a = new ReferenceCountingGC("objA");
    ReferenceCountingGC b = new ReferenceCountingGC("objB");

    a.instance = b;
    b.instance = a;

    a = null;
    b = null;
}

    

     我们可以看到,最后这2个对象已经不可能再被访问了,但由于他们相互引用着对方,导致它们的引用计数永远都不会为0,通过引用计数算法,也就永远无法通知GC收集器回收它们。

  b)可达性分析算法:

    通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连的时候,则证明此对象是不可用的。

    比如如下,右侧的对象是到GCRoot时不可达的,可以判定为可回收对象。

    

    在java中,可以作为GCRoot的对象包括以下几种:

  * 虚拟机栈中引用的对象。

  * 方法区中静态属性引用的对象。

  * 方法区中常量引用的对象。

  * 本地方法中JNI引用的对象。

  基于以上,我们可以知道,当当前对象到GCRoot中不可达时候,即会满足被垃圾回收的可能。

  (2)判断是否可以被回收

  那么是不是这些对象就非死不可,也不一定,此时只能宣判它们存在于一种“缓刑”的阶段,要真正的宣告一个对象死亡。至少要经历两次标记:

第一次:对象可达性分析之后,发现没有与GCRoots相连接,此时会被第一次标记并筛选。

第二次:对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,此时会被认定为没必要执行。

    在finalize里可以将该对象重新赋予给某个引用,从而使对象不会被回收。

3  什么时候执行GC

  Eden区空间不够存放新对象的时候,执行Minor GC。升到老年代的对象大于老年代剩余空间的时候执行Full GC,或者小于的时候被HandlePromotionFailure 参数强制Full GC 。

  调优主要是减少 Full GC 的触发次数,可以通过 NewRatio(新生代和老年代的大小比例) 控制新生代转老年代的比例,通过MaxTenuringThreshold 设置对象进入老年代的年龄阀值。

  基于分代垃圾回收机制,新生代分为:Eden区、两个Survivor区(From区和To区),每次只占用Eden区和一个Survivor区,另外一个Survivor区空闲。新创建的对象优先在Eden区分配分配内存空间,当Eden区的空间不足以存放新对象时,会触发一次Minor GC,将Eden区和一个Survivor区中存活的对象复制到另一个Survivor区,并且对象的年龄+1,然后清空Eden区和Survivor区。在满足如下情况之一,新生代中的对象会移动到老年代:

  (1)Eden区满时,进行Minor GC,当Eden和一个Survivor区中依然存活的对象无法放入到另一个Survivor中,则通过分配担保机制提前转移到老年代中。 

  (2)若对象体积太大, 新生代无法容纳这个对象,-XX:PretenureSizeThreshold超过这个值的时候,对象直接在old区分配内存,默认值是0,意思是不管多大都是先在eden中分配内存, 此参数只对Serial及ParNew两款收集器有效。

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

          虚拟机对每个对象定义了一个对象年龄(Age)计数器。当年龄增加到一定的临界值时,就会晋升到老年代中,该临界值由参数:-XX:MaxTenuringThreshold来设置。

          如果对象在Eden出生并在第一次发生MinorGC时仍然存活,并且能够被Survivor中所容纳的话,则该对象会被移动到Survivor中,并且设Age=1;以后每经历一次Minor GC,该对象还存活的话Age=Age+1。

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

          虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor区中相同年龄(设年龄为age)的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄(age)的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

    JVM引入动态年龄计算,主要基于如下两点考虑:

    1. 如果固定按照MaxTenuringThreshold设定的阈值作为晋升条件: a)MaxTenuringThreshold设置的过大,原本应该晋升的对象一直停留在Survivor区,直到Survivor区溢出,一旦溢出发生,Eden+Svuvivor中对象将不再依据年龄全部提升到老年代,这样对象老化的机制就失效了。 b)MaxTenuringThreshold设置的过小,“过早晋升”即对象不能在新生代充分被回收,大量短期对象被晋升到老年代,老年代空间迅速增长,引起频繁的Major GC。分代回收失去了意义,严重影响GC性能。

    2. 相同应用在不同时间的表现不同:特殊任务的执行或者流量成分的变化,都会导致对象的生命周期分布发生波动,那么固定的阈值设定,因为无法动态适应变化,会造成和上面相同的问题。

  持久代(Permanent generation)也称之为方法区(Method area):用于保存类常量以及字符串常量。注意,这个区域不是用于存储那些从老年代存活下来的对象,这个区域也可能发生GC。发生在这个区域的GC事件也被算为 Major GC 。只不过在这个区域发生GC的条件非常严苛,必须符合以下三种条件才会被回收:

  1、所有实例被回收

  2、加载该类的ClassLoader 被回收

  3、Class 对象无法通过任何途径访问(包括反射)

  Major GC:清理永久代,但是由于很多MojorGC 是由MinorGC 触发的,所以有时候很难将MajorGC 和MinorGC区分开。

  FullGC:是清理整个堆空间—包括年轻代和永久代。FullGC 一般消耗的时间比较长,远远大于MinorGC,因此,有时候我们必须降低FullGC 发生的频率。

4  垃圾回收算法 

  (1)“标记-清除”(Mark-Sweep)算法:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

    它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

  (2)“复制”(Mark-Copying)算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

  这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

  (3)“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

  在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,因此一般选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或“标记-整理”算法来进行回收。

5  系统崩溃前的一些现象

  (1)每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s

  (2)FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC

  (3)老年代的内存越来越大并且每次FullGC后老年代没有内存被释放

   之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值。

6  原因分析

  (1)为什么崩溃前垃圾回收的时间越来越长?

    根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记、清除(复制),标记部分只要内存大小固定时间是不变的,变的是复制部分,因为每次垃圾回收都有一些回收不掉的内存,所以增加了复制量,导致时间延长。所以,垃圾回收的时间也可以作为判断内存泄漏的依据。

  (2)为什么Full GC的次数越来越多?

    因此内存的积累,逐渐耗尽了年老代的内存,导致新对象分配没有更多的空间,从而导致频繁的Full GC垃圾回收。

  (3)为什么年老代占用的内存越来越大?

    因为年轻代的内存无法被回收,越来越多的对象被Copy到年老代。

8  调优目标

  (1)将进入老年代的对象数量降到最低

  (2)减少Full GC的执行时间

9  调优方法

  (1)针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值。

  (2)年轻代和年老代将根据默认的比例(1:2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整二者之间的大小。也可以针对回收代,比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小

  (3)合理设置年轻代和老年代大小

    更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间,小的年老代会导致更频繁的Full GC。

    更小的年轻代必然导致更大年老代,小的年轻代会导致young GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率,但是会增加老年代的gc时间。

    如何选择应该依赖应用程序对象生命周期的分布情况:

      a)如果应用存在大量的临时对象,应该选择更大的年轻代;

      b)如果存在相对较多的持久对象,年老代应该适当增大。

    但很多应用都没有这样明显的特性,在抉择时应该根据以下两点:

     (A)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理

     (B)通过观察应用一段时间,看应用在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间。

  (4)在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC ,默认为Serial收集器。

  (5)线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。最大线程数计算公式如下:

    (MaxProcessMemory – JVMMemory – ReservedOsMemory) / (ThreadStackSize) = Number of threads

    注:

      MaxProcessMemory:进程最大寻址空间。(一般为服务器内存)

      JVMMEMORY:JVM的内存空间(堆+永久区)即-Xmx大小 (应该是实际分配大小)

      ReservedOsMemory:操作系统预留内存

      ThreadStackSize:-Xss大小

  (6)可以通过下面的参数打Heap Dump信息

      -XX:HeapDumpPath

      -XX:+PrintGCDetails

      -XX:+PrintGCTimeStamps

      -Xloggc:/usr/aaa/dump/heap_trace.txt

    通过下面参数可以控制OutOfMemoryError时打印堆的信息

      -XX:+HeapDumpOnOutOfMemoryError

10  调优案例

   (1)一个服务系统,经常出现卡顿,分析原因,发现Full GC时间太长

    执行命令: jstat  -gc  pid:

 S0C        S1C        S0U      S1U      EC       EU        OC         OU       MC       MU        CCSC       CCSU      YGC     YGCT    FGC    FGCT     GCT   
4032.0     4032.0     2541.6    0.0    32320.0  17412.6   80620.0    51918.6   88576.0   84546.7   12288.0   11336.9    522     5.408   5      6.946    5.952

  解释如下:

  S0C:第一个幸存区的大小;S1C:第二个幸存区的大小;S0U:第一个幸存区的使用大小;S1U:第二个幸存区的使用大小;EC:Enden区的大小;EU:Enden区的使用大小;

  OC:老年代大小;OU:老年代使用大小;MC:方法区大小;MU:方法区使用大小;CCSC:压缩类空间大小;CCSU:压缩类空间使用大小;YGC:年轻代垃圾回收次数;

  YGCT:年轻代垃圾回收消耗时间;FGC:老年代垃圾回收次数;FGCT:老年代垃圾回收消耗时间;GCT:垃圾回收消耗总时间

  分析上面的数据,发现Young GC执行了522次,耗时5.408秒,每次Young GC耗时1ms,在正常范围,而Full GC执行了5次,耗时6.946秒,每次平均1.389s,数据显示出来的问题是:Full GC耗时较长,分析该系统的参数发现,NewRatio=9,也就是说,新生代和老生代大小之比为1:9,这就是问题的原因:

  1,新生代太小,导致对象提前进入老年代,触发老年代发生Full GC;

  2,老年代较大,进行Full GC时耗时较大;

  优化的方法是:调整NewRatio的值,调整到4,发现Full GC没有再发生,只有Young GC在执行。这就是把对象控制在新生代就清理掉,没有进入老年代(这种做法对一些应用是很有用的,但并不是对所有应用都要这么做)

   (2)一应用在性能测试过程中,发现CPU占用率很高,Full GC频繁,使用jmap -dump:format=b,file=文件名.hprof pid 来dump内存,生成dump文件,并使用Eclipse下的mat工具进行分析,发现:

  

  从图中可以看出,这个线程存在问题,队列LinkedBlockingQueue所引用的大量对象并未释放,从而导致一直在执行Full GC,从而导致CPU占用率达到100%。

11  生产环境问题定位方法

  (1)通过ps -ef | grep java或者执行top -c ,显示进程运行信息列表。按下P,进程按照cpu使用率排序获取程序的pid

  

  (2)查看该pid下线程对应的系统占用情况。top -Hp 8813 ,显示一个进程的线程运行信息列表。按下P,进程按照cpu使用率排序

  

  (3)发现pid 8851线程占用的CPU最大

  (4)将这几个pid转为16进制, printf “0x%x\n” 8851 为0x2293

  (5)下载当前的java线程栈jstack pid>pid.log 将线程栈 dump 到日志文件中

  (6)在日志中查询(5)中对应的线程情况,发现都是GC线程导致的 

  (7)dump java堆数据

    jmap -dump:format=b,file=heap.log pid 保存堆快照

  (8)使用MAT加载堆文件,可以看到javax.crypto.JceSecurity对象占用了95%的内存空间,初步定位到问题。然后排查代码,查看是什么导致的内存溢出。例如使用了静态变量的Map,所以每次运行到某个方法时都会向这个Map put一个对象,而这个map属于类的维度,所以不会被GC回收。这就导致了大量的new的对象不被回收。

  (9)优化代码,如果通过这个流程无法解决问题,或者没法优化代码,那么走最后一步:GC调优:

12  GC调优流程

(1)GC日志介绍

  YoungGC日志解释如下:

  

  FullGC日志解释如下:

  

(2)查看GC 日志 

    1768.617: [GC [PSYoungGen: 1313280K->31072K(1341440K)] 3990240K->2729238K(4137984K), 0.0992420 secs] [Times: user=0.36 sys=0.01, real=0.10 secs]

    1770.525: [GC [PSYoungGen: 1316704K->27632K(1345536K)] 4014870K->2750306K(4142080K), 0.0552640 secs] [Times: user=0.20 sys=0.00, real=0.06 secs]

    [Full GC [PSYoungGen: 47079K->0K(1350144K)] [ParOldGen: 2780532K->191662K(2796544K)] 2827611K->191662K(4146688K)

    [PSPermGen: 60530K->60530K(524288K)],3.4921610 secs] [Times: user=13.39 sys=0.08, real=3.49 secs]

  日志介绍:

    PSYoungGen: 1313280K->31072K(1341440K)]

      格式为[PSYoungGen: a->b(c)]。PSYoungGen表示新生代使用的是多线程垃圾收集器Parallel Scavenge。a为GC前新生代已占用空间,b为GC后新生代已占用空间。新生代又细分为一个Eden区和两个Survivor区,Minor GC之后Eden区为空,b就是Survivor中已被占用的空间。括号里的c表示整个新生代的大小。

    3990240K->2729238K(4137984K)

      格式为x->y(z)。x表示GC前堆的已占用空间,y表示GC后堆已占用空间,z表示堆的总大小。

      由新生代和Java堆占用大小可以算出年老代占用空间,此例中就是4137984K-1341440K=2796544k=2731M。

    [Times: user=0.36 sys=0.01, real=0.10 secs]

      提供cpu使用及时间消耗,user是用户态消耗的cpu时间,sys是系统态消耗的cpu时间,real是实际的消耗时间。

    分析上述日志,可以看出两个问题:

      1. 每次Minor GC,晋升至老年代的对象体积较大,平均为20m+(2750306K-2729238K=21068K=20.57M),这导致老年代占用持续升高,Full GC频率较高,直观现象是内存占用一直升高;

      2. Full GC的时间很长,上面看到的是3.49 secs,这导致ull FGC开销很大,直观现象是CPU占用一直上升,达到100%;

    因而调优思路很明确:

       1. 减少每次Young GC晋升到老年代的对象大小;

       2. 尽可能的减少每次Full GC的时间开销;

(3)进行了如下的尝试

  一. 新生代使用默认的Parallel Scavenge GC,但是加入如下参数 :

    -Xmn1350m -XX:-UseAdaptiveSizePolicy  -XX:SurvivorRatio=6

  调优思路:

    Young GC每次晋升到Old Gen的内容较多,而这很可能是因为Parallel Scavenge垃圾收集器会动态的调整JVM的Eden 和Survivor区,导致Survior空间过小,导致更多对象进入老年代。 

    -Xmn1350m设置堆内新生代的大小。通过这个值可以得到老生代的大小:-Xmx减去-Xmn。

    -XX:-UseAdaptiveSizePolicy表示关闭动态调整年轻代区大小和相应的Survivor区比例, -XX:SurvivorRatio=6表示Eden去和Suvior区的大小比例为6:2:2

  调优效果:

     

  可以看到调优后full gc频率大为减少(由4min一次--->变为30h一次),同时因为少了频繁调整new gen的开销,ygc耗时也略微减少了。

  遗留问题:

    虽然Full GC频率大为降低,但是每次Full GC的耗时还是一样,500ms+~2000ms

  二. 老年代改用CMS GC,加入jvm参数如下(原来的配置不变):

    -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+UseCMSInitiatingOccupancyOnly 

    -XX:CMSInitiatingOccupancyFraction=70

  调优思路:

    老年代使用CMS GC,启用碎片整理,降低Full GC的耗时,此时新生代会默认使用ParNew GC收集器

     调优效果:

       

  Oldgen GC开销还是较大,虽然比ps gc略好,而且通过gc日志发现,主要耗时都是在remark的rescan阶段

  91832.767: [CMS-concurrent-mark-start]
  91834.022: [CMS-concurrent-mark: 1.256/1.256 secs] [Times: user=4.06 sys=0.94, real=1.25 secs]
  91834.022: [CMS-concurrent-preclean-start]
  91834.040: [GC91834.040: [ParNew: 1091621K->50311K(1209600K), 0.0469420 secs] 3059979K->2018697K(4021504K), 0.0473540 secs] [Times: user=0.16   sys=0.01, real=0.05 secs]
  91834.123: [CMS-concurrent-preclean: 0.051/0.101 secs] [Times: user=0.31 sys=0.05, real=0.10 secs]
  91834.123: [CMS-concurrent-abortable-preclean-start]
  91834.900: [CMS-concurrent-abortable-preclean: 0.769/0.777 secs] [Times: user=2.36 sys=0.53, real=0.78 secs]
  91834.903: [GC[YG occupancy: 595674 K (1209600 K)]91834.904: [Rescan (parallel) , 0.6762340 secs]91835.580: [weak refs processing, 0.0728400 secs]91835.653: [scrub string table, 0.0009380 secs] [1 CMS-remark: 1968386K(2811904K)] 2564060K(4021504K), 0.7555510 secs] [Times: user=2.73 sys=0.03, real=0.76 secs]
  91835.659: [CMS-concurrent-sweep-start]

  三. 降低remark的时间开销,加入参数:-XX:+CMSScavengeBeforeRemark

  调优思路:

    通常情况下进行remark会先对new gen进行一次扫描,而且这个开销占比挺大,所以加上这个参数,在remark之前强制进行一次Young GC。

三  垃圾收集器介绍  

1  相关概念

1  并行和并发

  并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

  并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。

2  吞吐量(Throughput)

  吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即:

  吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。

  假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

3  Minor GC 和 Full GC

  新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

  老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

2  Serial、SerialOld(-XX:+UseSerialGC)

  Serial收集器是一个串行收集器。在JDK1.3之前是Java虚拟机新生代收集器的唯一选择。目前也是ClientVM下ServerVM 4核4GB以下机器默认垃圾回收器。Serial收集器并不是只能使用一个CPU进行收集,而是当JVM需要进行垃圾回收的时候,需暂停所有的用户线程,直到回收结束。

  使用算法:新生代复制算法、老年代标记-整理算法;垃圾收集的过程中会Stop The World(服务暂停)

   

  JVM中文名称为Java虚拟机,因此它像一台虚拟的电脑在工作,而其中的每一个线程都被认为是JVM的一个处理器,因此图中的CPU0、CPU1实际上为用户的线程,而不是真正的机器CPU。

  Serial收集器虽然是最老的,但是它对于限定单个CPU的环境来说,由于没有线程交互的开销,专心做垃圾收集,所以它在这种情况下是相对于其他收集器中最高效的。

  SerialOld是Serial收集器的老年代收集器版本,它同样是一个单线程收集器,这个收集器目前主要用于Client模式下使用。如果在Server模式下,它主要还有两大用途:一个是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,另外一个就是作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。

2  ParNew(-XX:+UseParNewGC)

  ParNew其实就是Serial收集器的多线程版本。除了使用多线程,其他像收集算法、STW、对象分配规则、回收策略与 Serial 收集器完成一样,在底层上,这两种收集器也共用了相当多的代码。除了Serial收集器外,只有它能与CMS收集器配合工作。

  使用算法:标记-复制算法

  参数控制:

  -XX:+UseParNewGC  使用ParNew收集器

  -XX:ParallelGCThreads 限制线程数量

    

  ParNew是许多运行在Server模式下的JVM首选的新生代收集器。但是在单CPU的情况下,它的效率远远低于Serial收集器,因为线程切换需要消耗时间,所以一定要注意使用场景。

3  Parallel Scavenge(-XX:+UseParallelGC)

  Parallel Scavenge又被称为吞吐量优先收集器,和ParNew 收集器类似,是一个新生代并行收集器。目前是默认垃圾回收器。

  使用算法:复制算法

  参数控制:

  控制最大垃圾收集时间: -XX:MaxGCPauseMillis 

  直接设置吞吐量大小: -XX:GCTimeRatio(默认99%)

  XX:UseAdaptiveSizePolicy:开启这个参数后,就不需要手工指定新生代大小,Eden 与 Survivor 比例(SurvivorRatio)等细节,只需要设置好基本的堆大小(-Xmx 设置最大堆),以及最大垃圾收集时间与吞吐量大小,虚拟机就会根据当前系统运行情况收集监控信息,动态调整这些参数以尽可能地达到我们设定的最大垃圾收集时间或吞吐量大小这两个指标。

  Parallel Scavenge收集器的目标是达到一个可控件的吞吐量,所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。如果虚拟机总共运行了100分钟,其中垃圾收集花了1分钟,那么吞吐量就是99% 。

  Parallel Scavenge 收集器也是一个使用复制算法多线程,工作于新生代的垃圾收集器,看起来功能和 ParNew 收集器一样,它有啥特别之处吗?

  关注点不同,CMS 等垃圾收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 目标是达到一个可控制的吞吐量,也就是说 CMS 等垃圾收集器更适合用到与用户交互的程序,因为停顿时间越短,用户体验越好,而 Parallel Scavenge 收集器关注的是吞吐量,所以更适合做后台运算等不需要太多用户交互的任务。

4  ParallelOld(-XX:+UseParallelOldGC)

  ParallelOld是并行收集器,和SerialOld一样,ParallelOld是一个老年代收集器,是老年代吞吐量优先的一个收集器。这个收集器在JDK1.6之后才开始提供的,在此之前,Parallel Scavenge只能选择Serial Old来作为其老年代的收集器,这严重拖累了Parallel Scavenge整体的速度。而ParallelOld的出现后,“吞吐量优先”收集器才名副其实!

  使用算法:标记 - 整理算法

  

  在注重吞吐量与CPU数量大于1的情况下,都可以优先考虑Parallel Scavenge + ParalleloOld收集器。

5  CMS (-XX:+UseConcMarkSweepGC)

  CMS是一个老年代收集器,全称 Concurrent Low Pause Collector,是JDK1.4后期开始引用的新GC收集器,在JDK1.5、1.6中得到了进一步的改进。CMS是对于响应时间的重要性需求大于吞吐量要求的收集器对于要求服务器响应速度高的情况下,使用CMS非常合适。当CMS进行GC失败时,会自动使用Serial Old策略进行GC。开启CMS回收器,新生代会默认使用ParNew GC回收器

  CMS 收集器是以实现最短 STW 时间为目标的收集器,如果应用很重视服务的响应速度,希望给用户最好的体验,则 CMS 收集器是个很不错的选择!

  CMS的一大特点,就是用两次短暂的暂停来代替串行或并行标记整理算法时候的长暂停。

  使用算法:标记 - 清理

  参数控制:

-XX:+UseConcMarkSweepGC  使用CMS收集器

-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长

-XX:+CMSFullGCsBeforeCompaction  设置进行几次Full GC后,进行一次碎片整理

-XX:ParallelCMSThreads  设定CMS的线程数量(一般情况约等于可用CPU数量)

  

  CMS的执行过程如下:

  a)初始标记(STW initial mark)

    在这个阶段,需要虚拟机停顿正在执行的应用线程,官方的叫法STW(Stop Tow World)。这个过程从GC Roots扫描直接关联的对象,并作标记。这个过程会很快的完成。

  b)并发标记(Concurrent marking)

    这个阶段紧随初始标记阶段,在“初始标记”的基础上继续向下追溯标记,进行GC Roots Tracing的过程,在整个过程中耗时最长。注意这里是并发标记,表示用户线程可以和GC线程一起并发执行,这个阶段不会暂停用户的线程

  c)并发预清理(Concurrent precleaning)

    这个阶段仍然是并发的,JVM查找正在执行“并发标记”阶段时候进入老年代的对象(可能这时会有对象从新生代晋升到老年代,或被分配到老年代)。通过重新扫描,减少在一个阶段“重新标记”的工作,因为下一阶段会STW。

  d)重新标记(STW remark)

    这个阶段会再次暂停正在执行的应用线程,重新从重根对象开始查找并标记并发阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致),并处理对象关联。为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这一次耗时会比“初始标记”更长,并且这个阶段可以并行标记。此阶段也需要“Stop The World”。

  e)并发清理(Concurrent sweeping)

    这个阶段是并发的,应用线程和GC清除线程可以一起并发执行。

  f)并发重置(Concurrent reset)

    这个阶段仍然是并发的,重置CMS收集器的数据结构,等待下一次垃圾回收。

CMS的缺点:

  1、内存碎片。由于使用了 标记-清理 算法,导致内存空间中会产生内存碎片。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。

  为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。

  CMS收集器做了一些小的优化,就是把未分配的空间汇总成一个列表,当有JVM需要分配内存空间的时候,会搜索这个列表找到符合条件的空间来存储这个对象。但是内存碎片的问题依然存在,如果一个对象需要3块连续的空间来存储,因为内存碎片的原因,寻找不到这样的空间,就会导致Full GC。

  promotion failed问题:

  promotion failed是在进行Minor GC时,Survivor区放不下,对象只能放入老年代,而此时老年代也放不下造成的。多数是由于老年代有足够的空闲空间,但是由于碎片较多,这时如果新生代要转移到老年带的对象比较大,所以,必须尽可能提早触发老年代的CMS回收来避免这个问题(promotion failed时老年代CMS还没有机会进行回收,又放不下转移到老年代的对象,因此会出现下一个问题concurrent mode failure,需要stop-the-wold GC- Serail Old)。

  下面是一个promotion failed的一条gc日志:

106.641: [GC 106.641: [ParNew (promotion failed): 14784K->14784K(14784K), 0.0370328 secs]106.678: [CMS106.715: [CMS-concurrent-mark: 0.065/0.103 secs] [Times: user=0.17 sys=0.00, real=0.11 secs]
(concurrent mode failure): 41568K->27787K(49152K), 0.2128504 secs] 52402K->27787K(63936K), [CMS Perm : 2086K->2086K(12288K)], 0.2499776 secs] [Times: user=0.28 sys=0.00, real=0.25 secs]

  promotion failed一般是进行Minor GC的时候,发现Survivor空间不够,所以需要移动新生代的对象到老年代,然而,有些时候尽管老年代有足够的空间,但是由于CMS采用标记清除算法会产生很多碎片,因此,这些碎片无法完成大对象向老年代转移,因此会触发一次Full GC。

  这个问题的直接影响就是它会导致提前进行CMS Full GC, 尽管这个时候CMS的老年代并没有填满,只不过有过多的碎片而已,但是Full GC导致的stop-the-wold是难以接受的。

  解决这个问题的办法就是可以让CMS在进行一定次数的Full GC(标记清除)的时候进行一次标记整理算法:

-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5

  也就是CMS在进行5次Full GC(标记清除)之后进行一次标记整理算法,从而可以控制老年带的碎片在一定的数量以内,甚至可以配置CMS在每次Full GC的时候都进行内存的整理。

  另外,有些应用存在比较大的对象朝生夕灭,这些对象在Survivor空间无法容纳,因此,会提早进入老年代,老年代如果有碎片,也会产生promotion failed, 因此我们应该控制这样的对象在新生代,然后在下次Minor GC的时候就被回收掉,这样避免了过早的进行CMS Full GC操作,可以通过增加Survivor空间的大小来解决这个问题。

  2、需要更多的CPU资源。由于使用了并发处理,很多情况下都是GC线程和应用线程并发执行的,这样就需要占用更多的CPU资源,也是牺牲了一定吞吐量的原因。

  CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。

  3、需要更大的堆空间。因为CMS标记阶段应用程序的线程还是执行的,那么就会有堆空间继续分配的问题,为了保障CMS在回收堆空间之前还有空间分配给新加入的对象,必须预留一部分空间。CMS默认在老年代空间使用68%时候启动垃圾回收。可以通过-XX:CMSinitiatingOccupancyFraction=n来设置这个阀值。

  无法处理浮动垃圾(Floating Garbage) 可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。

  由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”。

  也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。 

  要是CMS运行期间预留的内存无法满足程序需要即CMS垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CM SIniti-atingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。

6  G1收集器

  官方文档:https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html

1  G1收集器的特点

  G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:

  1. 空间整合

  在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。

  G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),每个regions都有一个分代的角色:eden、survivor、old。虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合。对每个角色的数量并没有强制的限定,也就是说对每种分代内存的大小,可以动态变化。

  G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。G1 从整体上看采用的是标记-整理法,局部(两个 Region)上看是基于复制算法实现的,两个算法都不会产生内存碎片,收集后提供规整的可用内存,这样有利于程序的长时间运行。

  2. 可预测停顿

  这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

  G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。

  关于停顿时间的设置并不是越短越好。 

  设置的时间越短意味着每次收集的CSet越小【设置的停顿时间越小则收集的垃圾越少,而收集的垃圾越少则意味着收集的region个数越少,而收集的region个数越少意味着由这些被收集的region所构成的CSet就越小】,导致垃圾逐步积累变多,最终不得不退化成Serial GC;

  停顿时间设置的过长,那么会导致每次都会产生长时间的停顿,影响了程序对外的响应时间。

  这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

   

  除了和传统的新老生代,幸存区的空间区别,Region还多了一个H,它代表Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象,这样超大对象就直接分配到了老年代,防止了反复拷贝移动。

  G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。

2  G1收集器概念

  1. 收集集合(Collection Sets或CSet

    一组可被回收的分区(region)的集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自eden空间、survivor空间、或者老年代。

  2. 记忆集合(Remembered Sets or RSets)

    RSet记录了其它Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet既可。

3  G1收集器收集步骤

  收集步骤:

  1、初始标记(Initial Marking) 

    仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。

  而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。

  2、并发标记(Concurrent Marking)

    从GC Roots开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这阶段耗时较长,但是可以与用户程序并发执行。

    当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。

  3、最终标记(Final Marking) 

    对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。

    为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。

  4、筛选回收(Live Data Counting and Evacuation) 

    首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

   

4  G1的适合场景:

  • 服务端多核CPU、JVM内存占用较大的应用。
  • 应用在运行过程中会产生大量内存碎片、需要经常压缩空间。
  • 想要更可控、可预期的GC停顿周期;防止高并发下应用的雪崩现象。

5  G1 GC模式:

  G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的。

  (1)Young GC:选定所有年轻代里的Region。通过控制年轻代的Region个数,既年轻代内存大小,来控制Young GC的时间开销。

  (2)Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。 

  Mixed GC不是Full GC,它只能回收部分老年代的Region,如果Mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(Full GC)来收集整个GC heap。所以本质上,G1是不提供Full GC的。

  G1 GC会在Young GC和Mixed GC之间不断地切换运行,同时定期地做全局并发标记,在实在赶不上对象创建速度的情况下使用Full GC(Serial GC,也就是从G1会回收到备选的方案,一定要尽量避免此情况出现)。

6  global concurrent marking:

  global concurrent marking的执行过程类似于CMS,但是不同的是,在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。

  global concurrent marking的执行过程分为四个步骤。

    初始标记(inital mark,STW):它标记了从GC Root开始直接可达的对象。

    并发标记(Concurrent Marking):这个阶段从GC Root开始对heap中的对象进行标记,标记线程与应用程序线程并发执行,并且收集各个Region的存活对象信息。

    重新标记(Remark,STW):标记那些在并发标记阶段发生变化的对象,将被回收。

    清理(Cleanup):清除空Region(没有存活对象的),加入到free list。

  第一阶段inital mark是共用了Young GC的暂停,这是因为他们可以复用root scan操作,所以可以说global concurrent marking是伴随Young GC而发生的。

  第四阶段Cleanup只是回收了没有存活对象的Region,所以它并不需要STW。

7  G1在运行过程中的主要模式:

  YGC(不同于CMS)

  并发阶段

  混合模式

  Full GC(一般是G1出现问题时发生)

  G1 YGC在Eden充满时触发,在回收之后所有之前属于Eden的区域全部变成空白,既不属于任何一个分区(Eden、Survivor、Old)。

8  Mixed GC:

  什么时候发生Mixed GC?

  由一些参数控制,另外也控制着哪些老年代Region会被选入CSet(收集集合)。

  G1HeapWastePercent:在global concurrent marking结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。

  G1MixedGCLiveThresholdPercent:old generation region中的存活对象的占比,只有在此参数之下,才会被选入CSet。

  G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数。

  G1OldCSetRegionThresholdPercent:一次Mixed GC中能被选入CSet的最多old generation region数量。

 

 

参考

1、jvm优化—— 图解垃圾回收  https://my.oschina.net/u/1859679/blog/1548866

 2、GC算法 垃圾收集器  https://www.cnblogs.com/ityouknow/p/5614961.html

3、通过 jstack 与 jmap 分析一次线上故障  http://www.importnew.com/28916.html

4、性能调优-------(三)1分钟带你入门JVM性能调优  https://blog.csdn.net/wolf_love666/article/details/79787735

5、JVM相关  https://blog.csdn.net/wolf_love666/article/details/85712922

6、JVM命令大全  https://www.cnblogs.com/ityouknow/p/5714703.html

7、JVM调优之---一次GC调优实战  http://www.cnblogs.com/onmyway20xx/p/6626567.html

8、从实际案例聊聊Java应用的GC优化  https://tech.meituan.com/2017/12/29/jvm-optimize.html

 

9、线上服务的 GC 问题排查  https://mp.weixin.qq.com/s/Hs2bo37x7mcx7XTdNQVgZQ

posted on 2019-03-13 21:45  Vagrant。  阅读(628)  评论(0编辑  收藏  举报