• Java垃圾回收(GC)机制详解


    1 常见的JVM参数

    -XX:MaxTenuringThreshold:对象晋升老年代的阈值,默认值15(并不是绝对的,如果在Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代)
    -XX:MaxPermSize~~:永久代大小
    -XX:MaxDirectMemorySize:直接内存大小,默认与-Xmx一致
    -XX:+/-UseTLAB:是否开启TLAB
    -XX:MaxMetaspace=256m:元数据区,默认为无限大,受Java进程所使用的内存影响
    -XX:FieldsAllocationStyle:对象内存分布中的实例数据区域的存储顺序
    -XX:CompactFields=true:由于HotSpot在分配对象实例数据时相同大小的字段总是被分配到一起存储,在满足这个条件下因此父类中定义的变量会出现在子类之前,开启此参数那子类中较小的变量也允许插入父类变量的空隙中,以节省一点空间
    -XX:+UseCondCardMark:是否开启JVM卡表条件判断,尽量减少伪共享带来的性能损耗
    -XX:MaxGCPauseMillis(毫秒 >0):控制最大垃圾收集停顿时间,默认值200
    -XX:ParallelGCThreads=NUM:垃圾收集并行执行线程数,默认为CPU的核数
    -XX:+UseAdaptiveSizePolicy:是否开启自适应调节策略,JDK8默认开启
    -XX:SurvivorRatio:Eden和Survivor区的比例
    -XX:PretenureSizeThreshold:晋升老年代对象大小,超过指定大小直接在老年代分配,默认为0
    -XX:+PrintGCDetails:打印GC详细日志
    -XX:+PrintHeapAtGC:打印每次GC前后堆、方法区可用容量变化
    -XX:+PrintGCApplicationConcurrentTime :查看GC过程中用户线程并发时间
    -XX:+PrintGCApplicationStoppedTime:查看GC过程中用户线程停顿时间
    -XX:+PrintFlagsFinal:查看JVM参数的默认值

    垃圾收集器相关:

    (a)CMS相关
    -XX:+UseConMarkSweepGC:开启使用CMS垃圾收集器,新生代使用ParNew 老年代使用CMS
    -XX:CMSInitiatingOccupancyFraction=70:CMS垃圾收集器的回收阈值(老年代),JDK5及之前默认为68%,JDK6之后调整为92%。
    -XX:+UseCMSInitiatingOccupancyOnly:与XX:CMSInitiatingOccupancyFraction配合使用,只是用设定的回收阈值(上面指定的70%),如果不指定,JVM仅在第一次使用设定值,后续则自动调整。
    -XX:+/-CMSPrecleaningEnabled:开启/关闭CMS并发预清理。
    -XX:CMSScheduleRemarkEdenSizeThreshold:CMS可取消并发预处理阶段开启条件-->默认为2M
    -XX:CMSMaxAbortablePrecleanLoops:CMS可取消并发预处理阶段取消条件-->循环次数,默认为0
    -XX:CMSMaxAbortablePrecleanTime:CMS可取消并发预处理阶段取消条件-->最长执行时间,默认为5000毫秒
    -XX:CMSScheduleRemarkEdenPenetration:CMS可取消并发预处理阶段取消条件-->Eden区的内存使用率大于此配置后取消,默认值为50
    -XX:+UseCMSCompactAtFullCollection:在进行Full GC之前进行一次内存整理,默认开启
    -XX:CMSFullGCBeforeCompaction=N:当执行过N此无碎片整理Full GC后,下次Full GC之前进行一次内存整理,默认为0,表示每次都进内存整理
    -XX:+CMSScavengeBeforeRemark:强制在CMS最终/重标记阶段前进行一次Minor GC,防止可中断预清理一直没有等到年轻代Minor GC而导致年轻代对象太多而导致最终标记时间过长,导致停顿时间过长
    -XX:+CMSPermGenSweepingEnabled:开启CMS对永久代(元空间)的垃圾收集,默认不开启
    -XX:+CMSClassUnloadingEnabled:与-XX:+CMSPermGenSweepingEnabled配合使用,收集永久代时卸载不用的类


    (b)G1l垃圾收集器
    -XX:G1HeapRegionSize=8:设置G1垃圾收集器Region大小,取值范围应为1MB ~ 32MB,且应为2的N次幂。
    -XX:G1NewSizePercent:新生代最小值,默认值5%
    -XX:G1MaxNewSizePercent:新生代最大值,默认值60%
    -XX:ParallelGCThreads:STW期间,并行GC线程数
    -XX:ConcGCThreads=n:并发标记期间,GC线程数
    -XX:InitiatingHeapOccupancyPercent:设置触发标记周期的 Java 堆占用率阈值。默认值是45%。这里的java堆占比指的是nonyoungcapacitybytes,包括old+humongous
    -XX:G1HeapWastePercent:G1停止回收的最小内存,默认是堆的5%,就是说不必要每次回收就把所有的垃圾的处理完,可遗留少量的下次处理,这样也降低了单次GC消耗的时间
    -XX:+GCTimeRatio:计算花在Java应用线程上和花在GC线程上时间比率,默认是9,跟新生代内存的分配比例一样。参数的主要目的是让用户可以控制花在应用上的时间,G1的计算公式是100/(1+GCTimeRatio)。如果参数设置为9,则最多花10%的时间在GC上面,Parallel GC默认值是99,表示1%的时间被用在GC上面,这是因为Parallel GC贯穿整个GC,而G1则根据Region来进行划分,不需要全局性扫描整个内存
    -XX:G1ReserverPercent:G1为分配担保预留的空间比例,默认10%,也就是老年代会预留10%的空间来给新生代对象晋升,如果经常由于新生代对象晋升失败导致FullGC,可以适当调大此参数(调大此参数同时意味着老年代可使用的空间减少)

    堆内存参数

    文章配图
    -Xmx:指定最大堆内存,如-Xmx49。但这只是限制了堆内存Heap部分的最大值,不包括堆外内存和栈内存
    -Xms:指定堆内存的初始化大小,最小值,如Xms4g。而且指定的内存大小,并不是操作系统实际分配的初始值,而是GC先规划好,应用到才分配。专用服务需上需要保持-Xmx-Xms保持一致,否则应用刚启动可能就会有好几个FullGC。当两者配置不同时,堆内存扩容可能导致性能抖动。
    -Xmn:等价于-XX:NewSize,Young区的大小,使用G1垃圾收集器不应该设置该选项,在其他的某些业务场景可以设置,官方建议设置为-Xmx的1/2~1/4
    -XX:MaxPerSize:这是jdk1.7前使用的,JDK8后Meta空间默认无限大,此参数无效
    -XX:MetaspeaceSize:Java8默认不限制Meta空间,一般不允许设置该选项
    -XX:MaxDirectMemorySize:系统可食用的最大堆外内存,这个参数跟-Dsun.nio.MaxDirectMemorySize=1m等价
    -Xss:设置每个线程栈的字节数,影响栈的深度

    分析诊断相关

    -XX:+-HeapDumpOnOutOfMemoryError:当OOMError产生时,自动Dump堆内存
    -XX:HeapDumpPath:与HeapDumpOnOutOfMemoryError搭配使用,指定内存溢出时Dump文件的目录,默认为启动Java程序的工作目录下
    -XX:OnError:发生致命错误时执行的脚本
    -XX:OnOutOfMemoryError:抛出OOMError错误是执行的脚本
    -XX:ErrorFile=fileName:致命错误的日志文件名,绝对路径或者相对路径
    -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=1506:开启远程调试

    运行模式

    • -server:设置JVM使用server模式,特点是启动速度比较慢,但运行时性能和内存管理效率很高,适用于生产环境。在具有64位JDK环境下将默认启用此模式,而忽略-client参数
    • -clinet:JDK1.7之前在32位的X86机器上的默认值是-client选项。设置JVM使用client模式,特点是启动速度较快,但运行时性能和内存管理效率不高,通常用于客户端程序或者本地PC机开发和调试。
    • -Xint:在解释模式下运行,-Xint标记会强制JVM解释所有的字节码,这会降低运行速度,通常低10倍或者更多
    • -Xcomp:-Xcomp参数与-Xint正好相反,JVM会在第一次使用时把所有的字节码编译成本地代码,从而带来最大程度的优化。[注意预热]
    • -Xmixed:-Xmixed是混合模式,将解释模式和编译模式进行混合使用,有JVM自己决定,这是JVM的默认模式,也是推荐模式,使用java -version可以看到 mixed mode等信息;

    编译优化

    -XX:+DoEscapeAnalysis:开启逃逸分析
    -XX:+EliminateAllocations:开启标量替换
    -XX:+EliminateLocks :开启同步消除


    2 G1 收集器原理理解与分析

    前言

    G1收集器是一款在server端运行的垃圾收集器,专门针对于拥有多核处理器和大内存的机器,在JDK 7u4版本发行时被正式推出,在JDK9中更被指定为官方GC收集器。它满足高吞吐量的同时满足GC停顿的时间尽可能短。G1收集器专门针对以下应用场景设计

    • 可以像CMS收集器一样可以和应用并发运行
    • 压缩空闲的内存碎片,却不需要冗长的GC停顿
    • 对GC停顿可以做更好的预测
    • 不想牺牲大量的吞吐量性能
    • 不需要更大的Java Heap
    Can operate concurrently with applications threads like the CMS collector.
    Compact free space without lengthy GC induced pause times.
    Need more predictable GC pause durations.
    Do not want to sacrifice a lot of throughput performance.
    Do not require a much larger Java heap.

    G1从长期计划来看是以取代CMS为目标。与CMS相比有几个不同点使得G1成为GC的更好解决方案。第一点:G1会压缩空闲内存使之足够紧凑,做法是用regions代替细粒度的空闲列表进行分配,减少内存碎片的产生。第二点:G1的STW更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

    G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS). Comparing G1 with CMS, there are differences that make G1 a better solution. One difference is that G1 is a compacting collector. G1 compacts sufficiently to completely avoid the use of fine-grained free lists for allocation, and instead relies on regions. This considerably simplifies parts of the collector, and mostly eliminates potential fragmentation issues. Also, G1 offers more predictable garbage collection pauses than the CMS collector, and allows users to specify desired pause targets.

     

    概览

    在传统的GC收集器(serial,parallel,CMS)无一不例外都把heap分成固定大小连续的三个空间:young generation, old generation, and permanent generation

    但G1却独辟蹊径,采用了一种全新的内存布局

    在G1中堆被分成一块块大小相等的heap region,一般有2000多块,这些region在逻辑上是连续的。每块region都会被打唯一的分代标志(eden,survivor,old)。在逻辑上,eden regions构成Eden空间,survivor regions构成Survivor空间,old regions构成了old 空间。

    通过命令行参数-XX:NewRatio=n来配置新生代与老年代的比例,默认为2,即比例为2:1;-XX:SurvivorRatio=n则可以配置Eden与Survivor的比例,默认为8。

    GC时G1的运行方式与CMS方式类似,会有一个全局并发标记(concurrent global marking phase)的过程,去确定堆里对象的的存活情况。并发标记完成之后,G1知道哪些regions空闲空间多(可回收对象多),优先回收这些空的regions,释放出大量的空闲空间。这是为什么这种垃圾回收方式叫G1的原因(Garbage-First)。

    G1将其收集和压缩活动集中在堆中可能充满可回收对象(即垃圾)的区域,使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数量。

    需要注意的是,G1不是实时收集器。它能够以较高的概率满足设定的暂停时间目标,但不是绝对确定的。根据以前收集的数据,G1估算出在用户指定的目标时间内可以收集多少个区域。因此,收集器对于收集区域的成本有一个相当准确的模型,它使用这个模型来确定在暂停时间目标内收集哪些区域和收集多少区域。

    It is important to note that G1 is not a real-time collector. It meets the set pause time target with high probability but not absolute certainty. Based on data from previous collections, G1 does an estimate of how many regions can be collected within the user specified target time. Thus, the collector has a reasonably accurate model of the cost of collecting the regions, and it uses this model to determine which and how many regions to collect while staying within the pause time target.

    G1中的Region

    G1中每个Region大小是固定相等的,Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围从1M到32M,且是2的指数。如果不设定,那么G1会根据Heap大小自动决定。

    决定逻辑:

    size =(堆最小值+堆最大值)/ TARGET_REGION_NUMBER(2048) ,然后size取最靠近2的幂次数值, 并将size控制在[1M,32M]之间。具体代码如下

    // share/vm/gc_implementation/g1/heapRegion.cpp
    // Minimum region size; we won't go lower than that.
    // We might want to decrease this in the future, to deal with small
    // heaps a bit more efficiently.
    #define MIN_REGION_SIZE  (      1024 * 1024 )
    // Maximum region size; we don't go higher than that. There's a good
    // reason for having an upper bound. We don't want regions to get too
    // large, otherwise cleanup's effectiveness would decrease as there
    // will be fewer opportunities to find totally empty regions after
    // marking.
    #define MAX_REGION_SIZE  ( 32 * 1024 * 1024 )
    // The automatic region size calculation will try to have around this
    // many regions in the heap (based on the min heap size).
    #define TARGET_REGION_NUMBER          2048
    void HeapRegion::setup_heap_region_size(size_t initial_heap_size, size_t max_heap_size) {
      uintx region_size = G1HeapRegionSize;
      if (FLAG_IS_DEFAULT(G1HeapRegionSize)) {
        size_t average_heap_size = (initial_heap_size + max_heap_size) / 2;
        region_size = MAX2(average_heap_size / TARGET_REGION_NUMBER,
                           (uintx) MIN_REGION_SIZE);
      }
      int region_size_log = log2_long((jlong) region_size);
      // Recalculate the region size to make sure it's a power of
      // 2. This means that region_size is the largest power of 2 that's
      // <= what we've calculated so far.
      region_size = ((uintx)1 << region_size_log);
      // Now make sure that we don't go over or under our limits.
      if (region_size < MIN_REGION_SIZE) {
        region_size = MIN_REGION_SIZE;
      } else if (region_size > MAX_REGION_SIZE) {
        region_size = MAX_REGION_SIZE;
      }
    }
    

    G1中的GC收集

    G1保留了YGC并加上了一种全新的MIXGC用于收集老年代。G1中没有Full GC,G1中的Full GC是采用serial old Full GC。

    YGC

    当Eden空间被占满之后,就会触发YGC。在G1中YGC依然采用复制存活对象到survivor空间的方式,当对象的存活年龄满足晋升条件时,把对象提升到old generation regions(老年代)。

    G1控制YGC开销的手段是动态改变young region的个数,YGC的过程中依然会STW(stop the world 应用停顿),并采用多线程并发复制对象,减少GC停顿时间。

    YGC开始

    我们从图中看到Eden区中存活对象被复制到新的Survior区。

    YGC是否需要扫描整个老年代?

    我们知道判断对象是否存活需要从GC ROOTS结点出发,从GC ROOTS结点可达的对象就是存活的。在YGC时,老年代中的对象是不回收的,也就意味着GC ROOTS里面应包含了老年代中的对象。但扫描整个老年代会很耗费时间,势必影响整个GC的性能!。所以在CMS中使用了Card Table的结构,里面记录了老年代对象到新生代引用。Card Table的结构是一个连续的byte[]数组,扫描Card Table的时间比扫描整个老年代的代价要小很多!G1也参照了这个思路,不过采用了一种新的数据结构 Remembered Set 简称Rset。RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。而Card Table则是一种points-out(我引用了谁的对象)的结构,每个Card 覆盖一定范围的Heap(一般为512Bytes)。G1的RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。 这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。每个Region都有一个对应的Rset

     

    Rset结构

    RSet究竟是怎么辅助GC的呢?在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。 而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量。

    所以G1中YGC不需要扫描整个老年代,只需要扫描Rset就可以知道老年代引用了哪些新生代中的对象。

    MIXGC

    G1中的MIXGC选定所有新生代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region,在用户指定的开销目标范围内尽可能选择收益高的老年代Region进行回收。所以MIXGC回收的内存区域是新生代+老年代。

    在介绍MIXGC之前我们需要先了解global concurrent marking,全局并发标记。因为老年代回收要依赖该过程。

    全局并发标记

    全局并发标记过程分为五个阶段

    (1) Initial Mark初始标记 STW

    Initial Mark初始标记是一个STW事件,其完成工作是标记GC ROOTS 直接可达的对象。并将它们的字段压入扫描栈(marking stack)中等到后续扫描。G1使用外部的bitmap来记录mark信息,而不使用对象头的mark word里的mark bit。因为 STW,所以通常YGC的时候借用YGC的STW顺便启动Initial Mark,也就是启动全局并发标记,全局并发标记与YGC在逻辑上独立。

    (1) Initial Mark
    (Stop the World Event)This is a stop the world event. With G1, it is piggybacked on a normal young GC. Mark survivor regions (root regions) which may have references to objects in old generation.

    (2)Root Region Scanning 根区域扫描

    根区域扫描是从Survior区的对象出发,标记被引用到老年代中的对象,并把它们的字段在压入扫描栈(marking stack)中等到后续扫描。与Initial Mark不一样的是,Root Region Scanning不需要STW与应用程序是并发运行。Root Region Scanning必须在YGC开始前完成。

    (2) Root Region Scanning
    Scan survivor regions for references into the old generation. This happens while the application continues to run. The phase must be completed before a young GC can occur.

    (3)Concurrent Marking 并发标记

    不需要STW。不断从扫描栈取出引用递归扫描整个堆里的对象。每扫描到一个对象就会对其标记,并将其字段压入扫描栈。重复扫描过程直到扫描栈清空。过程中还会扫描SATB write barrier所记录下的引用。Concurrent Marking 可以被YGC中断

    (3) Concurrent Marking
    Find live objects over the entire heap. This happens while the application is running. This phase can be interrupted by young generation garbage collections.

    (4)Remark 最终标记 STW

    STW操作。在完成并发标记后,每个Java线程还会有一些剩下的SATB write barrier记录的引用尚未处理。这个阶段就负责把剩下的引用处理完。同时这个阶段也进行弱引用处理(reference processing)。注意这个暂停与CMS的remark有一个本质上的区别,那就是这个暂停只需要扫描SATB buffer,而CMS的remark需要重新扫描mod-union table里的dirty card外加整个根集合,而此时整个young gen(不管对象死活)都会被当作根集合的一部分,因而CMS remark有可能会非常慢。

    (4) Remark
    Completes the marking of live object in the heap. Uses an algorithm called snapshot-at-the-beginning (SATB) which is much faster than what was used in the CMS collector.

    (5)Cleanup 清除 STW AND Concurrent

    STW操作,清点出有存活对象的Region和没有存活对象的Region(Empty Region)

    STW操作,更新Rset

    Concurrent操作,把Empty Region收集起来到可分配Region队列。

    (5) Cleanup
    Performs accounting on live objects and completely free regions. (Stop the world)
    Scrubs the Remembered Sets. (Stop the world)
    Reset the empty regions and return them to the free list. (Concurrent)

    经过global concurrent marking,collector就知道哪些Region有存活的对象。并将那些完全可回收的Region(没有存活对象)收集起来加入到可分配Region队列,实现对该部分内存的回收。对于有存活对象的Region,G1会根据统计模型找处收益最高、开销不超过用户指定的上限的若干Region进行对象回收。这些选中被回收的Region组成的集合就叫做collection set 简称Cset!

    在MIXGC中的Cset是选定所有young gen里的region,外加根据global concurrent marking统计得出收集收益高的若干old gen region。

    在YGC中的Cset是选定所有young gen里的region。通过控制young gen的region个数来控制young GC的开销。

    YGC与MIXGC都是采用多线程复制清除,整个过程会STW。 G1的低延迟原理在于其回收的区域变得精确并且范围变小了。

    STAB

    上面global concurrent marking提到了STAB算法,那这个STAB到底为何物?STAB全称为snapshot-at-the-beginning,其目的是了维持并发GC的正确性。GC的正确性是保证存活的对象不被回收,换句话来说就是保证回收的都是垃圾。如果标记过程是STW的话,那GC的正确性是一定能保证的。但如果一边标记,一边应用在变更堆里面对象的引用,那么标记的正确性就不一定能保证了。

    为了解决这个问题,STAB的做法在GC开始时对内存进行一个对象图的逻辑快照(snapshot),通过GC Roots tracing 参照并发标记的过程,只要被快照到对象是活的,那在整个GC的过程中对象就被认定的是活的,即使该对象的引用稍后被修改或者删除。同时新分配的对象也会被认为是活的,除此之外其它不可达的对象就被认为是死掉了。这样STAB就保证了真正存活的对象不会被GC误回收,但同时也造成了某些可以被回收的对象逃过了GC,导致了内存里面存在浮动的垃圾(float garbage)。

     

    STAB具体细节:

    每个Region中都有那么几个指针

    |<-- (1) -->|<-- (2) -->|<-- (3) -->|<-- (4) -->|
    bottom prevTAMS nextTAMS top end

    其中top是该region的当前分配指针,[bottom, top)是当前该region已用(used)的部分,[top, end)是尚未使用的可分配空间(unused)。

    (1): [bottom, prevTAMS): 这部分里的对象存活信息可以通过prevBitmap来得知

    (2): [prevTAMS, nextTAMS): 这部分里的对象在第n-1轮concurrent marking是隐式存活的

    (3): [nextTAMS, top): 这部分里的对象在第n轮concurrent marking是隐式存活的

    为什么会用prevTAMS和nextTAMS两个指针?

    因为G1的并发标记的过程用了两个bitmap:

    一个prevBitmap记录第n-1轮concurrent mark所得的对象存活状态。由于第n-1轮concurrent marking已经完成,这个bitmap的信息可以直接使用。

    一个nextBitmap记录第n轮concurrent mark的结果。这个bitmap是当前将要或正在进行的concurrent mark的结果,尚未完成,所以还不能使用。

    所以Region会同时存在prevTAMS和nextTAMS两个指针,这两个指针是在 Initial Mark阶段就会设置好。

    所以我们很容易知道哪些对象在一次GC开始之后新分配的:在TAMS以上的对象就是新分配的,因而被视为隐式marked,标记为存活。

    切换到另外一个场景:如果在标记的过程中mark了某个对象但对象中某些引用这字段还没有被mark到,此时应用并发修改引用字段的值,那collecotr就拿不到完整的快照了,这不符合STAB的设想。

    为了解决这个问题就有了SATB write barrier。G1 GC具体使用的是Yuasa式的SATB write barrier的变种。它的相关论文是:

    Write barrier是对“对引用类型字段赋值”这个动作的环切,也就是说赋值的前后都在barrier覆盖的范畴内。在赋值前的部分的write barrier叫做pre-write barrier,在赋值后的则叫做post-write barrier。

    在HotSpot VM里,在引入G1 GC之前,其它GC都只用了post-write barrier,所以它在源码里没有特别的前后缀;而G1 GC特有的pre-write barrier则在源码里有_pre的后缀,可以留意一下。

    void oop_field_store(oop* field, oop value) {
      pre_write_barrier(field);
      *field = value; // the actual store
      post_write_barrier(field, value);
    }
    

    1、Pre/Post-write barrier与SATB的关系

    前面提到SATB要维持“在GC开始时活的对象”的状态这个逻辑snapshot。除了从root出发把整个对象图mark下来之外,其实只需要用pre-write barrier把每次引用关系变化时旧的引用值记下来就好了。这样,等concurrent marker到达某个对象时,这个对象的所有引用类型字段的变化全都有记录在案,就不会漏掉任何在snapshot里活的对象。当然,很可能有对象在snapshot中是活的,但随着并发GC的进行它可能本来已经死了,但SATB还是会让它活过这次GC。

    所以在G1 GC里,整个write barrier+oop_field_store是这样的:

    void oop_field_store(oop* field, oop new_value) {
      pre_write_barrier(field);             // pre-write barrier: for maintaining SATB invariant
      *field = new_value;                   // the actual store
      post_write_barrier(field, new_value); // post-write barrier: for tracking cross-region reference
    }
    

    按照Yuasa式SATB barrier的设计,pre-write barrier里面的抽象逻辑应当如下:

    void pre_write_barrier(oop* field) {
      if ($gc_phase == GC_CONCURRENT_MARK) { // SATB invariant only maintained during concurrent marking
        oop old_value = *field;
        if (old_value != null && !is_marked(old_value)) {
          mark_object(old_value);
          $mark_stack->push(old_value); // scan all of old_value's fields later
        }
      }
    }
    

    这比原本的Yuasa式设计少了些东西:没有检查目标对象是否已经mark,也不去对目标对象做mark和扫描它的字段。实际上该做的事情还是得做,只是不在这里做而已。那放在那里做呢放到了后面的logging barrier,这个后面讲到。

    Pre-write barrier的实际代码有好几个版本,其中最简单明白的版本是:

      // This notes that we don't need to access any BarrierSet data
      // structures, so this can be called from a static context.
      template <class T> static void write_ref_field_pre_static(T* field, oop newVal) {
        T heap_oop = oopDesc::load_heap_oop(field);
        if (!oopDesc::is_null(heap_oop)) {
          enqueue(oopDesc::decode_heap_oop(heap_oop));
        }
      }
    

    enqueue动作的实际代码则在G1SATBCardTableModRefBS::enqueue(oop pre_val)。

    它判断当前是否在concurrent marking phase用的是:

    JavaThread::satb_mark_queue_set().is_active()  
    

    2、logging write barrier

    为了尽量减少write barrier对应用mutator性能的影响,G1将一部分原本要在barrier里做的事情挪到别的线程上并发执行。
    实现这种分离的方式就是通过logging形式的write barrier:mutator只在barrier里把要做的事情的信息记(log)到一个队列里,然后另外的线程从队列里取出信息批量完成剩余的动作。

    以SATB write barrier为例,每个Java线程有一个独立的、定长的SATBMarkQueue,mutator在barrier里只把old_value压入该队列中。一个队列满了之后,它就会被加到全局的SATB队列集合SATBMarkQueueSet里等待处理,然后给对应的Java线程换一个新的、干净的队列继续执行下去。

    并发标记(concurrent marker)会定期检查全局SATB队列集合的大小。当全局集合中队列数量超过一定阈值后,concurrent marker就会处理集合里的所有队列:把队列里记录的每个oop都标记上,并将其引用字段压到标记栈(marking stack)上等后面做进一步标记。

    所以整个STAB过程讲完。

    G1命令行选项与最佳实践

    1、命令行选项

    -XX:+UseG1GC 告诉JVM使用G1收集器

    -XX:MaxGCPauseMillis=200 设置最大GC目标最大停顿时间为200ms,这是一个软指标。JVM会最大努力去达到它,因此有时停顿时间会达不到设置目标。G1配置是200ms

    -XX:InitiatingHeapOccupancyPercent=45 启动并发标记标记百分比,当整堆内存使用量达到百分比时,G1使用它来触发一个基于整个堆的并发标记循环,而不仅仅是一个代。默念值是45%

    2、最佳实践

    下面有几个关于使用G1的最佳实践

    不要设置Young Generation大小

    因为显式通过-Xmn设置young generation大小将会干预G1收集器的默认行为

    • G1将不再尊重设定的pause time,本质来说是因为设置young generation大小使得设定的pause time目标失效。
    • G1不再能够根据需要扩展和收缩young generation的空间。由于大小是固定的,所以不能更改大小。

    响应时间指标

    不要使用平均响应时间(ART)作为指标来设置XX:MaxGCPauseMillis=<N>,而要考虑设置90%以上时间都能达到目标的值。这意味着90%发出请求的用户不会经历高于目标的响应时间。记住,暂停时间是一个目标,并不能保证总能达到。

    什么是Evacuation Failure

    当JVM在GC期间复制对象到Survior区或或者提升对象时,堆空间被耗尽,堆区域升级失败。堆无法扩展,因为它已经处于最大值。这种情况在-XX:+PrintGCDetails将会以TO空间溢出(to-space overflow)的形式表示。代价是十分昂贵的,因为

    • GC仍然需要继续,所以必须释放空间
    • 未成功复制的对象必须在适当的位置保留
    • 对CSet中区域的rset的任何更新都必须重新生成
    • 所有这些步骤都很昂贵。

    如何避免Evacuation Failure

    为了避免Evacuation Failure,考虑以下选项。

    • 增大堆(内存)大小
      • 增大-XX:G1ReservePercent=n,默认是10
      • G1会预留一部分内存,制造一个假天花板,当真正Evacuation Failure时还有内存可用。
    • 早点启动标记周期
    • 增大并行标记的线程数,使用-XX:ConcGCThreads=n选项。

    完整的G1 GC开关列表

    • -XX:+UseG1GC 使用G1 GC。
    • -XX:MaxGCPauseMillis=n 设置最大GC停顿时间,这是一个软目标,JVM会尽最大努力去达到它。
    • -XX:InitiatingHeapOccupancyPercent=n 启动并发标记循环的堆占用率的百分比,当整个堆的占用达到比例时,启动一个全局并发标记循环,0代表并发标记一直运行。默认值是45%。
    • -XX:NewRatio=n 新生代和老年代大小的比例,默认是2。
    • -XX:SurvivorRatio=n eden和survivor区域空间大小的比例,默认是8。
    • -XX:MaxTenuringThreshold=n 晋升的阈值,默认是15(一个存活对象经历多少次GC周期之后晋升到老年代)。
    • -XX:ParallelGCThreads=n 设置GC并发阶段的线程数,默认值与JVM运行平台相关。
    • -XX:ConcGCThreads=n 设置并发标记的线程数,默认值与JVM运行平台相关。
    • -XX:G1ReservePercent=n 设置保留java堆大小比例,用于防止晋升失败/Evacuation Failure,默认值是10%。
    • -XX:G1HeapRegionSize=n 设置Region的大小,默认是根据堆的大小动态决定,大小范围是[1M,32M]

    使用G1记录GC

    这里简要介绍可以用来收集GC日志信息的相关配置开关。

    1、日志详细级别

    (1) -verbosegc(相当于-XX:+PrintGC)将日志的详细级别设置为详细。

    样例输出

    [GC pause (G1 Humongous Allocation) (young) (initial-mark) 24M- >21M(64M), 0.2349730 secs]
    [GC pause (G1 Evacuation Pause) (mixed) 66M->21M(236M), 0.1625268 secs]

    (2) -XX:+PrintGCDetails将细节级别设置为更精细。选项显示以下信息:

    显示每个阶段的平均时间、最小时间和最大时间。

    根扫描、RSet更新(带有已处理缓冲区信息)、RSet扫描、对象复制、终止(带有尝试次数)。

    还显示“其他”时间,如选择CSet所花费的时间、引用处理时间、引用排队时间和释放CSet时间。

    显示Eden、Survivor空间和总堆占用率。

    样例输出

    [Ext Root Scanning (ms): Avg: 1.7 Min: 0.0 Max: 3.7 Diff: 3.7]
    [Eden: 818M(818M)->0B(714M) Survivors: 0B->104M Heap: 836M(4096M)->409M(4096M)]

    (3)-XX:+UnlockExperimentalVMOptions -XX:G1LogLevel=finest 细节级别最为精细。精细到单个工作线程信息

    样例输出

    [Ext Root Scanning (ms): 2.1 2.4 2.0 0.0
    Avg: 1.6 Min: 0.0 Max: 2.4 Diff: 2.3]
    [Update RS (ms): 0.4 0.2 0.4 0.0
    Avg: 0.2 Min: 0.0 Max: 0.4 Diff: 0.4]
    [Processed Buffers : 5 1 10 0
    Sum: 16, Avg: 4, Min: 0, Max: 10, Diff: 10]

    2、时间打印

    这里有两个关于时间的开关

    (1) -XX:+PrintGCTimeStamps - 显示JVM启动后经过的时间。

    样例输出

    1.729: [GC pause (young) 46M->35M(1332M), 0.0310029 secs]

    (2) -XX:+PrintGCDateStamps - 为每个条目添加日期前缀。

    样例输出

    2012-05-02T11:16:32.057+0200: [GC pause (young) 46M->35M(1332M), 0.0317225 secs]

    总结

    在文章介绍了很多关于G1的一些原理和概念

    最后简单归纳一下:

      • G1把内存分成一块块的Region,每块的Region的大小都是一样的。
      • G1保留了YGC并加上了一种全新的MIXGC用于收集老年代。G1中没有Full GC,G1中的Full GC是采用serial old Full GC。在MIXGC中的Cset是选定所有young gen里的region,外加根据global concurrent marking统计得出收集收益高的若干old gen region。在YGC中的Cset是选定所有young gen里的region。通过控制young gen的region个数来控制young GC的开销。YGC与MIXGC都是采用多线程复制清除,整个过程会STW。
      • G1的低延迟原理在于其回收的区域变得精确并且范围变小了。
      • 全局并发标记分的五个阶段。
      • 用STAB来维持并发GC的准确性。
      • 使用G1的最佳实践
      • G1 GC日志打印

    转自:

    常见的JVM参数:https://baijiahao.baidu.com/s?id=1713969190703133662&wfr=spider&for=pc

    G1 收集器原理理解与分析: https://zhuanlan.zhihu.com/p/52841787?from_voters_page=true

    https://www.cnblogs.com/xiaoxi/p/6486852.html

    https://blog.csdn.net/laomo_bible/article/details/83112622

  • 相关阅读:
    php设置和获取cookie
    字符截取 支持UTF8/GBK
    PHP自毁程序
    php短信发送
    PHP版QQ互联OAuth示例代码分享
    javascript中window.open()与window.location.href的区别
    SpringBoot文件上传
    IDEA或Webstorm设置Ctrl+滚轮调整字体大小
    IDEA和WebStorm破解教程--激活80年(ideaIU-2018.3.6以及之前的版本)
    3 字节的 UTF-8 序列的字节 2 无效
  • 原文地址:https://www.cnblogs.com/ylz8401/p/16125007.html
Copyright © 2020-2023  润新知