zoukankan      html  css  js  c++  java
  • Java虚拟机:对象创建和垃圾回收

    对象实例化

    在实例化对象的过程中,JVM发生了什么化学反应?

    (1)下面从最简单的 Object ref = new Object(); 进行分析,查看字节码如下:

    18: new           #8                  // class java/lang/Object
    21: dup
    22: invokespecial #9                  // Method java/lang/Object."<init>":()V
    25: astore_2
    26: return

    NEW:如果找不到Class对象,则进行类加载。加载成功之后,在堆中分配内存,从Object开始到本类路径上的所有属性值都要分配内存。分配完毕之后,进行零值初始化。分配过程中,注意引用是占内存空间的,它是一个变量,占4个字节。NEW指令完成后,将指向实例对象的引用变量压入虚拟机栈顶。

    DUP:在栈顶复制该引用,这时,栈顶前两个元素是指向堆内实例对象的引用变量。如果<init>方法有参数,还需要把参数压入操作栈。这两个引用变量的目的不同,其中底下的引用用于赋值,或者保存到局部变量表中;位于上层的引用变量作为句柄调用相关方法。

    INVOKESPECIAL:调用对象实例方法,通过栈顶的引用变量调用<init>方法。注意:<clinit>是类初始化时执行的方法,而<init>是对象初始化时执行的方法。

    (2)从执行步骤的角度看:

    第一步,确认类元信息是否存在。当JVM接收到new指令,首先在metaspace中检查需要创建的类元信息是否存在。若不存在,则在双亲委派模式下对.class文件进行加载,如果找不到,就会抛出ClassNotFoundException异常,加载成功后会生成对应的Class对象。

    第二步,分配对象内存。首先计算对象占用空间大小,如果实例成员变量是引用变量,则仅分配引用变量空间(4字节),接着在对内存划分一块内存空间给新对象。分配空间时,需要进行同步操作(如CAS、区域加锁等)。

    第三步,设置默认值。成员变量值都需要设定默认值,即各种不同形式的零值。

    第四步,设置对象头。设置新对象的哈希码、GC信息、锁信息、对象所属的类元信息。

    第五步,执行init方法。初始化成员变量,执行实例代码块、调用类的构造方法,把堆内对象的首地址赋给引用变量。

    垃圾回收

    1.GC Roots

    它是一组活跃的引用。可达性分析算法将从这些引用节点开始往下搜索,搜索所走的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,此对象是不可用的。如下图所示,

    Object5、Object6、Object7到GC Roots是不可达的,所以它们会被判定为可回收对象。

     在Java语言中,可以作为GC ROOTS的对象

      类加载器、Thread、虚拟机栈的本地变量表、statc成员、常量引用,本地方法栈变量等。

      个人理解,GC Roots指的是一个引用,而不是实际的对象实例

    • 虚拟机栈中引用的对象,即局部变量。如一个方法中:Person p = new Person();p就可以作为GC Roots。
    • metaspace中类静态属性引用对象。如 private static Person p = new Person();
    • metaspace中常量引用对象。如 private final static Person p = new Person();
    • 本地方法中的JNI引用的对象。

     2.如何判断生存还是死亡?

    要宣告一个对象真正死亡,需要两次标记:

      第一次,进行可达性分析后,对象没有引用链项链,它会被第一次标记。然后判断是否有必要执行对象的finalize()方法。如果对象的finalize()方法没有被重写或者已经被调用过,则没有必要执行,无需进入第二次标记。判定死亡。

      如果需要执行finalize()方法,则此对象会被放入F-Queue队列中。并稍后由JVM创建的Finalizer线程执行F-Queue队列中对象的finalize()方法。对象可以在finalize()方法中重新与引用链上的任一引用关联完成自救。然后,GC将对F-Queue中的对象进行二次标记,如果此时自救成功,对象将被移出“即将回收的集合”,否则,此对象基本就被回收了。

    三、垃圾回收算法

    (1)标记-清除算法

    该算法从每个GC Roots触发,依次标记有引用关系的对象,最后将没有标记的对象清除。

    不足:

    • 这种算法会带来大量的空间碎片,导致需要分配一个较大连续空间时容易触发FGC。
    • 效率,标记和清除效率都不高。

    (2)复制算法

    将内存空间划分为大小相等两块,每次只使用其中的一块。当一块内存用完了,就把还存活的对象复制到另外一块上面,然后再把使用过的内存空间一次清理掉。

    优点:对整个半区进行内存回收,内存分配时不用考虑内存碎片,只要移动堆顶指针,按顺序分配内存即可,实现简单高效。

    不足:内存缩小为原来的一半。

    新生代中98%的对象都是“朝生夕死”的,所以并不需要1:1的比例划分内存空间,是将内存区分为较大的Eden区和两块较小的Survivor区。当回收时,将Eden区和Survivor存活的对象一次性复制到另外一个survivor空间,最后清理掉原来的两块空间。Hotspot默认的比例是8:1。也就是说,每次新生代中可用的内存空间为整个新生代空间的90%,因此只有10%的空间被浪费。

    (3)标记-整理算法

     复制算法在对象存活率较高时要进行较多的复制操作,效率将会变低。老年代一般不能直接选用这种算法。

    标记-整理算法标记的过程标记-清除一样,但是后续不是直接清理,而是把所有的存活对象都向一端移动,然后直接清理掉边界以外的内存。

    (4)分代收集算法

    它是基于复制算法的。按照对象生命周期的不同划分区域以及采用不同的垃圾回收算法,提高JVM的回收效率。

      jdk1.8的堆内存

      年轻代存活率低,可以使用复制算法,可以减少复制和空间浪费;老年代存活率高,可以采用标记整理算法。

      GC分类

      Miinor GC : 由于新生代的更替非常快,具有朝生夕灭的状态,因此新生代会频繁的触发minorGC

      Full GC :一般对老年代进行full gc。老年代存活比较久,并且占用的空间比较大,一次full gc的开销比较大,所以也JVM也不会经常触发full gc。

      年轻代中,尽可能快速地收集那些生命周期短的对象。

      它分为一个Eden区和两个Survivor区。

      新生代对象一般都是分配到Eden区,然后,当Eden区首次填满了之后,会触发一次Minor GC,此时会把Eden区中存活的对象复制到其中一块Survivor区,此处设置为Survivor0,并将存活对象的生命期+1,并清空Eden区。

      当下次Eden区再次填满了之后,又会触发MinorGC,然后会将Eden区和Survivor0存活的对象复制到Survivor1区,并且清空Eden和Survivor0区。

      survivor0和survivor1就如此交替使用。

      

      一般来说,年轻代对象的生命期阈值为15,如果超过了阈值,就要移到老年代。

      年轻代如何晋升为老年代:

    • 经历一定Minor次数依然存活的对象
    • 回收时Survivor对存不下的对象
    • Eden都放不下的大对象

      

      老年代中,存放生命周期较长的对象。采用标记-清理算法或标记-整理算法进行回收。

      触发full gc条件:

    • 老年代的空间不足
    • 永久代空间不足(jdk8之前)
    • CMS GC时出现promotion failed,concurrent mode failure。
    • Minor GC晋升到老年代的平均大小大于老年代的剩余空间
    • 调用System.gc()

    四、垃圾收集器

    两个收集器之间存在连线,说明它们可以搭配使用。

    停顿时间VS吞吐量

    • 停顿时间:即GC时用户线程停止工作的时间 -XX:MaxGCPauseMillis
    • 吞吐量:应用程序运行时间与垃圾回收时间的比例 -XX:GCTimeRatio

    (1)Serial / Serial Old收集器

    单线程收集器,在工作时只会使用一个CPU或一个线程去完成垃圾收集工作,并且必须暂停其他所有的工作线程,直到收集结束。

    “Stop the world”是由虚拟机后台自动发起的,在用户不可见的情况下把用户正常工作的线程都停止掉,对很多应用来说是可以接受的。

    其中Serial收集器是用于新生代(年轻代)的收集,采用复制算法;Serial Old收集器用于老年代,采用标记整理算法。

    串行组合:Serial + Serial Old

    Serial是JVM Client模式下默认的垃圾收集器

    优点:简单高效,没有线程交互的开销。

    开启选项:

    -XX:+SerialGC

    (2)ParNew收集器

    Serial的多线程版本。它是Server模式下的默认新生代收集器

    ParNew收集器在单线程环境下绝对不会有比Serial收集器更好的效果。它默认开启的线程数等于cpu的个数。

    日志格式

    -XX:+UseParNewGC

    {Heap before GC invocations=0 (full 0):
     par new generation   total 39296K, used 34944K [0x0000000081000000, 0x0000000083aa0000, 0x00000000ab550000)
      eden space 34944K, 100% used [0x0000000081000000, 0x0000000083220000, 0x0000000083220000)
      from space 4352K,   0% used [0x0000000083220000, 0x0000000083220000, 0x0000000083660000)
      to   space 4352K,   0% used [0x0000000083660000, 0x0000000083660000, 0x0000000083aa0000)
     tenured generation   total 87424K, used 0K [0x00000000ab550000, 0x00000000b0ab0000, 0x0000000100000000)
       the space 87424K,   0% used [0x00000000ab550000, 0x00000000ab550000, 0x00000000ab550200, 0x00000000b0ab0000)
     Metaspace       used 11682K, capacity 12020K, committed 12160K, reserved 1060864K
      class space    used 1464K, capacity 1576K, committed 1664K, reserved 1048576K
    // 空间分配失败,触发young gc
    0.735: [GC (Allocation Failure) 0.735: [ParNew
    Desired survivor size 2228224 bytes, new threshold 1 (max 15)
    - age   1:    4403560 bytes,    4403560 total
    // 空间变化
    : 34944K->4352K(39296K), 0.0037773 secs] 34944K->4834K(126720K), 0.0038913 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap after GC invocations=1 (full 0):
     par new generation   total 39296K, used 4352K [0x0000000081000000, 0x0000000083aa0000, 0x00000000ab550000)
      eden space 34944K,   0% used [0x0000000081000000, 0x0000000081000000, 0x0000000083220000)
      from space 4352K, 100% used [0x0000000083660000, 0x0000000083aa0000, 0x0000000083aa0000)
      to   space 4352K,   0% used [0x0000000083220000, 0x0000000083220000, 0x0000000083660000)
     tenured generation   total 87424K, used 482K [0x00000000ab550000, 0x00000000b0ab0000, 0x0000000100000000)
       the space 87424K,   0% used [0x00000000ab550000, 0x00000000ab5c88d8, 0x00000000ab5c8a00, 0x00000000b0ab0000)
     Metaspace       used 11682K, capacity 12020K, committed 12160K, reserved 1060864K
      class space    used 1464K, capacity 1576K, committed 1664K, reserved 1048576K
    }

     常用参数

    • -XX:+ParallelGCThreads=<N> :多少个GC线程,当CPU<8,取N=nCPU,当N>8,取N=5/8

    (3)Parallel Scavenge收集器

    它是一个新生代的收集器,也是采用复制算法,又是采用多线程,它与ParNew不同的是,PS收集器的目标的是达到一个可控制的吞吐量,吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)

    它无法与CMS收集器配合使用。

     参数:

    • -XX:MaxGCPauseMills:最大垃圾回收停顿时间。此参数并不是越小越好,GC停顿时间缩短是以牺牲吞吐量和新生代空间换取的
    • -XX:GCTimeRatio:0-100,指垃圾收集时间占总时间的比率,是吞吐量的倒数。
    • -XX:+UseAdaptiveSizePolicy:开启此参数,虚拟机会根据当前系统的运行情况来收集性能监控信息,动态调整SurvivorRatio等参数,以提供合适的停顿时间或最大吞吐量

     (4)Parallel Old收集器。

    它是Parallel Scavenge的老年代版本,在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel scavenge + Parallel Old的组合。

     

    (5)CMS(Concurrent Mark Sweep)收集器

    老年代收集器,基于标记清除算法,它是一种以获取最短回收停顿时间为目标的收集器。它的运作过程包括

    • 1.初始标记(Initial Mark):标记GC roots 能够直接关联的对象,发生STW,此期间暂停实际那相对Minor GC得暂停时间是比较短得的
    • 2.并发标记(Concurrent Mark):进行GC roots Tracing,与用户线程并发进行,遍历与1阶段可达的对象,这个阶段被标记的是活跃的对象。
    • 3.重新标记(Remark):修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
    • 4.并发清除(Concurrent Sweep):回收在标记阶段被鉴定为不可达的对象,活跃的对象是不会被移动的(标记清除算法)
    • 5.重新设置:清理数据结构,为下一次并发收集做准备。

     其中,初始标记重新标记依然会触发STW(Stop the word),暂停所有工作线程。而并发标记并发清除是可以跟用户线程并行的。

     

    下面看CMS的回收步骤

    1.CMS收集器可以跟年轻代的Serial/ParNew收集器结合使用。 

    2.发生两次stop the world事件:初始标记和重新标记。当年老代达到特定的占用比例时,CMS开始执行。 

      

     (1)初始标记是一个短暂暂停的、可达对象被标记的阶段。(2)并发标记寻找活跃对象的GC Roots Tracing,标记可达对象。最后,在(3)重新标记阶段,寻找在之前并发标记阶段中丢失的对象。

    3.并发清除

    在之前阶段没有被标记的对象会被就地释放。不进行压缩操作。注意:未被标记的对象等于死亡对象

    4.清除之后

    清除阶段之后,你可以看到大量内存被释放。你还可以注意到由于采用标记清除算法,会产生大量碎片,而且没有进行压缩操作。

    为了解决这个问题,CMS可以通过配置以下参数

    -XX:+UseCMSCompactAtFullCollcetion    // 强制JVM在FGC之后堆老年代进行压缩,执行一次空间碎片整理,但是整理阶段会引发STW
    -XX:+CMSFullGCsBeforeCompaction=n     // 解决上面的问题,在执行n次FGC之后才执行压缩

     5.日志格式

    -XX:+UseConcMarkSweepGC

    // 初始标记(STW,标记直接关联的GC ROOT)
    2.080: [GC (CMS Initial Mark) [1 CMS-initial-mark: 8808K(87424K)] 13994K(126720K), 0.0008197 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    2.081: [CMS-concurrent-mark-start]
    // 并发标记(指的是与用户线程并发)
    2.086: [CMS-concurrent-mark: 0.005/0.005 secs] [Times: user=0.08 sys=0.00, real=0.00 secs] 
    // 并发预清理
    2.086: [CMS-concurrent-preclean-start]
    2.087: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    // 重新标记(GC Roots Tracing)
    2.087: [GC (CMS Final Remark) [YG occupancy: 6292 K (39296 K)]2.087: [Rescan (parallel) , 0.0008588 secs]2.088: [weak refs processing, 0.0000243 secs]2.088: 
      [class unloading, 0.0016033 secs]2.089: [scrub symbol table, 0.0019613 secs]2.091: [scrub string table, 0.0002359 secs][1 CMS-remark: 8808K(87424K)] 15100K(126720K), 0.0049341 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
    // 并发清除
    2.092: [CMS-concurrent-sweep-start]
    2.093: [CMS-concurrent-sweep: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    2.094: [CMS-concurrent-reset-start]
    2.102: [CMS-concurrent-reset: 0.008/0.008 secs] [Times: user=0.03 sys=0.01, real=0.01 secs]

    CMS的缺点:

    • 对CPU的资源非常敏感,并发阶段,会占用一部分线程而导致程序变慢,总吞吐量降低。
    • 无法处理浮动垃圾。CMS并发清理阶段会有用户线程存在,这部分线程仍然会产生垃圾,而当次的CMS回收是不能清理的。
    • 基于标记清除算法,容易产生碎片。

    参数:

    • -XX:ConcGCThreads:并发GC线程数
    • -XX:CMSInitiatingOccupancyFranction:老年代触发CMS回收空间占比阈值。JDK6默认为92%。
    • -XX:+UseCMSCompactAtFullCollection:强制FullGC完成后要对老年代内存碎片压缩合并。
    • -XX:CMSFullGCsBeforeCompaction:用于设置执行多少次不压缩的FullGC后,JVM再进行老年代的空间碎片整理。
    • -XX:+CMSScavengeBeforeRemark:在FGC之前先做YGC
    • -XX:+CMSClassUnloadingEnabled:启动回收Metaspace区

    6、G1(Garbage-First Garbage Collector)

    之前介绍的几组垃圾收集器组合,都有几个共同点:

    • 年轻代、老年代是独立且连续的内存块;
    • 年轻代收集使用单eden、双survivor进行复制算法;
    • 老年代收集必须扫描整个老年代区域;
    • 都是以尽可能少而快地执行GC为设计原则。

    G1垃圾收集器也是以关注延迟为目标、服务器端应用的垃圾收集器,被HotSpot团队寄予取代CMS的使命,也是一个非常具有调优潜力的垃圾收集器。虽然G1也有类似CMS的收集动作:初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制,但单纯地以类似前三种的过程描述显得并不是很妥当。事实上,G1收集与以上三组收集器有很大不同:

    • G1的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大
    • G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩);
    • G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换
    • G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。

    (1)G1收集器的堆空间分配

    分区

    G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区

    卡片

    在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找堆分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。

    G1同样可以通过-Xms/-Xmx来指定堆空间大小。当发生年轻代收集或混合收集时,通过计算GC与应用的耗费时间比,自动调整堆空间大小。如果GC频率太高,则通过增加堆尺寸,来减少GC频率,相应地GC占用的时间也随之降低;目标参数-XX:GCTimeRatio即为GC与应用的耗费时间比,G1默认为9,而CMS默认为99,因为CMS的设计原则是耗费在GC上的时间尽可能的少。另外,当空间不足,如对象空间分配或转移失败时,G1会首先尝试增加堆空间,如果扩容失败,则发起担保的Full GC。Full GC后,堆尺寸计算结果也会调整堆空间。

    分代模型

    分代

    分代垃圾收集可以将关注点集中在最近被分配的对象上,而无需整堆扫描,避免长命对象的拷贝,同时独立收集有助于降低响应时间。虽然分区使得内存分配不再要求紧凑的内存空间,但G1依然使用了分代的思想。与其他垃圾收集器类似,G1将内存在逻辑上划分为年轻代和老年代,其中年轻代又划分为Eden空间和Survivor空间。但年轻代空间并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲分区加入到年轻代空间

    整个年轻代内存会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间-XX:G1MaxNewSizePercent(默认60%)之间动态变化,且由参数目标暂停时间-XX:MaxGCPauseMillis(默认200ms)、需要扩缩容的大小以及分区的已记忆集合(RSet)计算得到。当然,G1依然可以设置固定的年轻代大小(参数-XX:NewRatio、-Xmn),但同时暂停目标将失去意义。

    本地分配缓冲

    值得注意的是,由于分区的思想,每个线程均可以"认领"某个分区用于线程本地的内存分配,而不需要顾及分区是否连续。因此,每个应用线程和GC线程都会独立的使用分区,进而减少同步时间,提升GC效率,这个分区称为本地分配缓冲区(Lab)。

    其中,应用线程可以独占一个本地缓冲区(TLAB)来创建的对象,而大部分都会落入Eden区域(巨型对象或分配失败除外),因此TLAB的分区属于Eden空间;而每次垃圾收集时,每个GC线程同样可以独占一个本地缓冲区(GCLAB)用来转移对象,每次回收会将对象复制到Suvivor空间或老年代空间;对于从Eden/Survivor空间晋升(Promotion)到Survivor/老年代空间的对象,同样有GC独占的本地缓冲区进行操作,该部分称为晋升本地缓冲区(PLAB)。

    分区模型

    G1对内存的使用以分区(Region)为单位,而对对象的分配则以卡片(Card)为单位。

    • 巨型对象

      一个大小达到甚至超过分区大小一半的对象称为巨型对象(Humongous Object)。当线程为巨型分配空间时,不能简单在TLAB进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region)。G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收

      巨型对象会独占一个、或多个连续分区,其中第一个分区被标记为开始巨型(StartsHumongous),相邻连续分区被标记为连续巨型(ContinuesHumongous)。由于无法享受Lab带来的优化,并且确定一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高,如果可以,应用程序应避免生成巨型对象。

    • 已记忆集合(Remember Set 【RSet】)

      在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况

      事实上,并非所有的引用都需要记录在RSet中,如果一个分区确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。那么引用源自本分区的对象,当然不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在RSet中记录。最后只有老年代的分区可能会有RSet记录,这些分区称为拥有RSet分区(an RSet’s owning region)

    • Per Region Table(PRT)

      RSet在内部使用Per Region Table(PRT)记录分区的引用情况。由于RSet的记录要占用分区的空间,如果一个分区非常"受欢迎",那么RSet占用的空间会上升,从而降低分区的可用空间。G1应对这个问题采用了改变RSet的密度的方式,在PRT中将会以三种模式记录引用:

        1)稀少:直接记录引用对象的卡片索引  2)细粒度:记录引用对象的分区索引 3)粗粒度:只记录引用情况,每个分区对应一个比特位

      由上可知,粗粒度的PRT只是记录了引用数量,需要通过整堆扫描才能找出所有引用,因此扫描速度也是最慢的。

    收集集合(CSet)

    收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。

    候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。

    由上述可知,G1的收集都是根据CSet进行操作的,年轻代收集与混合收集没有明显的不同,最大的区别在于两种收集的触发条件。

    • 年轻代收集集合 CSet of Young Collection

      应用线程不断活动后,年轻代空间会被逐渐填满。当JVM分配对象到Eden区域失败(Eden区已满)时,便会触发一次STW式的年轻代收集。在年轻代收集中,Eden分区存活的对象将被拷贝到Survivor分区;原有Survivor分区存活的对象,将根据任期阈值(tenuring threshold)分别晋升到PLAB中,新的survivor分区和老年代分区。而原有的年轻代分区将被整体回收掉。

      同时,年轻代收集还负责维护对象的年龄(存活次数),辅助判断老化(tenuring)对象晋升的时候是到Survivor分区还是到老年代分区。年轻代收集首先先将晋升对象尺寸总和、对象年龄信息维护到年龄表中,再根据年龄表、Survivor尺寸、Survivor填充容量-XX:TargetSurvivorRatio(默认50%)、最大任期阈值-XX:MaxTenuringThreshold(默认15),计算出一个恰当的任期阈值,凡是超过任期阈值的对象都会被晋升到老年代。

    • 混合收集集合 CSet of Mixed Collection

      年轻代收集不断活动后,老年代的空间也会被逐渐填充。当老年代占用空间超过整堆比IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会启动一次混合垃圾收集周期。为了满足暂停目标,G1可能不能一口气将所有的候选分区收集掉,因此G1可能会产生连续多次的混合收集与应用线程交替执行,每次STW的混合收集与年轻代收集过程相类似。

      为了确定包含到年轻代收集集合CSet的老年代分区,JVM通过参数混合周期的最大总次数-XX:G1MixedGCCountTarget(默认8)、堆废物百分比-XX:G1HeapWastePercent(默认5%)。通过候选老年代分区总数与混合周期最大总次数,确定每次包含到CSet的最小分区数量;根据堆废物百分比,当收集达到参数时,不再启动新的混合收集。而每次添加到CSet的分区,则通过计算得到的GC效率进行安排。

    (2)G1的活动周期

    1)RSet的维护

    由于不能整堆扫描,又需要计算分区确切的活跃度,因此,G1需要一个增量式的完全标记并发算法,通过维护RSet,得到准确的分区引用信息。在G1中,RSet的维护主要来源两个方面:写栅栏(Write Barrier)和并发优化线程(Concurrence Refinement Threads)

    2)栅栏

    我们首先介绍一下栅栏(Barrier)的概念。栅栏是指在原生代码片段中,当某些语句被执行时,栅栏代码也会被执行。而G1主要在赋值语句中,使用写前栅栏(Pre-Write Barrrier)和写后栅栏(Post-Write Barrrier)。事实上,写栅栏的指令序列开销非常昂贵,应用吞吐量也会根据栅栏复杂度而降低。

    • 写前栅栏 Pre-Write Barrrier

      即将执行一段赋值语句时,等式左侧对象将修改引用到另一个对象,那么等式左侧对象原先引用的对象所在分区将因此丧失一个引用,那么JVM就需要在赋值语句生效之前,记录丧失引用的对象。JVM并不会立即维护RSet,而是通过批量处理,在将来RSet更新(见SATB)。

    • 写后栅栏 Post-Write Barrrier

      当执行一段赋值语句后,等式右侧对象获取了左侧对象的引用,那么等式右侧对象所在分区的RSet也应该得到更新。同样为了降低开销,写后栅栏发生后,RSet也不会立即更新,同样只是记录此次更新日志,在将来批量处理(见Concurrence Refinement Threads)。

    3)起始快照算法 Snapshot at the beginning (SATB)

    Taiichi Tuasa贡献的增量式完全并发标记算法起始快照算法(SATB),主要针对标记-清除垃圾收集器的并发标记阶段,非常适合G1的分区块的堆结构,同时解决了CMS的主要烦恼:重新标记暂停时间长带来的潜在风险。

    SATB会创建一个对象图,相当于堆的逻辑快照,从而确保并发标记阶段所有的垃圾对象都能通过快照被鉴别出来。当赋值语句发生时,应用将会改变了它的对象图,那么JVM需要记录被覆盖的对象。因此写前栅栏会在引用变更前,将值记录在SATB日志或缓冲区中。每个线程都会独占一个SATB缓冲区,初始有256条记录空间。当空间用尽时,线程会分配新的SATB缓冲区继续使用,而原有的缓冲去则加入全局列表中。最终在并发标记阶段,并发标记线程(Concurrent Marking Threads)在标记的同时,还会定期检查和处理全局缓冲区列表的记录,然后根据标记位图分片的标记位,扫描引用字段来更新RSet。此过程又称为并发标记/SATB写前栅栏。

    4)并发标记周期 Concurrent Marking Cycle

    并发标记周期是G1中非常重要的阶段,这个阶段将会为混合收集周期识别垃圾最多的老年代分区整个周期完成根标记、识别所有(可能)存活对象,并计算每个分区的活跃度,从而确定GC效率等级

    当达到IHOP阈值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默认45%)时,便会触发并发标记周期。整个并发标记周期将由初始标记(Initial Mark)、根分区扫描(Root Region Scanning)、并发标记(Concurrent Marking)、重新标记(Remark)、清除(Cleanup)几个阶段组成。其中,初始标记(随年轻代收集一起活动)、重新标记、清除是STW的,而并发标记如果来不及标记存活对象,则可能在并发标记过程中,G1又触发了几次年轻代收集。

    5)并发标记线程 Concurrent Marking Threads

    要标记存活的对象,每个分区都需要创建位图(Bitmap)信息来存储标记数据,来确定标记周期内被分配的对象。G1采用了两个位图Previous Bitmap、Next Bitmap,来存储标记数据,Previous位图存储上次的标记数据,Next位图在标记周期内不断变化更新,同时Previous位图的标记数据也越来越过时,当标记周期结束后Next位图便替换Previous位图,成为上次标记的位图。同时,每个分区通过顶部开始标记(TAMS),来记录已标记过的内存范围。同样的,G1使用了两个顶部开始标记Previous TAMS(PTAMS)、Next TAMS(NTAMS),记录已标记的范围。

    在并发标记阶段,G1会根据参数-XX:ConcGCThreads(默认GC线程数的1/4,即-XX:ParallelGCThreads/4),分配并发标记线程(Concurrent Marking Threads),进行标记活动。每个并发线程一次只扫描一个分区,并通过"手指"指针的方式优化获取分区。并发标记线程是爆发式的,在给定的时间段拼命干活,然后休息一段时间,再拼命干活。

    每个并发标记周期,在初始标记STW的最后,G1会分配一个空的Next位图和一个指向分区顶部(Top)的NTAMS标记。Previous位图记录的上次标记数据,上次的标记位置,即PTAMS,在PTAMS与分区底部(Bottom)的范围内,所有的存活对象都已被标记。那么,在PTAMS与Top之间的对象都将是隐式存活(Implicitly Live)对象。在并发标记阶段,Next位图吸收了Previous位图的标记数据,同时每个分区都会有新的对象分配,则Top与NTAMS分离,前往更高的地址空间。在并发标记的一次标记中,并发标记线程将找出NTAMS与PTAMS之间的所有存活对象,将标记数据存储在Next位图中。同时,在NTAMS与Top之间的对象即成为已标记对象。如此不断地更新Next位图信息,并在清除阶段与Previous位图交换角色。

    6)标记过程(Concurrent Marking)

    • 初始标记(Initial mark)(STW)

      初始标记(Initial Mark)负责标记所有能被直接可达的根对象(原生栈对象、全局对象、JNI对象),根是对象图的起点,因此初始标记需要将Mutator线程(Java应用线程)暂停掉,也就是需要一个STW的时间段。事实上,当达到InitiatingHeapOccupancyPercent阈值时,G1并不会立即发起并发标记周期,而是等待下一次年轻代收集,利用年轻代收集的STW时间段,完成初始标记,这种方式称为借道(Piggybacking)。在初始标记暂停中,分区的NTAMS都被设置到分区顶部Top,初始标记是并发执行,直到所有的分区处理完。

    • 根分区扫描 Root Region Scanning

      在初始标记暂停结束后,年轻代收集也完成的对象复制到Survivor的工作(通过借道),应用线程开始活跃起来。此时为了保证标记算法的正确性,所有新复制到Survivor分区的对象,都需要被扫描并标记成根,这个过程称为根分区扫描(Root Region Scanning),同时扫描的Suvivor分区也被称为根分区(Root Region)。根分区扫描必须在下一次年轻代垃圾收集启动前完成(并发标记的过程中,可能会被若干次年轻代垃圾收集打断),因为每次GC会产生新的存活对象集合。

    • 并发标记 Concurrent Marking(与用户线程并发)

      和应用线程并发执行,并发标记线程在并发标记阶段启动,由参数-XX:ConcGCThreads(默认GC线程数的1/4,即-XX:ParallelGCThreads/4)控制启动数量,每个线程每次只扫描一个分区,从而标记出存活对象图。在这一阶段会处理Previous/Next标记位图,扫描标记对象的引用字段同时,并发标记线程还会定期检查和处理STAB全局缓冲区列表的记录,更新对象引用信息。参数-XX:+ClassUnloadingWithConcurrentMark会开启一个优化,如果一个类不可达(不是对象不可达),则在重新标记阶段,这个类就会被直接卸载。所有的标记任务必须在堆满前就完成扫描,如果并发标记耗时很长,那么有可能在并发标记过程中,又经历了几次年轻代收集。如果堆满前没有完成标记任务,则会触发担保机制,经历一次长时间的串行Full GC。

    • 重新标记 Remark(STW)

      重新标记(Remark)是最后一个标记阶段。在该阶段中,G1需要一个暂停的时间,去处理剩下的SATB日志缓冲区和所有更新,找出所有未被访问的存活对象,同时安全完成存活数据计算。这个阶段也是并行执行的,通过参数-XX:ParallelGCThread可设置GC暂停时可用的GC线程数。同时,引用处理也是重新标记阶段的一部分,所有重度使用引用对象(弱引用、软引用、虚引用、最终引用)的应用都会在引用处理上产生开销。

    • 清除 Cleanup(STW)

      紧挨着重新标记阶段的清除(Clean)阶段也是STW的。Previous/Next标记位图、以及PTAMS/NTAMS,都会在清除阶段交换角色。清除阶段主要执行以下操作:

    1. RSet梳理,启发式算法会根据活跃度和RSet尺寸对分区定义不同等级,同时RSet数理也有助于发现无用的引用。参数-XX:+PrintAdaptiveSizePolicy可以开启打印启发式算法决策细节;
    2. 整理堆分区,为混合收集周期识别回收收益高(基于释放空间和暂停目标)的老年代分区集合,放入CSet
    3. 识别所有空闲分区,即发现无存活对象的分区。该分区可在清除阶段直接回收,无需等待下次收集周期。

      所以,我们可以看到,清除阶段,除了第三点,前两点并不是真正的清除垃圾对象,而是做好清除前的整理和计算。

    下面是一次Concurrent Marking的日志

    9.041: [GC pause (Metadata GC Threshold) (young) (initial-mark), 0.0080212 secs]
       [Parallel Time: 3.3 ms, GC Workers: 8]
         ....
       [Code Root Fixup: 0.0 ms]
       [Code Root Purge: 0.0 ms]
       [Clear CT: 0.5 ms]
       [Other: 4.2 ms]
          [Choose CSet: 0.0 ms]
          [Ref Proc: 3.7 ms]
          [Ref Enq: 0.0 ms]
          [Redirty Cards: 0.4 ms]
          [Humongous Register: 0.0 ms]
          [Humongous Reclaim: 0.0 ms]
          [Free CSet: 0.0 ms]
       [Eden: 62.0M(71.0M)->0.0B(66.0M) Survivors: 5120.0K->10.0M Heap: 70.4M(128.0M)->15.5M(128.0M)]
     [Times: user=0.00 sys=0.00, real=0.01 secs] 
    9.049: [GC concurrent-root-region-scan-start]
    9.053: [GC concurrent-root-region-scan-end, 0.0036653 secs]
    9.053: [GC concurrent-mark-start]
    9.057: [GC concurrent-mark-end, 0.0038701 secs]
    9.057: [GC remark 9.057: [Finalize Marking, 0.0004024 secs] 9.058: [GC ref-proc, 0.0002925 secs] 9.058: [Unloading, 0.0020772 secs], 0.0033580 secs]
     [Times: user=0.00 sys=0.00, real=0.00 secs] 
    9.061: [GC cleanup 16M->15M(128M), 0.0010063 secs]
     [Times: user=0.00 sys=0.00, real=0.00 secs] 
    9.062: [GC concurrent-cleanup-start]
    9.062: [GC concurrent-cleanup-end, 0.0000084 secs]

    (3)年轻代收集/混合收集周期

    年轻代收集和混合收集周期,是G1回收空间的主要活动。

    年轻代收集触发时机:当应用运行开始时,堆内存可用空间还比较大,只会在年轻代满时,触发年轻代收集;

    混合收集触发时机:随着老年代内存增长,当到达IHOP阈值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默认45%)时,或者-XX:G1HeapWastePercent(垃圾占比,默认5)达到阈值,G1开始着手准备收集老年代空间。首先经历并发标记周期,识别出高收益的老年代分区,前文已述。但随后G1并不会马上开始一次混合收集,而是让应用线程先运行一段时间,等待触发一次年轻代收集。在这次STW中,G1将保准整理混合收集周期。接着再次让应用线程运行,当接下来的几次年轻代收集时,将会有老年代分区加入到CSet中,即触发混合收集,这些连续多次的混合收集称为混合收集周期(Mixed Collection Cycle)。

    JVM可以通过参数-XX:ParallelGCThreads进行指定GC工作的线程数量。参数-XX:ParallelGCThreads默认值并不是固定的,而是根据当前的CPU资源进行计算。如果用户没有指定,且CPU小于等于8,则默认与CPU核数相等;若CPU大于8,则默认JVM会经过计算得到一个小于CPU核数的线程数;当然也可以人工指定与CPU核数相等。

    1)年轻代收集

    并行活动

    • 外部根分区扫描 Ext Root Scanning:此活动对堆外的根(JVM系统目录、VM数据结构、JNI线程句柄、硬件寄存器、全局变量、线程对栈根)进行扫描,发现那些没有加入到暂停收集集合CSet中的对象。如果系统目录(单根)拥有大量加载的类,最终可能其他并行活动结束后,该活动依然没有结束而带来的等待时间。
    • 更新已记忆集合 Update RS:并发优化线程会对脏卡片的分区进行扫描更新日志缓冲区来更新RSet,但只会处理全局缓冲列表。作为补充,所有被记录但是还没有被优化线程处理的剩余缓冲区,会在该阶段处理,变成已处理缓冲区(Processed Buffers)。为了限制花在更新RSet的时间,可以设置暂停占用百分比-XX:G1RSetUpdatingPauseTimePercent(默认10%,即-XX:MaxGCPauseMills/10)。值得注意的是,如果更新日志缓冲区更新的任务不降低,单纯地减少RSet的更新时间,会导致暂停中被处理的缓冲区减少,将日志缓冲区更新工作推到并发优化线程上,从而增加对Java应用线程资源的争夺。
    • RSet扫描 Scan RS:在收集当前CSet之前,考虑到分区外的引用,必须扫描CSet分区的RSet。如果RSet发生粗化,则会增加RSet的扫描时间。开启诊断模式-XX:UnlockDiagnosticVMOptions后,通过参数-XX:+G1SummarizeRSetStats可以确定并发优化线程是否能够及时处理更新日志缓冲区,并提供更多的信息,来帮助为RSet粗化总数提供窗口。参数-XX:G1SummarizeRSetStatsPeriod=n可设置RSet的统计周期,即经历多少此GC后进行一次统计
    • 代码根扫描 Code Root Scanning:对代码根集合进行扫描,扫描JVM编译后代码Native Method的引用信息(nmethod扫描),进行RSet扫描。事实上,只有CSet分区中的RSet有强代码根时,才会做nmethod扫描,查找对CSet的引用。
    • 转移和回收 Object Copy:通过选定的CSet以及CSet分区完整的引用集,将执行暂停时间的主要部分:CSet分区存活对象的转移、CSet分区空间的回收。通过工作窃取机制来负载均衡地选定复制对象的线程,并且复制和扫描对象被转移的存活对象将拷贝到每个GC线程分配缓冲区GCLAB。G1会通过计算,预测分区复制所花费的时间,从而调整年轻代的尺寸。
    • 终止 Termination:完成上述任务后,如果任务队列已空,则工作线程会发起终止要求。如果还有其他线程继续工作,空闲的线程会通过工作窃取机制尝试帮助其他线程处理。而单独执行根分区扫描的线程,如果任务过重,最终会晚于终止。
    • GC外部的并行活动 GC Worker Other:该部分并非GC的活动,而是JVM的活动导致占用了GC暂停时间(例如JNI编译)。

    串行活动

    • 代码根更新 Code Root Fixup:根据转移对象更新代码根。
    • 代码根清理 Code Root Purge:清理代码根集合表。
    • 清除全局卡片标记 Clear CT:在任意收集周期会扫描CSet与RSet记录的PRT,扫描时会在全局卡片表中进行标记,防止重复扫描。在收集周期的最后将会清除全局卡片表中的已扫描标志。
    • 选择下次收集集合 Choose CSet:该部分主要用于并发标记周期后的年轻代收集、以及混合收集中,在这些收集过程中,由于有老年代候选分区的加入,往往需要对下次收集的范围做出界定;但单纯的年轻代收集中,所有收集的分区都会被收集,不存在选择。
    • 引用处理 Ref Proc:主要针对软引用、弱引用、虚引用、final引用、JNI引用。当Ref Proc占用时间过多时,可选择使用参数-XX:ParallelRefProcEnabled激活多线程引用处理。G1希望应用能小心使用软引用,因为软引用会一直占据内存空间直到空间耗尽时被Full GC回收掉;即使未发生Full GC,软引用对内存的占用,也会导致GC次数的增加。
    • 引用排队 Ref Enq:此项活动可能会导致RSet的更新,此时会通过记录日志,将关联的卡片标记为脏卡片。
    • 卡片重新脏化 Redirty Cards:重新脏化卡片。
    • 回收空闲巨型分区 Humongous Reclaim:G1做了一个优化:通过查看所有根对象以及年轻代分区的RSet,如果确定RSet中巨型对象没有任何引用,则说明G1发现了一个不可达的巨型对象,该对象分区会被回收。
    • 释放分区 Free CSet:回收CSet分区的所有空间,并加入到空闲分区中。
    • 其他活动 Other:GC中可能还会经历其他耗时很小的活动,如修复JNI句柄等。

    下面是G1一次YGC的日志

    21.841: [GC pause (G1 Evacuation Pause) (young), 0.0066243 secs]
    // 并行活动 [Parallel Time: 4.6 ms, GC Workers: 8] [GC Worker Start (ms): Min: 21840.7, Avg: 21840.8, Max: 21840.9, Diff: 0.1] [Ext Root Scanning (ms): Min: 0.2, Avg: 0.4, Max: 0.9, Diff: 0.7, Sum: 3.2] [Update RS (ms): Min: 0.0, Avg: 0.4, Max: 0.7, Diff: 0.7, Sum: 3.4] [Processed Buffers: Min: 1, Avg: 2.3, Max: 5, Diff: 4, Sum: 18] [Scan RS (ms): Min: 0.0, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum: 1.6] [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.3] [Object Copy (ms): Min: 3.1, Avg: 3.2, Max: 3.2, Diff: 0.1, Sum: 25.4] [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Termination Attempts: Min: 1, Avg: 1.1, Max: 2, Diff: 1, Sum: 9] [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] [GC Worker Total (ms): Min: 4.2, Avg: 4.3, Max: 4.3, Diff: 0.1, Sum: 34.0] [GC Worker End (ms): Min: 21845.1, Avg: 21845.1, Max: 21845.1, Diff: 0.0]
    // 串行活动 [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] [Clear CT: 0.6 ms] [Other: 1.5 ms] [Choose CSet: 0.0 ms] [Ref Proc: 1.0 ms] [Ref Enq: 0.0 ms] [Redirty Cards: 0.3 ms] [Humongous Register: 0.0 ms] [Humongous Reclaim: 0.0 ms] [Free CSet: 0.1 ms] [Eden: 67.0M(67.0M)->0.0B(66.0M) Survivors: 9216.0K->10.0M Heap: 97.2M(128.0M)->31.2M(128.0M)] [Times: user=0.00 sys=0.00, real=0.01 secs]

    2)并发标记周期后的年轻代收集 Young Collection Following Concurrent Marking Cycle

     当G1发起并发标记周期之后,并不会马上开始混合收集。G1会先等待下一次年轻代收集,然后在该收集阶段中,确定下次混合收集的CSet(Choose CSet)。

    3)混合收集周期 Mixed Collection Cycle

    单次的混合收集与年轻代收集并无二致。根据暂停目标,老年代的分区可能不能一次暂停收集中被处理完,G1会发起连续多次的混合收集,称为混合收集周期(Mixed Collection Cycle)。G1会计算每次加入到CSet中的分区数量、混合收集进行次数,并且在上次的年轻代收集、以及接下来的混合收集中,G1会确定下次加入CSet的分区集(Choose CSet),并且确定是否结束混合收集周期。

    10624.311: [GC pause (G1 Evacuation Pause) (mixed), 0.0054322 secs]
       [Parallel Time: 4.6 ms, GC Workers: 8]
          [GC Worker Start (ms): Min: 10624311.6, Avg: 10624311.6, Max: 10624311.8, Diff: 0.2]
          [Ext Root Scanning (ms): Min: 0.1, Avg: 0.4, Max: 1.1, Diff: 1.0, Sum: 3.4]
          [Update RS (ms): Min: 0.2, Avg: 0.9, Max: 1.3, Diff: 1.1, Sum: 7.2]
             [Processed Buffers: Min: 1, Avg: 1.3, Max: 2, Diff: 1, Sum: 10]
          [Scan RS (ms): Min: 0.5, Avg: 0.6, Max: 0.8, Diff: 0.3, Sum: 5.0]
          [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
          [Object Copy (ms): Min: 2.4, Avg: 2.4, Max: 2.4, Diff: 0.1, Sum: 19.4]
          [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
             [Termination Attempts: Min: 1, Avg: 4.9, Max: 9, Diff: 8, Sum: 39]
          [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
          [GC Worker Total (ms): Min: 4.3, Avg: 4.4, Max: 4.5, Diff: 0.2, Sum: 35.2]
          [GC Worker End (ms): Min: 10624316.0, Avg: 10624316.0, Max: 10624316.0, Diff: 0.0]
       [Code Root Fixup: 0.0 ms]
       [Code Root Purge: 0.0 ms]
       [Clear CT: 0.2 ms]
       [Other: 0.6 ms]
          [Choose CSet: 0.0 ms]
          [Ref Proc: 0.3 ms]
          [Ref Enq: 0.0 ms]
          [Redirty Cards: 0.2 ms]
          [Humongous Register: 0.0 ms]
          [Humongous Reclaim: 0.0 ms]
          [Free CSet: 0.0 ms]
       [Eden: 2048.0K(2048.0K)->0.0B(5120.0K) Survivors: 5120.0K->1024.0K Heap: 82.8M(128.0M)->72.8M(128.0M)]
     [Times: user=0.00 sys=0.00, real=0.01 secs] 

    4)转移失败的担保机制 Full GC

    转移失败(Evacuation Failure)是指当G1无法在堆空间中申请新的分区时,G1便会触发担保机制,执行一次STW式的、单线程的Full GC。Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。参数-XX:G1ReservePercent(默认10%)可以保留空间,来应对晋升模式下的异常情况,最大占用整堆50%,更大也无意义。

    G1在以下场景中会触发Full GC,同时会在日志中记录to-space-exhausted以及Evacuation Failure:

    1. 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
    2. 从老年代分区转移存活对象时,无法找到可用的空闲分区
    3. 分配巨型对象时在老年代无法找到足够的连续分区

    由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生。

    MixedGC的相关参数:

    • G1MixedGCLiveThresholdPercent:old区的region被回收时存活对象占比
    • G1MixedGCCountTarget:一次concurrent marking后,最多执行Mixed GC的次数,默认为8

    G1常用参数:

    • -XX:+UseG1GC:开启G1
    • -XX:InitiatingHeapOccupancyPercent:触发mixed GC的老年代堆占比阈值,默认45%
    • -XX:G1HeapWastePercent:触发mixed GC的垃圾占比阈值,默认5%
    • -XX:G1HeapRegionSize=n:region的大小,1-32M,2048个,默认1M
    • -XX:MaxGCPauseMillis=n:最大停顿时间,默认200ms
    • -XX:G1ReservePercent:为了降低晋升失败机率设置一个假的堆的储备空间的上限大小,默认值是10,意思是堆空间要保留10%
    • -XX:ParallelGCThreads:GC工作线程的并行数量(可以有多少个GC线程同时工作)
    • -XX:ConcGCThreads=n:GC的并发线程数=1/4*ParallelGCThreads,表示可以与用户线程并发的GC线程数

    最佳实践:

    • 年轻代大小:避免使用-Xmn、-XX:NewRatio(新生代和老年代的比值)等显式设置Young区的大小,会覆盖暂停时间目标(MaxGCPauseMillis)。
    • 暂停时间目标:暂停时间不要太严苛,其吞吐量目标是90%的app time和10%GC Time,太严苛会直接影响吞吐量。
    • 避免疏散失败:疏散失败是说当Java虚拟机在Survivor和晋升的对象垃圾回收期间,堆空间用光了就会发生晋升失败。堆空间不能再扩展了因为已经在最大值了,这个代价非常大。通过设置-XX:G1ReservePercent=n避免

    G1的特点

    • 并发与并行:G1充分利用多CPU、多核环境下的优势,使用多个CPU来缩短STW听段时间。G1收集器可以通过并发的方式让Java程序执行。
    • 分代收集:G1总体上是基于“标记-整理”算法,但每个region之间是“复制”实现的,这意味两种算法都不会产生碎片。
    • 可停顿预测,G1能够建立可预测的停顿时间模型,能够让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。

    在G1中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,JVM是使用Remembered Set避免全堆扫描的。

    G1中每个region都有与之对应的remembered set,当虚拟机发现程序堆Reference类型的数据进行写操作时,会产生一个Write Barrier中断写操作,检查Reference引用的对象是否处于不同的两个region中,如果是,便通过CardTable把相关引用信息记录到被引用对象所属的region的remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会遗漏。

    G1收集器的运作步骤分为以下几个步骤

    • 初始标记:标记GC Roots直接关联的对象。会引起STW。
    • 并发标记:从GC Root开始对堆中对象进行可达性分析,找出存活对象,可以与用户线程并发执行。不会引起STW。
    • 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,会引起STW,与CMS类似
    • 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

    参考资料:

    1、https://blog.csdn.net/coderlius/article/details/79272773

    2、周志明. 深入理解JVM虚拟机

    3、码出高效 Java开发手册

  • 相关阅读:
    element ui el-date-picker 判断所选时间是否交叉
    MDN中的箭头函数!!!
    es6 解构
    element ui 实现可编辑表格
    节流 防抖。。。。深入理解
    element ui 表格对齐方式,表头对齐方式
    纯html + css 导航栏
    PHP 1
    apache 建立虚拟主机
    Can't connect to local MySQL server through socket '/tmp/mysql.sock'
  • 原文地址:https://www.cnblogs.com/yn-huang/p/10764412.html
Copyright © 2011-2022 走看看