JVM垃圾算法与垃圾收集器

垃圾算法

日后待完善。

分带垃圾算法

根据对象存活周期的不同将内存分为几块,一般将堆分为新生代和老年代。
在新生代中,每次收集都会有大量对象(近99%)死去,所以可以选择复制算法。
老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
标记-清除”或“标记-整理”算法会比复制算法慢10倍以上

1.标记-复制算法

将内存分为大小相同的两块,每次使用其中的一块。
当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。
缺点:只能使用一半空间,浪费空间。
在这里插入图片描述

2.标记-清除算法

标记存活的对象, 统一回收所有未被标记的对象(一般垃圾收集器选择这种)。
也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。
缺点:1. 效率问题 (如果需要标记的对象太多,效率不高);2. 空间问题(标记清除后会产生大量不连续的碎片)。
在这里插入图片描述

3.标记-整理算法

标记过程仍然与“标记-清除”算法一样。
后续步骤是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
根据老年代的特点推出的一种标记算法。
在这里插入图片描述

垃圾收集器

在这里插入图片描述

1.Serial收集器

-XX:+UseSerialGC -XX:+UseSerialOldGC
单线程垃圾收集器,在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
新生代采用复制算法,老年代采用标记-整理算法
在这里插入图片描述
它主要有两大作用:
1.在JDK1.5 以及以前的版本中与Parallel Scavenge收集器搭配使用
2.CMS收集器的后备方案。

2.Parallel Scavenge收集器

-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代)
是Serial收集器的多线程版本,默认的收集线程数跟cpu核数相同,当然也可以使用参数(- XX:ParallelGCThreads)指定收集线程数,一般不推荐修改。
关注点是吞吐量(高效率的利用CPU),CMS等垃圾收集器的关注点更多的是用户线程的停 顿时间(提高用户体验)。
新生代采用复制算法,老年代采用标记-整理算法
JDK8默认的新生代和老年代收集器
在这里插入图片描述

3.ParNew收集器

-XX:+UseParNewGC
ParNew收集器跟Parallel收集器很类似,区别是可以和CMS收集器配合使用
新生代采用复制算法,老年代采用标记-整理算法
除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器)配合工作
在这里插入图片描述

4.CMS收集器

-XX:+UseConcMarkSweepGC(old)
是一种以获取最短回收停顿时间为目标的收集器,所以关注点更多的是用户线程的停顿时间(提高用户体验)
使用 标记-清除 算法实现

运行流程

在这里插入图片描述
1.初始标记:暂停所有其他线程(STW),并记录下GC Roots直接能引用的对象,速度很快

2.并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。

3.重新标记:为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短(STW)。主要用到三色标记里的增量更新算法做重新标记

4.并发清理:开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理。

5.并发重置:重置本次GC过程中的标记数据。

特点

1.三色标记
并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。
在这里插入图片描述
黑色:表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
灰色:表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
白色:表示对象尚未被垃圾收集器访问过。若在分析结束阶段, 仍然是白色的对象, 即代表不可达。

2.多标-浮动垃圾
在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过 (被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应回收但没有回收的内存,称之为“浮动垃圾”。
浮动垃圾不会影响垃圾回收的正确性,只是需要等下一轮垃圾回收才被清除。
针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。

3.漏标-读写屏障
漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案: 增量更新 和 原始快照
增量更新(Incremental Update):当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。即黑色对象一旦新插入了指向白色对象的引用后, 它就变回灰色对象,等待重新扫描。
原始快照(Snapshot At The Beginning,SATB):当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根,重新扫描一次,这样就能扫描到白色的对象,直接标记为黑色。

4.记忆集与卡表
为了避免做GCRoots可达性扫描过程中碰到跨代引用的问题(这样效率低),引入的数据结构:
记忆集(Remember Set):在新生代记录从非收集区到收集区的指针集合,避免把整个老年代加入GCRoots扫描范围。
卡表(card Table):Hotspot使用卡表的方式实现记忆集,使用一个字节数组实现:CARD_TABLE[ ],每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡页”。一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0。GC时,只要筛选本收集区的卡表中变脏的元素加入GCRoots里。Hotspot使用写屏障维护卡表状态。一个卡页 2^9大小,即512字节。

优点:

1.并发收集
2.低停顿

缺点:

1.对CPU资源敏感(会和服务抢资源)
2.无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理)
3.使用的"标记-清除"回收算法会导致收集结束时有大量空间碎片产生
4.执行过程中存在不确定性,在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并 发标记和并发清理阶段会出现

5.G1

-XX:+UseG1GC
针对配备多核处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.

运行流程

在这里插入图片描述
1.初始标记(initial mark,STW):暂停所有其他线程,并记录下gc roots直接能引用的对象,速度很快。
2.并发标记(Concurrent Marking):从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。 因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
3.最终标记(Remark,STW):为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短(STW)。主要用到三色标记里的增量更新算法做重新标记。
4.筛选回收(Cleanup,STW):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划。 不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。
比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

特点:

1.堆分配:划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,也可以用参数"-XX:G1HeapRegionSize"手动指定Region大小,但是推荐默认的计算方式。
在这里插入图片描述
默认年轻代堆内存占比是5%,如果堆大小为4096M,年轻代占据200MB左右内存,大概是100个Region,可以通过"-XX:G1NewSizePercent”设置新生代初始占比。JVM会不停的给年轻代增加更多的Region,最多新生代占比不超过60%,可以通过“-XX:G1MaxNewSizePercent”调整。
年轻代分配比例:年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1,假设年轻代有1000个region,eden区对应800个,s0对应100 个,s1对应100个。
Region的区域功能能动态变化:一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能变成老年代。
Humongous区:一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。
2.并行与并发:使用多CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间
3.分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
4.空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于"标记-整理" 算法实现的收集器;从局部上来看是基于“复制”算法实现的。
5.可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"- XX:MaxGCPauseMillis"指定)内完成垃圾收集。如果设置停顿时间太小,时间一长最终会占满堆引发Full GC反而降低性能, 所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒比较合理。

分类:

YoungGC:G1会计算现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills设定的值,那么增加年轻代的region,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC。
MixedGC:老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的 Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区。主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC。正常情况G1的垃圾收集是先做 MixedGC。
Full GC:停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。

适用场景:

1.50%以上的堆被存活对象占用
2.对象分配和晋升的速度变化非常大
3.垃圾回收时间特别长,超过1秒
4.8GB以上的堆内存(建议值)
5.停顿时间是500ms以内

问题:

1.为什么G1用SATB?CMS用增量更新?
SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的根对象会做深度扫描。
G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。

2.G1相对于CMS以及之前的一些垃圾收集器来说,为什么它对大内存的处理能力更强?
大内存的堆一次回收耗费时间可能很长,导致STW时间也会很长,G1提供可预测的停顿功能,这是G1相对于CMS的一大优势,降低停顿时间是G1 和 CMS 共同的关注点。
G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集,这样可以在一定程度上控制一次回收的STW时间。
G1每次在最大停顿时间内根据回收成本并行回收一部分垃圾,整个系统可以在卡顿几乎无感知的情况下一边处理业务一边收集垃圾。相对而言,CMS的STW时间会更卡顿,用户体验感更差。

6.ZGC

-XX:+UseZGC
JDK11中新加入的具有实验性质的低延迟垃圾收集器,使用了读屏障、 颜色指针等技术来实现可并发的标记-整理算法。

运行流程:
在这里插入图片描述
1.并发标记(Concurrent Mark):与G1一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记 (Mark Start)和最终标记(Mark End)也会出现短暂的停顿(STW)。ZGC的标记是在指针上,会更新染色指针中的Marked 0、 Marked 1标志位。
2.并发预备重分配(Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
3.并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的**内存屏障(读屏障)**所截获。根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。只有第一次访问旧对象会变慢,一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于分配新对象,但是转发表还得留着, 因为可能还有访问在使用这个转发表。
4.并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉。

特点:

1.内存布局
基于Region内存布局, 暂时不设分代,使用了读屏障、 颜色指针等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
Region具有大、 中、 小三类容量:
小型Region(Small Region) : 容量固定为2MB, 用于放置小于256KB的小对象。
中型Region(Medium Region) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。
大型Region(Large Region) : 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或 以上的大对象。 每个大型Region只会存放一个大对象, 预示着虽然名字叫作“大型Region”, 但它的实际容量完全有可能小于中型 Region, 最小容量可低至4MB。
在这里插入图片描述
2.NUMA-aware
Non Uniform Memory Access Architecture 非统一内存访问架构。
UMA代表内存只有一块,所有CPU都去访问这一块内存,就会存在竞争问题(争夺内存总线访问权), 有竞争就会有锁,有锁效率就会受到影响,而且CPU核心数越多,竞争就越激烈。
NUMA为每个CPU对应一块内存,且这块内存在主板上离这个CPU是最近的,每个CPU优先访问这块内存,效率自然提高。ZGC能自动感知NUMA架构并充分利用NUMA架构特性。
在这里插入图片描述
3.颜色指针(Colored Pointers)
ZGC的核心设计之一。以前的垃圾回收器的GC信息都保存在对象头中, 而ZGC的GC信息保存在指针中。
在这里插入图片描述
每个对象有一个64位指针,这64位被分为:
18位:预留给以后使用;
1位:Finalizable标识,此位与并发引用处理有关,它表示这个对象只能通过finalizer才能访问;
1位:Remapped标识,设置此位的值后,对象未指向relocation set中(relocation set表示需要GC的 Region集合);
1位:Marked1标识;
1位:Marked0标识,和上面的Marked1都是标记对象用于辅助GC;
42位:对象的地址(所以它可以支持2^42=4T内存)

之所以有两个mark标记,是因为每一个GC周期开始时,会交换使用标记位,使上次GC周期中修正的已标记状态失效,所有引用都变成未标记。
GC周期1:使用mark0, 则周期结束所有引用mark标记都会成为01。
GC周期2:使用mark1, 则期待的mark标记10,所有引用都能被重新标记。 通过对配置ZGC后对象指针分析我们可知,对象指针必须是64位,那么ZGC就无法支持32位操作系统,同样的也就无法支持压缩指针了(CompressedOops,压缩指针也是32位)。

这样的好处在于:
1.一旦某个Region的存活对象被移走后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才清理,这使得理论上只要还有一个空闲Region,ZGC就能完成收集。
2.颜色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,ZGC只使用了读屏障。
3.颜色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数 据,以便日后进一步提高性能。

4.读屏障(Load Barriers)
在标记和移动对象的阶段,每次「从堆里对象的引用类型中读取一个指针」的时候,都需要加上一个Load Barriers。
如果从堆中读取一个被GC移动过的对象,JVM会加上一个读屏障,这个屏障会把读出的指针更新到对象的新地址上,并且把堆里的这个指针“修正”到原本的字段里。
就算GC把对象移动 了,读屏障也会发现并修正指针,于是应用代码就永远都会持有更新后的有效指针,而不需要STW。
利用上面提到的颜色指针,如果指针是Bad Color,程序还不能往下执 行,需要「slow path」,修正指针;如果指针是Good Color,正常往下执行。
判断对象是Bad Color还是Good Color的依据是什么? 就是根据上一段提到的Colored Pointers的4个颜色位。 当加上读屏障时,根据对象指针中这4位的信息,就能知道当前对象是Bad/Good Color。

ZGC触发时机

ZGC目前有4中机制触发GC:
1.定时触发,默认为不使用,可通过ZCollectionInterval参数配置。
2.预热触发,最多三次,在堆内存达到10%、20%、30%时触发,主要是统计GC时间,为其他GC机制使用。
3.分配速率,基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点, 在耗尽之前触发GC(耗尽时间 - 一次GC最大持续时间 - 一次GC检测周期时间)。
4.主动触发,(默认开启,可通过ZProactive参数配置) 距上次GC堆内存增长10%,或超过5分钟时,对比距上 次GC的间隔时间跟(49 * 一次GC的最大持续时间),超过则触发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

paopaodog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值