彻底搞懂GC机制和GC算法

1. 分代

堆中内存分为新生代和老年代,其中新生代又分为Eden区、(Survivor)From区、(Surviver)To区,大致如图:
在这里插入图片描述

2. GC分类

2.1 新生代垃圾回收器:Minor GC/Young GC
2.2 老年代垃圾回收器:Mojor GC/Old GC
2.3 整堆回收:Full GC(回收堆区和方法区

3. 垃圾回收算法

  • 复制算法
    思想:将内存按容量分为两块,每次只使用其中一块,每次将其中一块存活的对象复制到另一块上去,再清空这块区域。这种方式不需要考虑内存碎片,只需要考虑复制的开销;
    注意:对象复制过区之后,对应的引用指针也改变了。这种算法适合新生代,因为新生代对象朝生夕死,需要复制的对象少,效率高。
  • Appel式回收
    思想:优化的复制算法,将新生代分为Eden区、from区、to区,默认比例8:1:1,新对象分配在Eden区,经历一次GC,存活的对象转移到from/to区,之后存活对象在from和to区进行复制算法相同操作,这种策略只浪费10% 的新生代空间。而当from/to区不够时依赖老年代进行分配担保。
  • 标记-清除算法
    思想:将垃圾回收分为标记和清除两个阶段,其中标记是将需要回收的对象进行扫描标记,清除就是对这些垃圾对象进行清除;
    注意:这种算法一般用在老年代,但是会产生大量内存碎片,会触发提前GC。
  • 标记-整理算法
    思想:在标记清除算法的思想上,清除之后对内存空间进行整理,将存活对象向一端移动;
    注意:这种算法效率偏低,而对象移动之后也需要进行对象指针调整。

4.常见的垃圾回收器

记住这张图!!!
在这里插入图片描述
4.1 Serial和Serial Old
古老的单线程垃圾回收器,一个回收新生代,一个回收老年代。
Stop The World:在进行垃圾回收时,会暂停所有的用户线程。

4.2 Parallel Scavange和Parallel Old
jdk1.8默认的多线程垃圾回收器。
参数设置:
停顿时间 -XX:MaxGCPauseMillis;
吞吐量大小 -XX:GCTimeRatio [0,100] 默认值99 ,意为垃圾回收时间99%;
二者是相互平衡受制的。

4.3 ParNew
和CMS配套使用,和Serial没啥区别,就是他是多线程的,停顿时间更少,jdk1.9之后合并到CMS了。

4.4 CMS(Concurrent Mark Sweep)
在这里插入图片描述
第一款并发垃圾回收器
目的:实现停顿时间最短,注重响应速度;
该垃圾回收器在标记清除的思想上,把垃圾回收分为四个步骤
初始标记: 标记GC-Roots能关联的对象(时间短)
并发标记: 和用户线程一起执行,标记GC-Roots引用链的所有对象(时间长)
重新标记: 标记在并发标记过程中导致标记变动的对象(时间短)
并发清除: 和用户线程一起执行,清除不在引用链上的对象(时间长)
参数设置:-XX:+UseConcMarkSweepGC,表示新生代使用ParNew,老年代的用CMS

------------------------CMS存在的问题:--------------------
1. CPU敏感:cms是并发的垃圾回收器,当cpu核心数低于4个时,cms对用户影响较大;
2. 浮动垃圾:在并发清理阶段还会产生新的垃圾,这些垃圾只能在下次GC回收,所以需要预留一部分内存来存放这些浮动垃圾,而万一这部分内存不够的话,就会出现Concurrent Mode Failure,这时候虚拟机就会临时启用Serial Old来替代CMS;
3. 内存碎片:标记清除算法会产生内存碎片,参数
-XX:+UseCMSCompactAtFullCollection默认开启,会进行内存整理,这里一般用Serial Old,所以内存大,对象多时会很卡。

疑问:问什么用标记清除,不用标记整理?
因为标记整理过程中会涉及对象引用的改变,这个过程需要暂停所有用户线程,会延长STW的时间。

5.G1 (Garbege First)
思想:传统的分代回收思想,总逃不过STW的不可预测性,而G1则时将整个堆空间划分为许多个独立的大小相同的Region区域,每个Region区可以根据内存空间需要扮演Eden区、From区、To区和Old区,回收器根据不同的区域采用不同的策略去处理。
在这里插入图片描述

Region:
可以通过参数-XX:G1HeapRegionSize来设置,范围为1M到32M,2的N次幂,一般建议增大该数值,GC间隔会更长点;
每个Region区内有个Humongous区域来专门处理超过半个Region大小的大对象,而超过整个Region大小的超大对象,则会放入N个连续的Humongous Region中,G1将Humongous Region当作老年代的一部分来看待。

G1的运行过程
在这里插入图片描述
初始标记: 标记GCRoots能直接关联到的对象,并且修改TAMS(Top At Mark Start)指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行MinorGC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。

TAMS是啥? 要达到GC与用户线程并发运行,必须要解决回收过程中新对象的分配,所以G1为每一个Region区域设计了两个名为TAMS(TopatMarkStart)的指针,从Region区域划出一部分空间用于记录并发回收过程中的新对象。这样的对象认为它们是存活的,不纳入垃圾回收范围。

并发标记: 从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,并发时有引用变动的对象,这些对象会漏标,漏标的对象会被一个叫做SATB(snapshot- at the beginning)算法(下面细说)来解决。

最终标记: 用于处理并发阶段结后仍遗留下来的最后那少量的SATB记录(漏标对象)。

筛选回收: 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

特点

1. 并行与并发: G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU (CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

2. 分代收集: 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

3. 空间整合: 与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

总结:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.深入

5.1 三色标记法: 在三色标记法之前有一个算法叫Mark-And-Sweep (标记清除)。这个算法会设置一个标志位来记录对象是否被使用。最开始所有的标记位都是0,如果发现对象是可达的就会置为1,一步步下去就会呈现一个类似树状的结果。等标记的步骤完成后,会将未被标记的对象统一清理, 再次把所有的标记位设置成0方便下次清理。
这个算法最大的问题是GC执行期间需要把整个程序完全暂停,不能异步进行GC操作。因为在不同阶段标记清扫法的标志位0和1有不同的含义,那么新增的对象无论标记为什么都有可能意外删除这个对象。对实时性要求高的系统来说,这种需要长时间挂起的标记清除法是不可接受的。所以就需要一个算法来解决GC运行时程序长时间挂起的问题,那就三色标记法。
三色标记最大的好处是可以异步执行,从而可以以中断时间极少的代价或者完全没有中断来进行整个GC。
三色标记法很简单。首先将对象用三种颜色表示,分别是白色、灰色和黑色。
黑色: 根对象,或者该对象与它的子对象都被扫描过。
灰色: 对本身被扫描,但是还没扫描完该对象的子对象。
白色: 未被扫描对象,如果扫描完所有对象之后,最终为白色的为不可达对象,既垃圾对象。

5.2 GC并发情况下的漏标问题

5.2.1 CMS中的解决方案
Incremental Update算法:当一个白色对象被一个黑色对象引用,将黑色对象重新标记为灰色,让垃圾回收器重新扫描;

5.2.2 G1中的解决方案
SATB(snapshot-at-the-beginning): 刚开始做一个快照,当B和C消失的时候要把这个引用推到GC的堆栈,保证C还能被GC扫描到,最重要的是要把这个引用推到GC的堆栈,是灰色对象指向白色的引用,如果一旦某一个引用消失掉了,我会把它放到栈(GC 方法运行时数据也是来自栈中),我其实还是能找到它的,我下回直接扫描他就行了,那样白色就不会漏标。
对应G1的垃圾回收过程中的:
最终标记( Final Marking)
对用户线程做另一个短暂的暂停,用于处理并发阶段结后仍遗留下来的最后那少量的SATB记录(漏标对象)。

5.2.3 对比
SATB算法是关注引用的删除。(B->C的引用)
Incremental Update算法关注引用的增加。(A->C 的引用)
G1如果使用Incremental Update算法,因为变成灰色的成员还要重新扫,重新再来一遍, 效率太低了。
所以G1在处理并发标记的过程比CMS效率要高,这个主要是解决漏标的算法决定的。

5.3 G1中的细节实现

5.3.1 跨代引用
堆空间通常被划分为新生代和老年代。由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,那么回收新生代的话,需要跟踪从老
年代到新生代的所有引用,所以要避免每次YGC时扫描整个老年代,减少开销。

RSet (记忆集)
记录了其他Region中的对象到本Region 的引用,RSet的价值在于使得垃圾收集器不需要扫描整个堆,找到谁引用了当前分区中的对象,只需要扫描RSet即可。
RSet本身就是一个Hash表,如果是在G1的话,则是在一个Region区里面。

CardTable(卡表)
由于做新生代GC时,需要扫描整个OLD区,效率非常低,所以JVM设计了CardTable,如果 一个OLD区CardTable中有对象指向Y区,就将 它设为Dirty .
(标志位1),下次扫描时,只需要扫描CARDTABLE上是Dirty的内存区域即可。
字节数组CARDTABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)。一 般来说,卡页大小
都是以2的N次幂的字节数,假设使用的卡页是2的10次幂,即1K,内存区域的起始地址是0x0000的话,数组CARD_TABLE 的第0、1、2号元素,分别
对应了地址范围为0x0000~0xO3FF、0x0400 ~ 0x07FF、0x0800~0x011FF 的卡页内存。

小结
这里描述的是G1处理跨代引用的细节,其实在CMS中也有类似的处理方式,比如CardTable,也需要记录一个RSet来记录,我们对比一下,在G1中是每
一个Region都需要一个 RSet的内存区域,导致有G1的RSet可能会占据整个堆容量的20%乃至更多。但是CMS只需要一份,所以就内存占用来说,G1
占用的内存需求更大,虽然G1的优点很多,但是我们不推荐在堆空间比较小的情况下使用G1,尤其小于6个G。

安全点与安全区域
安全点
用户线程暂停,GC线程要开始工作,但是要确保用户线程暂停的这行字节码指令是不会导致引用关系的变化。所以JVM会在字节码指令中,选一些指令,作为“安全点”,比如方法调用、循环跳转、异常跳转等,一般是这些指令才会产生安全点。
为什么它叫安全点,是这样的,GC时要暂停业务线程,并不是抢占式中断(立马把业务线程中断)而是主动是中断
主动式中断是设置一个标志,这个标志是中断标志,各业务线程在运行过程中会不停的主动去轮询这个标志,一旦发现中断标志为True,就会在自己最近
的“安全点”上主动中断挂起。
安全区域
为什么需要安全区域?
要是业务线程都不执行( 业务线程处于Sleep或者是Blocked状态),那么程序就没办法进入安全点,对于这种情况,就必须引入安全区域。
安全区域是指能够确保在某一段代码片段之中, 引用关系不会发生变化, 因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区
城看作被扩展拉伸了的安全点。
在这里插入图片描述
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,这段时间里JVM要发起GC就不必去管这个线程了。
当线程要离开安全区域时,它要检查JVM是否已经完成了根节点枚举,或者其他GC中需要暂停用户线程的阶段
1、如果完成了,那线程就当作没事发生过,继续执行。
2、否则它就必须一直等待,直 到收到可以离开安全区域的信号为止。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值