看完不要再说还不懂JVM垃圾回收

  一、什么是垃圾回收

  我们知道,在C/C++中,要想使用创建一个对象,内存由开发者主动申请,并且在对象使用完之后,开发者需要主动释放内存,这样无疑增加了开发者的负担;因此在Java语言中,为了减少开发者对内存的管理负担,无需开发者主动管理内存,JVM会定期清理内存,清理内存的动作叫垃圾回收,并由此衍生出来一系列的垃圾回收算法、垃圾回收器。

  PS:由于JVM种类较多,且各个类型JVM不尽相同,本文只针对于HotSpot VM

  1.1、什么是垃圾

  一个重要的概念,要清理垃圾,首先得知道什么是垃圾,关于这个问题,很多人第一反应是:当一个对象无用的时候就是垃圾,那么问题来了,什么样的对象才是无用对象呢?用一句话总结:当一个对象没有被任何对象引用时,就是垃圾对象。

  二、垃圾标记算法(如何找到垃圾)

  什么是垃圾标记算法呢,我们知道想要清理垃圾对象,首先得找出来哪些是垃圾对象,那么如何找到垃圾对象呢,就得用到垃圾标记算法;从JVM诞生以来,有且仅有以下两种垃圾标记算法:

  Reference count(引用计数法)Root Searching(可达性分析算法)2.1、Reference count(引用计数法)

  如下图所示:Java对象头上维护了一个引用计数器,用来记录当前对象被其他对象引用的个数;如果当前对象引用个数为0时认为是垃圾,立即清理。

  

  维护引用计数器包括两个步骤:

  当有引用指向当前对象时,引用计数器+1当指向当前对象引用断开时,引用计数器-1,同时判断引用计数器是否为0,如果为0立即清理

  优点:

  可立即清理垃圾STW时间短,因为不用扫描全部堆

  缺点(因为其缺陷性,JVM并没有采用这种算法):

  计数器值的增减处理繁重,严重依赖计数器计数器需要占用一定的内存空间,造成内存空间浪费JDK1.2开始增加了多种引用方式:强引用、软引用、弱引用、虚引用,且在不同引用情况下程序应进行不同的操作,只采用一个引用计数器无法准确的区分这么多种引用的情况如下图,循环依赖情况下,即使已经是垃圾对象,但是计数器不为0,导致无法被回收

  2.2、Root Searching(可达性分析算法)

  如下图所示,可达性分析算法是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时,证明此对象没有被任何对象引用,可以被清理。

  

  GC Roots有哪些

  JVM stacknative method stackrun-time constant poolstatic references in method areaClazz三、垃圾回收算法(如何清理垃圾)

  JVM通过垃圾标记算法找到垃圾之后,接下来就需要清理垃圾,那么JVM是通过什么策略来清理垃圾呢?就用到了垃圾回收算法,从JVM诞生以来,有且仅有以下三种垃圾标记算法:

  Mark-Sweep(标记清除)Coping(拷贝)Mark-Compact(标记压缩)3.1、Mark-Sweep(标记清除)

  Mark-Sweep 算法是指,当一个对象被标记为垃圾后,回收线程直接将它所在的内存清空,这样做意味着高效,但同时也会造成内存碎片化。

  优点:高效,只需要清空垃圾对象所对应的内存区域即可,没有额外的操作

  缺点:内存碎片化

  适用场景:适用于垃圾对象占比较少的场景,比如老年代

  

  3.2、Coping(拷贝)

  Coping 算法是指,将内存一分为二,每次只用其中一半,当进行垃圾回收时,把存活对象拷贝到另一块区域,然后直接清空当前区域,如此往复。

  优点:高效,不会造成内存碎片化

  缺点:每次只使用一半内存空间,浪费内存资源

  适用场景:适用于垃圾对象占比较多的场景,比如年轻代

  

  3.3、Mark-Compact(标记压缩)

  前面两种垃圾回收算法,Mark-Sweep 会造成内存碎片化,而 Coping 算法每次只使用一半内存空间,导致资源浪费,因此提出了 Mark-Compact 算法:每次垃圾回收时,将存活对象移动到内存区域的一侧,将另一侧区域直接清空。

  优点:不会造成内存碎片化,节约内存空间

  缺点:由于每次要移动整理存活对象,效率比较低

  适用场景:适用于垃圾对象占比较少的场景,比如老年代

  

  四、JVM堆内存模型

  讲垃圾回收器之前,先来看下JVM堆内存模型,目前JVM的堆内存模型主要有三种:

  物理分代模型逻辑分代&物理分区模型物理分区模型4.1、物理分代模型

  物理分代模型,是将堆分为 新生代 和 老年代 两个区域,新生代又分为Eden、Survivor1(S1)、Survivor2(S2)三个区域,具体如下图:

  

  那么采用分代模型的意义何在呢?得从Java的对象生命历程说起,Java中绝大多数对象从创建到销毁时间很短,例如:一次HTTP请求完成之后,本次请求所创建的一系列对象都已经是垃圾对象了,而一次HTTP请求从请求到响应一般都是毫秒级别的。但也有一小部分存活时间比较久的对象,例如缓存对象;当然也会存在一些从来不会销毁的对象,例如配置类对象。

  基于Java对象的这种特性,将Java分为内存分为新生代和老年代,绝大部分对象会在新生代就被回收掉,少数存活时间长的对象和大对象会进入老年代,如下图,Java对象生命历程可分为几个阶段:

  对象创建时,保存在栈中(逃逸分析、标量替换),则不用GC,因为栈的特性,使用完就POP;对象创建时,保存在堆中:如果为大对象,直接放在Old区域(老年代);如果不是大对象,放Eden区域中;当Eden区空间不足时,启动 Yong GC 回收Eden区域,将存活对象放一个Survivor(暂且叫S1)中,同时清空Eden区域;经过几次 Yong GC 之后,S1区空间不足时,启动 Yong GC 回收S1区域,将存活对象放另一个Survivor(暂且叫S2),同时清空S1区域;经过几次 Yong GC 之后,S2区空间不足时,启动 Yong GC 回收S2区域,将存活对象放S1中,同时清空S2区域;存活对象从S1复制到S2,S2复制到S1,如此往复,存活对象每经历一次Yong GC,年龄加1,当存活对象达到一定年龄时依然存活,下次 Yong GC 会将其放入老年代(PS:年龄大小可通过参数设置,不同垃圾回收器默认年龄不尽相同);接下来看看老年代垃圾回收,当老年代空间不足时,启动 Full GC 清理垃圾对象;因为垃圾对象远远小于存活对象,所以一般采用 Mark-Sweep、Mark-Compact 垃圾回收算法。

  4.2、逻辑分代&物理分区模型

  逻辑分代&物理分区模型,这种模型只在G1垃圾回收器中使用,这种模型是将堆内存分为物理上的一个个的Region,每个 Region 大小1~32MB(默认为1MB),最多不能超过2048个Region;每个Region可以是以下任何一种分代空间,且在同一时间只能是一种分代空间:

  空闲区域EdenSurvivorOldHumongous:对象大小超过Region空间50%,为大对象,且有可能出现跨Region的情况

  4.3、物理分区模型

  物理分区模型,是将堆分为一个个的物理分区,不再有逻辑分区的概念。

  五、垃圾回收器

  JVM 截止目前为止,有以下十种垃圾回收器,左边6种为 物理分代模型;G1为 物理分区&逻辑分代;ZGC、Shenandoah为 物理分区模型;

  左边6种垃圾回收器,其中上面三种工作在年期代,下面三种工作在老年代,虚线相连的两个垃圾回收器可以相互配合使用。

  

  5.1、Serial

  Serial 适用于Client模式下的客户端程序,GC回收线程为单线程,在垃圾回收时,必须暂停所有的工作线程,直到GC线程回收结束(Stop The World,后续简称STW)。

  适用于Client模式下的客户端程序,单线程回收STW工作在新生代,采用 Coping 垃圾回收算法

  5.2、Serial Old

  Serial Old 的GC线程也单线程,在垃圾回收时也需要STW,不同的是Serial Old 工作在老年代,Serial 工作在新生代;另外Serial Old采用的是Mark-Sweep、Mark-Compact 垃圾回收算法。

  单线程回收STW工作在老年代,采用 Mark-Sweep、Mark-Compact 垃圾回收算法

  5.3、Paraller Scavenge

  前面讲了Serial、Serial Old 两种单线程垃圾回收器,在内存很小的时候,对于单个CPU的机器来说,单线程GC由于没有线程交互的开销,专心做垃圾收集可以达到很高的回收效率;随着时代的发展,计算机内存越来越大,单CPU也发展为多核CPU、多CPU机器,这种情况下,单线程垃圾回收器 GC 时间越来越长,导致STW越来越长,因此JVM提出了多线程垃圾回收器。

  Paraller Scavenge 的 GC线程采用多线程,GC线程进行垃圾回收的时候,业务线程同样需要STW,和Serial 相比而言,Paraller Scavenge 效率更高,STW时间更短。

  多线程回收STW工作在新生代,采用 Coping 垃圾回收算法

  5.4、Paraller Old

  Paraller Old 和 Paraller Scavenge 一样,GC线程采用多线程,不同的是 Paraller Old 工作在老年代,采用 Mark-Compact 垃圾回收算法

  多线程回收STW工作在老年代,采用 Mark-Compact 垃圾回收算法

  5.5、ParNew

  ParNew 和 Paraller Scavenge 的算法一模一样,那么JVM为什么要做两个一模一样的垃圾回收器呢?是因为 Paraller Scavenge 不能配合 CMS 使用,因此JVM做了 ParNew 垃圾回收器,配合CMS使用。

  5.6、CMS

  多线程垃圾回收器每次垃圾回收时,必须让业务线程STW,随着计算机内存越来越大,STW时间势必会越来越长,大家可以想一想,如果在秒杀、大促的这种超大流量的情况下,一秒钟的STW会影响多少用户的操作。因此为了缩短STW时间,JVM提出了并发清理的概念,也就是CMS垃圾回收器,旨在减少STW时间。

  CMS 首次使用了并发清理的概念,垃圾清理总体可分为四个阶段(真正是六个阶段,因为其他两个阶段为辅助标记阶段,这里只讲四个重要的阶段):

  初始标记:标记那些和GC ROOT直接连接的对象,存在STW,但是STW时间很短并发标记:GC线程和业务线程同时工作,标记出来哪些是垃圾重新标记:因为并发标记时会造成漏标(三色标记中会讲到),必须重新标记,存在STW并发清理:业务线程工作的同时,GC线程清理垃圾,采用 Mark-Sweep 垃圾回收算法

  下面来看看CMS的特点:

  算法:三色标记 + Incremental-Update + 写屏障低STW停顿,适用于注重服务的响应速度,希望系统停顿时间短并发标记过程中会存在问题:浮动垃圾(在三色标记中会讲到)因为其采用的是 Mark-Sweep 垃圾回收算法,所以会造成内存碎片化如果在并行清理的过程中老年代的空间不足以容纳应用产生的垃圾,则会抛出“concurrent mode failure”,一旦出现这种情况,则必须暂停所有业务线程,启动Full GC开始清理,而CMS Full GC 采用的是Serial Old 单线程回收,因此一旦发生Full GC, 会造成较长时间的停顿。

  造成 “concurrent mode failure” 的原因有三种情况:

  1、浮动垃圾过多;2、内存碎片化;3、业务线程产生对象速度,大于GC清理对象的速度。5.7、G1

  前面讲到的六种垃圾回收器,都是物理分代模型,G1作为一个承上启下的垃圾回收器,首次提出了分区模型,同时保留了分代模型,G1将堆内存分为物理上的一个个的Region,每个 Region 大小1~32MB(默认为1MB),最多不能超过2048个Region;每个Region可以是以下任何一种分代空间,且在同一时间只能是一种分代空间:

  空闲区域EdenSurvivorOldHumongous:对象大小超过Region空间50%,为大对象,且有可能出现跨Region的情况

  G1的特点:

  Jdk9默认为G1,逻辑分代&物理分区可管理上百G内存G1 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。算法:垃圾回收算法:三色标记 + SATB + 写屏障

  G1 有哪些 GC 类型?

  Yong GC:当Eden区空间不足,多线程执行Mixed GC:默认当老年代占整个堆大小的超过45%,触发MixedGC,回收所有年轻代Region + 部分老年代Region,整个回收过程大致分为四个阶段(真正是六个阶段,因为其他两个阶段为辅助标记阶段,这里只讲四个重要的阶段): 初始标记:标记那些和GC ROOT直接连接的对象,存在STW,但是STW时间很短 并发标记:GC线程和业务线程同时工作,标记出来哪些是垃圾 最终标记:因为并发标记会造成漏标(三色标记中会讲到),必须重新标记,存在STW 筛选回收:回收所有年轻代Region + 部分老年代Region,为什么是部分老年代Region呢?是因为这个过程中会产生STW,G1为了最大概率满足GC停顿时间的要求,会优先回收那些满足回收条件,且STW时间不超过最大停顿时间的老年代RegionFull GC:Old区空间不足时,会触发Full GC,G1 Full GC 采用的是 Serial 单线程回收,因此一旦产生Full GC,就会造成较长时间的STW停顿。可通过调低Mixed GC触发阈值,让Mixed GC尽早回收来解决。

  G1 的 GC 回收过程如下图所示:

  

  G1如何保证极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征?

  G1默认最大停顿时间为200ms,通过分而治之的理念,优先处理符合回收条件的一个或者几个Region,最大可能保证不超过最大停顿时间;G1新老年代比例5% ~ 60%之间,G1通过新老年代比例,可以预测某次垃圾回收的STW停顿时间,并根据STW时间的长短,动态地调整新老年代比例,以极高概率满足GC停顿时间要求的同时,尽量减少GC的次数,提高吞吐量性能(不建议手动指定新老年代比例,如果手动指定后,G1将不会动态调整新老年代比例);利用CSet、RSet避免全堆扫描,关于CSet、RSet在后面会讲到。5.8、ZGC & Shenandoah

  ZGC

  为分区模型,彻底丢掉了分代模型可以管理4T内存jdk11开始支持ZGC算法:颜色指针 + 读屏障

  Shenandoah

  为分区模型,彻底丢掉了分代模型可以管理4T内存目前大部分只用于openjdkjdk12开始可以用Shenandoah算法:颜色指正 + 写屏障5.9、Epsilon

  比较特殊的一个垃圾回收器,Epsilon是一个没有任何垃圾回收的垃圾回收器,主要用于调试代码。

  六、几个比较重要的数据结构6.1、Card Table

  首先来看一个场景:在分代模型的情况下,A对象在年轻代,B对象在老年代,A对象被B对象引用,这种情况下,YongGC时,想用可达性分析算法扫描A是否被引用,会发生什么情况呢?

  

  上面这种情况,如果想知道A对象是否被引用,必须要扫描整个老年代,才能知道A被哪些对象引用,大家想想,一次YongGC要扫描整个堆区域,这是一件效率极其低下的动作,因此针对这个问题,JVM提出了Card Table的概念,如下图所示:

  

  所谓的Card Table,是把老年代区域分为一个个的Card,每个Card 为512个字节;那么这么多Card怎么管理起来呢,就用到Card Table,Card Table是一个简单字节数组,每一个数组项对应一个Card,数组里面的记录对应Card的状态,即:如果当前Card里面有对象引用了年轻代的对象,将当前Card标记为Dirty。

  接下来我们看看借助Card Table,YongGC时,想用可达性分析算法扫描A是否被引用,会怎么操作呢:通过 GC ROOT 扫描到B、D两个对象,查看B、D对象所对应Card是否Dirty,发现D对象对应的Card不是Dirty,说明D对象所在的Card里面没有对象引用年轻代对象,所以放弃对D所在的Card的扫描。而B对象所在Card为Dirty,所以我们只用扫描B对象所在的Card区域;相对于扫描整个堆区域,YongGC借助CardTable效率提高了不止一点点。

  6.2、Collection Set(CSet)

  首先来考虑一个问题,G1在MixedGC收时,为了最大概率保证不超过最长STW时间,会优先回收那些满足回收条件,且STW时间不超过最大停顿时间的老年代Region,那么G1怎么知道哪些Region满足回收条件呢?这就用到了Collection Set,简称CSet。

  CSet是一组可被回收的Region集合,G1清理垃圾时,会从CSet挑选一个或多个满足最大STW时间的Region进行清理;CSet的分区可来自:Eden、Survivor、Old区;在CSet中存活的对象,在一次GC过程中会被移动到另一个可用空间。

  

  6.3、Remember Set(RSet)

  先来看一个场景,如下图:在G1中,GC ROOT 指向B对象,B对象引用了A对象,其中B和A分别属于两个不同的Region,根据可达性分析算法,想要知道A是否被引用,必须要扫描所有的Region;全堆扫描是一个效率极其低下的动作,因此G1提出了RSet的概念:

  

  所谓的RSet,是在每个Region里,拿出来一小块区域,专门记录哪些Region引用了当前Region的对象;RSet是个哈希表,key记录了对象所在Region,Value记录了对象所在Card(这里的Card 和 Card Table里面的Card一模一样),具体如下图:

  

  那么我们再来看下,借助RSet如何知道A对象是否被引用,首先当前RSet是否有数据,如果RSet为空,表示当前Region的对象没有被其他Region对象引用过,那么只需要利用可达性分析算法扫描当前区域,就可以知道A有没有被引用; 如果RSet不为空,则只需要扫描当前Region,和RSet里面的Region 下的某个Card区域。

  显而易见,RSet 和 Card Table 具有类似的功能,就是避免在标记垃圾时进行全堆扫描操作。

  七、三色标记算法

  我们在前面讲垃圾回收器时,CMS 和 G1 用到了 三色标记算法,其中 CMS 使用的算法是:三色标记 + Incremental-Update + 写屏障,而 G1 使用的算法是:三色标记 + SATB + 写屏障,那么到底什么是三色标记?什么是Incremental-Update?什么是SATB?CMS 和 G1 为什么采用不同的算法?接下来我们带着问题来看看三色标记算法到底是什么。

  7.1、什么是三色标记算法

  三色标记算法是指,利用可达性分析算法,对象被GC线程扫描标记的状态:

  白色:还未扫描到的对象灰色:对象自身已经扫描标记完成,孩子还未扫描标记完成黑色:对象自身扫描标记完成,孩子扫描标记完成

  如下图:A为黑色,说明A本身已经扫描标记完成,并且A的孩子B也已经扫描标记完成;B为灰色,说明B本身扫描完成,B的孩子还未扫描完成;C为白色,说明C还未扫描到。

  三色标记算法一次扫描标记完成之后,如果没有其他操作干扰(如:写屏障),则只会存在黑色、白色两种状态,黑色表示能存活对象,白色表示垃圾对象。

  

  前面讲到三色标记算法,用于CMS、G1两种垃圾回收器中,而这两种垃圾回收器都是并发标记,也就是说GC线程和业务线程同时工作,在并发标记中,三色标记算法会存在两个缺陷:浮动垃圾、漏标。

  7.2、三色标记漏洞 - 浮动垃圾

  浮动垃圾是什么?是指那些已经是垃圾的对象,却在一次GC回收过程中存活了下来。那么什么情况下会产生浮动垃圾呢,请看下图:

  第一步:GC线程:A 已经完全标记,B 已经完成自身标记,正在标记C;第二步:业务线程:将A -> B 的引用失效;第三步:GC线程:因为B自身已经扫描完成,所以感知不到 A -> B 的引用失效,待把C、D标记完成后,将B设置为黑色。

  其实B、C、D都已经是垃圾对象,但是本次三色标记被标记成了黑色,黑色代表存活对象,所以本次GC不会将B、C、D回收掉,B、C、D就是浮动垃圾。

  浮动垃圾会造成什么后果呢?其实浮动垃圾对GC的影响并不是很大,只不过是让垃圾对象多存活一段时间,在下次GC的时候,一定会将这些浮动垃圾清理掉,因此JVM里并没有针对浮动垃圾做任何操作。

  7.3、三色标记漏洞 - 漏标

  漏标是什么?是指那些本该存活的对象,在一次GC回收过程中却被当做垃圾对象回收了。

  产生漏标需要两个必要条件,缺一不可:

  黑色对象 -> 白色对象建立链接灰色对象 -> 白色对象引用断开

  如下图,结合具体例子来看看,产生漏标的过程:

  第一步:GC线程:A 已经完全标记,B 已经完成自身标记,正在标记 C第二步:业务线程:A -> D 新建了引用关系,同时 B -> D 的引用失效第三步:GC线程:因为A已经扫描完成,所以未感知到 A -> D 的建立引用,认为没有任何引用指向D,D漏标被回收

  漏标会造成非常严重的问题,如图所示,当顺着 A -> D 的指针,去找B对象,结果发现B对象不存在返回NULL,这不就是NullPointerException吗;那么如何避免漏标呢,前面说到漏标有两个缺一不可的必要条件,那么我们只需要在两个产生条件上下做控制,就可以避免漏标的产生:

  黑色对象 -> 白色对象建立链接时,通过写屏障将黑色对象标记为灰色,灰色对象需要重新标记,这种方法叫 Incremental-Update(增量更新);灰色对象 -> 白色对象引用断开,先将该引用原始快照保存下来,等到重新标记阶段将该引用取出来,重新扫描白色对象是否被引用,这种方法叫做 SATB(Snapshot-At-The-Beginning)。7.4、三色标记漏标解决方案

  上面提到了三色标记漏标的两种解决方案,Incremental-Update、SATB,下面我们就详细讲讲这两种方案的具体流程。

  7.4.1、Incremental-Update(增量更新)

  CMS 中使用 Incremental-Update 来解决漏标的问题,具体操作如下:

  第一步:GC线程:A 已经完全标记,B 已经完成自身标记,正在标记C第二步:业务线程:A -> D 新建了引用关系,利用写屏障将A重新标记为灰色(注意:这里的写屏障,并不是指内存屏障,是指类似切面编程的理念,不改变原有逻辑的情况下,将A标记为灰色)第三步:GC线程:A变为灰色,需要重新标记

  7.4.2、SATB(原始快照)

  G1 中使用 SATB 来解决漏标问题,具体操作如下:

  第一步:GC线程:A 已经完全标记,B 已经完成自身标记,正在标记C第二步:业务线程:同时 B -> D 引用断开,利用写屏障将 B -> D 的引用原始快照记录下来第三步:在重新标记阶段,将B -> D 的引用原始快照拿出来,重新扫描D是否被引用。

  7.5、后记 - G1为何选择SATB

  为什么G1会选择SATB来解决漏标问题呢,我们知道:当灰色对象-> 白色对象引用消失时,该引用原始快照会被记录下来,下次扫描时会拿到该引用,检查白色对象是否有引用指向它,这是时候配合RSet,无需遍历整个堆,只需要遍历当前Region,和当前Region的Rset指向的那些Region区域,即可知道有没有引用指向它,时间开销极小。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值