GC逻辑模型
1. 分代模型
serial和parallel、parNew以及CMS都是基于分代模型实现的GC组件。
分代模型将内存大致分为几个部分:
年轻代、幸存区、老年代。
其中年轻代和幸存区由一个回收器组件进行回收。
老年区由另一个组件进行回收。
serial下的两个组件分别是:SerialGC 和 SerialOldGC
parallel则是ParallelGC和ParallelOldGC
CMS也是基于分代模型,不过由于其没有对应的年轻代回收组件,大多数情况下使用parNew进行搭配,parNew和ParallelGC没有本质的区别,主要是为了配合CMS进行了优化。
分代模型图解:
年轻代进行GC后没有成功清理的的对象将会进入幸存区。
Eden与Survivor区默认8:1:1
eden区加上Survivor区和老年代的比例一般是1:2
(这些比例都是可以调整的)
如果新生代放不下新建的对象,则会发起Minor GC,Minor GC后依旧放不下则会将对象提前放入老年代(如果老年代也放不下则会调用full GC,full GC后依旧放不下则会OOM)
serial、parallel
serial和parallel的执行流程类似,最大的去呗是serial是单线程的,每个阶段都会STW,而parallel是多线程的,执行流程如下图所示
CMS(Concurrent Mark Sweep):
CMS为老年代的GC组件,执行流程如下图
初始标记:标记roots,也就是标记根节点。并发标记:和程序同时进行,从roots节点进行寻址,标记有效对象。
重新标记:由于并发标记和程序同时进行,所以可能会出现一些误标记,类似于标记时对象已经没有了引用,但在对象的finally方法里进行了自救,该对象就不该作为垃圾处理,所以在重新标记阶段会对误标进行修正。
并发清理:对垃圾对象进行清理,cms的并发清理流程是标记-清除,将有效对象之外的对象全部删除掉
并发重置:重置所有对象状态,准备下一次GC流程
2. 无分代模型
G1和ZGC目前的实现都大幅度的弱化了传统分代模型的概念,ZGC更是完全没有分代的概念,整套垃圾回收机制由一套复杂的算法进行实现
其中G1是从CMS演化而来,流程上略有些相似
垃圾判断标准都是用的寻址法,也就是确定一些根节点roots,从roots进行引用寻址,找得到的都不是垃圾,找不到的都当做垃圾处理
大致流程如下图
G1:
G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数"-XX:G1HeapRegionSize"手动指定Region大小
Region不再是物理隔阂,一个region可能之前是年轻代,在经过GC之后可能就会变成老年代,同理,一个老年代经过GC之后可能重新分配为年轻代,对应的内存模型类似下图
G1中eden区,survivor,old区的变化机理和CMS的分代机制相同,采用的是复制拷贝(标记整理)算法,将一个区块内有效对象复制拷贝到另一个区块中,拷贝结束后直接清掉那个region。
不同的是G1中多了一个humongous区,这个部分是用来专门用来存放大对象的,优化了大对象直接进入老年代可能引起的full GC问题,大对象的判定标注为超过单个region 50%的大小,如果单个region放不下大对象,则对横跨几个region进行存放
G1执行流程如下:
初始标记:对roots进行标记并发标记:和cms类似,从roots节点开始,和程序进行并发标记
最终标记:同上,和cms类似,修正因为并发标记产生的一些对象状态错误标记的问题
筛选回收:G1的筛选回收逻辑较为复杂,因为G1可以指定每个阶段的停顿时间,且每个阶段的停顿时间目前最长为200毫秒,但由于200毫秒不一定能清理完所以的垃圾,所以G1会先进行一次效价比分析,判断可以在短时间内清理且获得最大内存释放收益的对象进行优先清理。G1的回收方式为标记整理,可以简单理解为复制粘贴,也就是将一个区块内有效对象复制拷贝到另一个区块中,拷贝结束后直接清掉那个region,由于算法过于复杂,目前没有实现并发,但限制了每个流程执行的时间,所以在感觉上不会太明显。
并发重置:重置每一个region块的状态,准备下一次GC流程
PS:
Young GC:G1垃圾收集分类YoungGCYoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region(调整内存中给年轻代分配的总空间,以达到年轻代扩容的效果),继续给新对象存放,不会马上做YoungGC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC
MixedGC:不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old,在执行复制拷贝的过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC
Full GC:停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)
ZGC:
ZGC是一款JDK 11中新加入的具有实验性质的低延迟垃圾收集器
ZGC中暂时没有分代概念,而是将内存划分为小型region,中型region和大型region三部分进行数据存储,使用读屏障、 颜色指针等技术来实现可并发的标记-整理算法
小型Region(Small Region) : 容量固定为2MB, 用于放置小于256KB的小对象。
中型Region(Medium Region) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。
大型Region(Large Region) : 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或以上的大对象。 每个大型Region中只会存放一个大对象, 这也预示着虽然名字叫作“大型Region”, 但它的实际容量完全有可能小于中型Region, 最小容量可低至4MB。
执行流程和内存模型如下图所示。
初始标记:同样,拿到roots节点列表
并发标记:与G1相同,采用可达性分析方法进行对象标记,但与G1不同的是ZGC并不是在对象头进行标记,而是在指针层进行标记
最终标记:在最终标记的阶段会有短暂的停休,目的也是修正标记
并发预备重分配:这个阶段需要统计出这次GC需要清理哪些region,然后将这些region组成一个重分配集(Relocation Set)。
并发重分配:该阶段为ZGC的核心阶段,该阶段将会使用并发预备重分配生成的重分配集(Relocation Set),从中将有效对象几种移动到新的region中,然后维护一个重映射表,重映射表中存储了有效对象的旧内存地址和新内存地址,便于读屏障在进行对象读取时实时修改对象引用的内存指针
并发重映射:将重映射表中对应的内存地址修改批量进行更新,修正所有移动过的对象引用地址
颜色指针Colored Pointers
ZGC的核心设计之一。以前的垃圾回收器的GC信息都保存在对象头中,而ZGC的GC信息保存在指针中。每个对象有一个64位指针,这64位被分为:
18位:预留给以后使用;
1位:Finalizable标识,此位与并发引用处理有关,它表示这个对象只能通过finalizer才能访问(finalizer:object基类的一个空方法,如果被重写则会在GC之前调用该方法,该方法会且只会被调用一次);
1位:Remapped标识,设置此位的值后,对象未指向relocation set中(relocation set表示需要GC的Region集合);
1位:Marked1标识;
1位:Marked0标识,和上面的Marked1都是标记对象用于辅助GC;
42位:对象的地址(所以它可以支持2^42=4T内存):
关于两个marked标识的问题:每一个GC周期开始时,会交换使用的标记位,使上次GC周期中修正的已标记状态失效,所有引用都变成未标记。
GC周期1:使用mark0, 则周期结束所有引用mark标记都会成为01。
GC周期2:使用mark1, 与周期1相同,所有的mark标记都会成为10。
读屏障
ZGC采用的读屏障的方式来修正指针引用,由于ZGC采用的是复制整理的方式进行GC,很有可能在对象的位置改变之后指针位置尚未更新时程序调用了该对象,那么此时在程序需要并行的获取该对象的引用时,ZGC就会对该对象的指针进行读取,判断Remapped标识,如果标识为该对象位于本次需要清理的region区中,该对象则会有内存地址变化,会在指针中将新的引用地址替换原有对象的引用地址,然后再进行返回。
如此,使用读屏障便解决了并发GC的对象读取问题
roots根节点类型
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中相关的对象。
- 方法区中的类静态属性相关的对象。
- 方法区中常量相关的对象。
- 本地方法栈中JNI(Native方法)相关的对象。
三色标记
目前主流的GC可达性分析的算法基本都基于三色标记的思想实现,不过实现方法各不相同。
三色标记可以简单的理解为将所有的对象分黑、灰、白成三种颜色,三种颜色代表对象的不同状态。
黑色:代表该对象以及该对象的所有子节点已经都进行过可达性分析。
灰色:代表该对象已经进行过可达性分析,但该对象的子节点尚未全部完成可达性分析。
白色:代表该对象尚未进行过可达性分析。
如果所有的可达性分析都做完,那么白色对象即为待回收的垃圾对象,因为没有任何引用可以到达该对象以改变对象的颜色标签。
卡表
卡表简单的理解就是为了解决跨区引用带来的问题,对于分区块进行GC的GC组件来说都会存在这个问题。
例如:G1在清理一个region里存的对象的时候该region里的的对象被其他的region里的对象引用或者该region里的对象引用了其他region的对象时,如果进行跨收集区的可达性分析效果较差。
在垃圾收集场景中,收集器只需通过记忆集判断出某一块非收集区域是否存在指向收集区域的指针即可
于是出现了记忆集的概念。
卡表就是记忆集的实现,JVM使用的写屏障来维护卡表。
卡表是使用一个字节数组实现:CARD_TABLE[ ],每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡页”。
一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0.
GC时,便利卡表数组,找到值为1的的元素标识,筛选该标识对应的收集区的卡表中变脏的元素加入GCRoots里。
在卡表的层面来说,整个内存是分成无数个小块存在卡表(CARD_TABLE[ ])中的,所有的脏元素都会加入GCRoots中,GCRoots是不分区/代的,所有的区/代都要从GCroot开始进行可达性分析,所以就解决了跨区/跨代引用的问题
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。