这篇文章是学习G1过程中记下的一些笔记,大部分内容是从参考文章中复制过来的。简介GC演化随着内存大小的不断增长而演进:几M——几十M:串行、单线程STW(StopTheWorld)垃圾回收。数百M-1G:parallel,并行多线程垃圾回收。几个G:cms,ConcurrentGc。几十个G:G1。数百GB–TB:ZGC。G1之前的收集器,当Heap区越来越大时,STW阶段耗时较长,CMS由于内存碎片需要压缩,也会造成较长的停顿时间。所以无论堆内存大小如何,都需要一个吞吐量高、停顿时间短的收集器。介绍G1的全称是GarbageFirst。它在JDK6u14中发布,并在JDK7u4发布时正式推出。它旨在取代CMS垃圾收集器。它已成为JDK9中的默认垃圾收集器。G1是一种响应时间优先的GC算法。最大的特点是暂停时间是可配置的。用户可以设置整个GC过程的预期停顿时间。参数-XX:MaxGCPauseMillis指定G1收集过程的目标停顿时间。默认值是200ms,但不是硬性规定,只是期望值。那么G1是如何满足用户期望的呢?您需要暂停预测模型(PausePredictionModel)。G1根据本模型计算出的历史数据,预测本次集合需要选择的Region数量,尽可能满足用户设定的目标停顿时间。内部结构堆分为N(可配置,默认2048)个相等的region(区域),每个region占据一个连续的地址空间,垃圾回收以region为单位进行,这个region的大小是可配置的,如果不配置,G1会根据堆大小自动决定你的区域大小。分配时,如果选择的区域已满,会自动寻找下一个空闲区域进行分配。一个Region的大小可以通过参数-XX:G1HeapRegionSize设置,取值范围为1M到32M,是2的指数。如果不设置,那么G1会自动根据Heap大小(heapsize/2048).G1中的区域主要分为两种:新生代区域:G1不需要设置新生代大小(默认5-60%),Eden区-新分配的对象Survivor区-新生代在GC后存活但确实不需要提升老年代区域中的对象提升到老年代,直接分配给老年代中的巨型对象,占用多个区域的对象为HumongousRegion:G1有专门分配巨型对象的Region,而不是进入老年代Region。大小达到甚至超过分区大小一半(可配置)的对象称为HumongousObject。线程在为jumbo分配空间时,不能简单地在TLAB中分配,因为jumbo对象的移动开销很大,有可能一个partition容纳不下jumbo对象。所以hugeobjects会直接分配在oldgeneration,占用的连续空间称为hugepartition(HumongousRegion)。G1做了内部优化。一旦发现没有指向巨型对象的引用,可以直接在新生代回收周期中回收。Humongous对象将独占一个或多个连续分区,其中第一个分区标记为StartsHumongous,相邻的连续分区标记为ContinuesHumongous。由于不能享受Lab带来的优化,需要扫描整个堆来确定一块连续的内存空间,确定大对象起始位置的成本非常高。如果可能,应用程序应避免生成巨大的对象。巨大的对象永远不会被移动,它们要么直接被回收,要么永远存在。确定巨大对象的起始位置的成本非常高,应用程序应避免生成巨大的对象。GC基本可达性分析是如何判断一个对象是否是垃圾的?JVM采用可达性分析算法,以“GCROOT”为根节点,根据引用关系向下查找。下图中的对象a和b是不可达的,会被标记为垃圾。GCRoot的对象:在虚拟机栈(栈帧中的局部变量表)中引用的对象,如每个线程调用的方法栈中的参数、局部变量、临时变量等。本地方法栈中JNI引用的对象(即通俗的Native方法)。方法区中类静态属性引用的对象,如Java类的引用类型静态变量。方法区常量引用的对象,如字符串常量池中的引用。Jvm内部的引用,如基本数据类型对应的Class对象,系统类加载器等。在遍历对象的过程中,三色标记算法根据“已访问”的情况将对象标记为以下三种颜色ornot":white:还没有访问过。黑色:这个对象被访问过,这个对象引用的其他所有对象也都被访问过。灰色:这个对象已经被访问过,但是这个对象引用的其他对象还没有被访问过。浮动垃圾假设E已经遍历(变成灰色),此时应用程序执行objD.fieldE=null(D>E的引用被破坏)。在这一刻之后,“应该”收集对象E/F/G。但是因为E已经变灰了,所以还是会认为是存活对象,继续遍历。最终的结果是:这部分对象仍然会被标记为alive,即本轮GC不会回收这部分内存。这部分应该被回收,但是没有被回收的内存被称为“浮动垃圾”。直到下一轮垃圾回收才被清除。遗漏标记假设GC线程已经遍历到E(变成灰色),此时应用线程先执行:varG=objE.fieldG;objE.fieldG=null;//灰色E断开对白色G的引用objD.fieldG=G;//黑D引用白G缺失标记的结果是:G会一直留在白集合中,最后作为垃圾被清除。这直接影响应用程序的正确性,是不可接受的。G1的解决方案是writebarrier+SATB。WritebarrierWritebarrier:给对象的成员变量赋值时,在赋值操作前后添加一些处理(类似AOP的概念)//writebarrier-预写操作*field=new_value;post_write_barrier(字段,值);//写屏障-写后操作}SATBSATB(SnapshotAtTheBeginning,初始快照),是并发标记阶段开始时对象之间的引用关系,以逻辑快照形式保存的一种手段。简单理解就是在并发标记的时候,以当前引用关系作为基础引用数据,而不管Mutator(Snapshot名称的由来)在并发运行期间引用关系的修改,并认为被标记时还活着。SATB数据将在gc期间被扫描。上图Rset中,RegionB和C为老年代,RegionA为新生代。RegionAisunreachabletoGCRoot:younggc时,是否需要扫描所有年老代对象?mixgc的时候(回收一些对象),需要扫描所有老年代?RememberedSet(简称RS或RSet)就是用来解决这个问题的。RSet会记录引用关系(记录老指年轻,老指老,其他不记录)。每个Region都有一个RSet,通过哈希表实现。这个哈希表的key是引用这个region的其他region的地址(只记录old指young和old指old,young指young和young指old不记录)。value是一个数组,数组的元素是referrer对象对应的CardPage的CardTable中的下标。mixgc时Rset会被重置。在做youngGC的时候,只需要选择youngregion的RSet作为rootset即可(也就是标记的时候,RSet也作为ROOTS遍历)。这些RSets记录了old->young的跨代引用,避免扫描整个老年代,混合gc。所以RSet的引入大大降低了GC的工作量。摘自一大篇R解释:G1GC在points-out卡表之上加了一层结构,形成points-intoRSet:每个region会记录哪些其他region有指向自己的指针,而这些指针在卡的范围。这个RSet其实就是一个哈希表,key是其他region的起始地址,value是一个集合,里面的元素就是card表的索引。例如,如果区域A的RSet中某一项的key为区域B,value中有一张索引为1234的卡片,则说明区域B中的一张卡片有一个指向区域A的引用。那么对于区域A、RSet记录点入关系;而卡表中仍然记录着点出关系。TLABTLAB(ThreadLocalAllocationBuffer):本地线程缓冲区。由于堆内存是应用程序共享的,所以应用程序的多个线程在分配内存时需要加锁同步。为避免锁定,G1GC将默认启用TLAB优化。每个应用线程都会分配一个TLAB,每个TLAB独占一个线程。当对象不是Humongous对象并且TLAB也可以加载时,该对象会分配给最先创建该对象的线程。在TLAB中。这样分配会很快,因为TLAB是属于线程的,不需要加锁。PLABPLAB(PromotionThreadLocalAllocationBuffer):“PromotionThreadLocalAllocationBuffer”的思想和TLAB一样。G1的回收过程是由多个线程执行的。为了防止多个线程复制到同一个内存段,复制过程也需要加锁。为了避免加锁,G1的每个线程都关联了一个PLAB,这样就不需要加锁了。GCg1的gcscore:younggc,使用mark-copy算法。mixgc,使用mark-copy算法。fullgc,使用mark-collat??ion算法。Younggc当JVM无法为eden区分配新的对象时(新生代总区域大小超过新生代大小的限制),超过就会进行younggc。年轻gc只选择年轻代区域(Eden/Survivor)进入收集集(CollectionSet,简称CSet)进行回收。为了满足用户对暂停时间的配置,在每次GC之后,G1会根据用户设置的GC暂停时间上限动态调整新生代的大小。年轻的gc是STW。gc步骤选择收集集(ChooseCSet):G1会根据用户设置的GC停顿时间上限,选择最大数量的youngzone区域作为收集集。根处理(RootScanning):接下来需要从GCROOTS遍历,从ROOTS中直接找到对象到collectionset,将它们移动到Survivor区,并将它们的引用对象加入到标记栈中。RSetScan(ScanRS):以ROOTS为对象遍历RSet,寻找可以直接到达collectionset的对象,将它们移动到Survivor区,并将它们的引用对象添加到标记栈中。移动(Evacuation/ObjectCopy),遍历上面的标记栈,将栈中的所有对象移动到Survivor区。剩下的就是一些收尾工作,Redirty(配合后面的并发标记),ClearCT(清理CardTable),FreeCSet(清理回收集合),清除移动前的区域,加入空闲区域等等,这些操作一般耗时都比较短。mixgc混合回收:young+old。它会选择所有年轻代区域(Eden/Survivor)和部分老年代区域进入回收集合进行回收。当老年代使用的内存加上本次要分配的内存超过IHOP阈值(InitiatingHeapOccupancyPercent,默认为45%),就会启动mxgc。首先执行一个年轻代回收过程,也就是STW。InitialMarkInitialMarkInitialMark:标记所有可以直接从GCRoot到达的对象。younggc之后的survivor对象也会被认为是GCRoot,STW,younggc的停顿时间会被复用(和younggc一起执行)。初始标记负责标记所有直接可达的根对象(本机堆栈对象、全局对象、JNI对象)。根是对象图的起点,所以初始标记需要挂起Mutator线程(Java应用线程),也就是需要一段STW。实际上,当达到IHOP阈值时,G1并不会立即发起并发标记周期,而是等待下一次年轻代收集,并利用年轻代收集的STW周期完成初始标记。这种方法称为Piggybacking)。在初始标记暂停期间,将分区的NTAMS设置到分区的顶部,并发执行初始标记,直到处理完所有分区。RootRegionScanning的初始标记暂停结束后,新生代收集也完成了将对象复制到Survivor的工作,应用线程开始活跃。此时,为了保证标记算法的正确性,所有新复制到Survivor分区的对象都需要被扫描并标记为roots。这个过程称为根区域扫描(RootRegionScanning),扫描到的Survivor分区也称为RootRegionScanning。根区域。根分区扫描必须在下一次新生代垃圾回收开始前完成(在并发标记过程中,可能会被多次新生代垃圾回收打断),因为每次GC都会产生新的存活对象集合。ConcurrentMarkingConcurrentMarking:并发阶段。从上一阶段扫描的对象开始,逐个遍历搜索,每找到一个对象就标记为存活,扫描SATB。与应用线程并发执行,并发标记线程在并发标记阶段启动。启动次数由参数-XX:ConcGCThreads控制(默认GC线程数的1/4,即-XX:ParallelGCThreads/4),每个线程每次只扫描一个分区,从而标记存活对象图.在此阶段,处理Previous/Next标签位图,并扫描标签对象的引用字段。同时,并发标记线程还定期检查和处理STAB全局缓冲区列表的记录,更新对象引用信息。参数-XX:+ClassUnloadingWithConcurrentMark将开启优化。如果一个类不可达(不是对象不可达),在remarking阶段会直接卸载该类。必须在堆满之前扫描所有标记任务。如果并发标记耗时较长,可能在并发标记过程中进行了多次新生代回收。如果在堆满之前标记任务没有完成,就会触发保证机制,经历长时间的串行FullGC。LiveDataAccountingLiveDataAccounting实时数据记账(LiveDataAccounting)是标记操作的附加产物。只要标记了一个对象,就会同时计算字节数并计入分区空间。只有低于NTAMS的对象才会被标记和计算。在标记周期结束时,Next位图将被清除并等待下一个标记周期。Remark(finalmark)RemarkRemark:这将是STW。虽然在前面的并发标记过程中扫描了SATB,但毕竟前一阶段还是一个并发过程,所以需要在并发标记完成后再次挂起所有用户线程,重新标记SATB。重新标记(Remark)是最后一个标记阶段。在这个阶段,G1需要一个暂停时间来处理剩余的SATBlogbuffer和所有的更新,找出所有未被访问的存活对象,并安全地完成存活数据计算。这个阶段也是并行执行的。GC暂停时可用的GC线程数可以通过参数-XX:ParallelGCThread设置。同时,引用处理也是remarking阶段的一部分,所有大量使用引用对象(弱引用、软引用、幻像引用和最终引用)的应用程序都会在引用处理中产生开销。Cleanup清理:识别高产老年代分区,清理并重置标记状态,STW。Remark阶段之后的Clean阶段也是STW。上一个/下一个标记位图以及PTAMS/NTAMS将在清理阶段转换角色。清理阶段主要进行以下操作:RSet排序,启发式算法根据activity和RSetsize定义不同级别的分区,RSet数学也有助于找到无用的引用。参数-XX:+PrintAdaptiveSizePolicy可以启用打印启发式算法的决策细节;清理堆分区,并为混合收集周期识别具有高回收回报(基于空闲空间和暂停目标)的老年代分区的收集;识别所有空闲分区,即找到没有survivors对象的分区。分区可以在清除阶段直接回收,无需等待下一个回收周期。FullGC当mixGC跟不上内存分配的速度,导致老年代满时,就会进行FullGC回收整个堆。G1中的FullGC也是单线程串行的,而且是全挂起,开销很大。Fullgc在以下场景触发:从新生代分区复制存活对象时,找不到可用的空闲分区。从老年代分区转移存活对象时,找不到可用的空闲分区。分配大对象时无法在老年代找到足够的连续分区。综上所述,G1并不是一个高效的收集器。它对老年代使用复制型恢复算法。虽然不存在碎片问题,但是效率低下。因为老年代的对象大多是活的,所以每次收集的时候需要移动的对象很多。在清除算法中,清除死对象,所以从效率的角度,清除算法在老年代会更好。但是由于G1的增量恢复,可以控制停顿,可以保证每次停顿的停顿时间都在允许的范围内。对于大多数应用程序,暂停时间比吞吐量更重要。再加上G1各种细节的优化,效率已经很高了。参考:这可能是最清晰易懂的G1GC数据JVM系列16(三色标注和读写屏障)
