1.7 垃圾回收

1.7.1 判断对象死亡

(垃圾回收机制的G1里面说法太多,本文为一些博客总结处理,存在个人理解和错误,多多包涵)

引⽤计数法(主要在redis中使用)

可达性分析算法

作为GC Roots 的对象:

  1. 虚拟机栈:帧中的本地变量表引用的对象

  2. native方法引用的对象

  3. 方法区中的静态变量和常量引用的对象

1.7.2 强引用,软引用,弱引用,虚引用

  1. 强引用不可回收
  2. 软引用一般用作缓冲,内存足够就不会回收
  3. 弱引用一发现就会回收(ThreadLoacal中使用)
  4. 虚引用用于跟踪对象被垃圾回收的过程

1.7.2 回收一个类

  1. 所有的实例都被回收
  2. 加载类的 ClassLoader 已经被回收
  3. 对应的 Class 对象没有在其他地方被使用

1.7.3 垃圾回收算法

  1. 标记清除算法
  2. 复制算法
  3. 标记整理算法
  4. 分代算法

1.7.4 垃圾回收器

image.png

JDK8默认 ps+po

1. 串行收集器

概念

使用单线程进行垃圾回收的收集器,每次回收时,串行收集器只有一个工作线程,对于并行能力较弱的计算机来说,串行收集器的专注性和独占性往往有更好的性能表现。串行收集器可以在新生代和老年代中使用,根据作用于不同的堆空间,分为新生代串行收集器和老年代收集器。

-XX:+UseSerialGC :年轻串行(Serial),老年串行(Serial Old)

Serial收集器:

Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

image.png

Serial Old收集器

1、Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。
2、主要意义也是在于给Client模式下的虚拟机使用。
3、如果在Server模式下,那么它主要还有两大用途:
一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用
另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

2. 并行收集器

概念

ParNew收集器

1、Serial收集器的多线程版本

2、单CPU不如Serial,因为存在线程交互的开销

-XX:+UseParNewGC 新生代并行(ParNew),老年代串行(Serial Old)

-XX:ParallelGCThreads=n 设置并行收集器收集时使用的CPU数。并行收集线程数。一般最好和计算机的CPU相当

20181217150839544.png

Parallel Scavenge收集器

-XX:+UseParallelGC 新生代使用并行回收收集器,老年代使用串行收集器

1、吞吐量优先”收集器

2、新生代收集器,复制算法,并行的多线程收集器

3、目标是达到一个可控制的吞吐量(Throughput)。

4、吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

5、两个参数用于精确控制吞吐量:

-XX:MaxGCPauseMillis 是控制最大垃圾收集停顿时间

-XX:GCTimeRatio 直接设置吞吐量大小

-XX:+UseAdaptiveSizePolicy 动态设置新生代大小、Eden与Survivor区的比例、晋升老年代对象年龄

6、并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

7、并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

Parallel Old收集器

-XX:+UseParallelOldGC 新生代和老年代都使用并行回收收集器

1、Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

2、**在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

CMS收集器

1、以获取最短回收停顿时间为目标的收集器

2、非常符合互联网站或者B/S系统的服务端上,重视服务的响应速度,希望系统停顿时间最短的应用

3、基于**“标记—清除”**算法实现的

4、CMS收集器的内存回收过程是与用户线程一起并发执行的

5、它的运作过程分为4个步骤,包括:

初始标记,“Stop The World”,只是标记一下GC Roots能直接关联到的对象,速度很快

并发标记,并发标记阶段就是进行GC RootsTracing的过程

重新标记,Stop The World”,是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,但远比并发标记的时间短

并发清除(CMS concurrent sweep)
6、优点:并发收集、低停顿
7、缺点:

对CPU资源非常敏感。
无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。


一款基于“标记—清除”算法实现的收集器

-XX:+UseConcMarkSweepGC 应用CMS收集器

-XX:ConcGCThreads 设置并发线程数量

-XX:CMSInitiatingOccupancyFraction 设置当老年代空间实用率达到百分比值时进行一次cms回收,默认为68,当老年代的空间使用率达到68%的时候,会执行CMS回收

如果内存使用率增长的很快,在CMS执行的过程中,已经出现了内存不足的情况,此时CMS回收就会失败,虚拟机将启动老年代串行回收器进行垃圾回收,这回导致应用程序中断,直到垃圾回收完成后才会正常工作,这个过程GC的停顿时间可能较长,所以该值需要根据实际情况设置。

-XX:+UseCMSCompactAtFullCollection 设置cms在垃圾收集完成后进行一次内存碎片整理

-XX:CMSFullGCsBeforeCompaction 设定进行多少次cms回收后,进行一次内存压缩

3. G1(Garbage-First)收集器

1、当今收集器技术发展的最前沿成果之一

2、G1是一款面向服务端应用的垃圾收集器。

3、优点:

并行与并发:充分利用多CPU、多核环境下的硬件优势

分代收集:不需要其他收集器配合就能独立管理整个GC堆

空间整合:**“标记—整理”算法实现的收集器,局部上基于“复制”算法不会产生内存空间碎片**

可预测的停顿:**能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒**
复制代码

4、G1收集器的运作大致可划分为以下几个步骤:

初始标记:标记一下GC Roots能直接关联到的对象,需要停顿线程,但耗时很短

并发标记:是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行

最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录

筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划
复制代码

-XX:+UserG1Gc 应用G1收集器

-XX:MaxGCPauseMillis 指定最大停顿时间

-XX:ParallelGCThreads 设置并行回收的线程数量

20181217150905429.png

3.1 G1 的特点

v2-23df74e33f2b584d0254665628386b95_720w.jpg

1 分代和分区

每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。 年轻代、幸存区、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑,在物理上不需要连续,则带来了额外的好处——有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾,这也就是G1名字的由来,即首先收集垃圾最多的分区

对于新生代来说,并不是使用了这种算法,依然是新生代满了一起进行回收,整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小。

G1还是一种带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩。每个分区的大小从1M到32M不等,但是都是2的冥次方。

2 Cset 和 Rset

image-20210624165540792.png

收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。

在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。

RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。

RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可。

这里的个人理解:为什么需要G1 需要rset : 因为收集的是Cset里面里面的对象,需要知道Cset里面的对象有没有被谁引用(如果被引用,肯定不能被回收),所以需要存在谁引用了我的指针来表示

如下图所示,Region1和Region3中的对象都引用了Region2中的对象,因此在Region2的RSet中记录了这两个引用。

Region1 引用了 region2 ,把 region1 的卡表信息,,记录到 region2 的卡表类,当这时候需要回收 region2的时候, 可以直接定位到 region1 的某个位置(entry 对应的位置),,避免了对于整个 region1 的扫描

一个region 对应多个 entry

image-20210624165717817.png

上图中有三个Region,每个Region被分成了多个Card,在不同Region中的Card会相互引用,Region1中的Card中的对象引用了Region2中的Card中的对象,蓝色实线表示的就是points-out的关系,而在Region2的RSet中,记录了Region1的Card,即红色虚线表示的关系,这就是points-into。 而维系RSet中的引用关系靠post-write barrier和Concurrent refinement threads来维护。

post-write barrier记录了跨Region的引用更新,更新日志缓冲区则记录了那些包含更新引用的Cards。一旦缓冲区满了,Post-write barrier就停止服务了,会由Concurrent refinement threads处理这些缓冲区日志。 RSet究竟是怎么辅助GC的呢?在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量。

GC时GC Root的对象只可能在两个位置:CSet里的Region和非CSet里的Region,CSet是需要收集的region集合而非CSet里的region不需要收集,如果GC Root对象在CSet的Region里,依次遍历可达对象即可;如果GC Root在非CSet里,就需要依次遍历从非CSet到CSet里对象的引用即扫描所有非CSet region。非常耗时,如果有了RSet,通过扫描CSet里所有Region的RSet就能知道不参与收集的其他Region对CSet中对象的引用,避免了全局扫描这些不参与收集的Region

总结下 Cset Rset CardTable

Cset 表示需要收集的Region,由于 G1是垃圾优先,所以Cset里面可以理解为是一些收集性价比最高的区间,收集的时候,由于需要知道谁引用了我,所以存在的是Rset,用于表示谁引用了我,避免扫描整个堆空间,Rset记录的是Cardtable,的位置,可以定位到某个对象所在的区的entry。(个人理解,对于垃圾回收器,资料太多太杂,很多错误夹杂其中,很多博客说法都是不同的,除非去看源码,基本上没法理解都是有点问题)

3. 卡表

image.png

一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0.

GC时,只要筛选本收集区的卡表中变脏的元素加入GCRoots里。

卡表的维护

Hotspot使用写屏障维护卡表状态。

在G1 堆中,存在一个CardTable的数据,CardTable 是由元素为1B的数组来实现的,数组里的元素称之为卡片/卡页(Page)。这个CardTable会映射到整个堆的空间,每个卡片会对应堆中的512B空间。

如下图所示,在一个大小为1 GB的堆下,那么CardTable的长度为2097151 (1GB / 512B);每个Region 大小为1 MB,每个Region都会对应2048个Card Page。

image.png

4. STAB技术

下面一起介绍

5. 写屏障

当引用发生改变的时候,会见 引用对象的 卡表对应的卡片进行 进行更新, 把卡片变成 dirty, 同时 把卡片 压入 一个队列中,等待后续进行 更新到 Rset 中

image.png

image.png

3.2 young GC

image.png

image.png

image.png

3.3 MixGC

image.png

  • 初始标记(initial-mark),在这个阶段,应用会经历STW,通常初始标记阶段会跟一次新生代收集一起进行,换句话说——既然这两个阶段都需要暂停应用,G1 GC就重用了新生代收集来完成初始标记的工作。在新生代垃圾收集中进行初始标记的工作,会让停顿时间稍微长一点,并且会增加CPU的开销。初始标记做的工作是设置两个TAMS变量(NTAMS和PTAMS)的值,所有在TAMS之上的对象在这个并发周期内会被识别为隐式存活对象;
  • 根分区扫描(root-region-scan),这个过程不需要暂停应用,在初始标记或新生代收集中被拷贝到survivor分区的对象,都需要被看做是根,这个阶段G1开始扫描survivor分区,所有被survivor分区所引用的对象都会被扫描到并将被标记。survivor分区就是根分区,正因为这个,该阶段不能发生新生代收集,如果扫描根分区时,新生代的空间恰好用尽,新生代垃圾收集必须等待根分区扫描结束才能完成。如果在日志中发现根分区扫描和新生代收集的日志交替出现,就说明当前应用需要调优。
  • 并发标记阶段(concurrent-mark),并发标记阶段是多线程的,我们可以通过-XX:ConcGCThreads来设置并发线程数,默认情况下,G1垃圾收集器会将这个线程总数设置为并行垃圾线程数(-XX:ParallelGCThreads)的四分之一;并发标记会利用trace算法找到所有活着的对象,并记录在一个bitmap中,因为在TAMS之上的对象都被视为隐式存活,因此我们只需要遍历那些在TAMS之下的;记录在标记的时候发生的引用改变,SATB的思路是在开始的时候设置一个快照,然后假定这个快照不改变,根据这个快照去进行trace,这时候如果某个对象的引用发生变化,就需要通过pre-write barrier logs将该对象的旧的值记录在一个SATB缓冲区中,如果这个缓冲区满了,就把它加到一个全局的列表中——G1会有并发标记的线程定期去处理这个全局列表。
  • 重新标记阶段(remarking),重新标记阶段是最后一个标记阶段,需要暂停整个应用,G1垃圾收集器会处理掉剩下的SATB日志缓冲区和所有更新的引用,同时G1垃圾收集器还会找出所有未被标记的存活对象。这个阶段还会负责引用处理等工作。
  • 清理阶段(cleanup),清理阶段真正回收的内存很小,截止到这个阶段,G1垃圾收集器主要是标记处哪些老年代分区可以回收,将老年代按照它们的存活度(liveness)从小到大排列。这个过程还会做几个事情:识别出所有空闲的分区、RSet梳理、将不用的类从metaspace中卸载、回收巨型对象等等。识别出每个分区里存活的对象有个好处是在遇到一个完全空闲的分区时,它的RSet可以立即被清理,同时这个分区可以立刻被回收并释放到空闲队列中,而不需要再放入CSet等待混合收集阶段回收;梳理RSet有助于发现无用的引用。

3.4 具体过程

需要注意的是 youngGC 会发送在Mix GC的过程中

image.png

3.5 巨型对象

巨型对象:在G1中,如果一个对象的大小超过分区大小的一半,该对象就被定义为巨型对象(Humongous Object)。巨型对象时直接分配到老年代分区,如果一个对象的大小超过一个分区的大小,那么会直接在老年代分配两个连续的分区来存放该巨型对象。巨型分区一定是连续的,分配之后也不会被移动——没啥益处。

由于巨型对象的存在,G1的堆中的分区就分成了三种类型:新生代分区、老年代分区和巨型分区,如下图所示:

image.png

1.7.5 三色标记法

三色标记法,主要用于 CMS 和 G1 在并发标记阶段判定对象是否存活的问题

白色:表示对象尚未被垃圾回收器访问过。显然,在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。

黑色:表示对象已经被垃圾回收器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其它的对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。

灰色:表示对象已经被垃圾回收器访问过,但这个对象至少存在一个引用还没有被扫描过

image.png

1.并发标记主要存在的问题

如上图所示 ,
B–>C的连接断了,这时候,产生了A–C的连接,这时候,C会被当成垃圾回收掉
而实际上 C是不能回收的

由此引出了两种方案

2. 增量更新

当B-》C 断开,A-》C连上的时候, 将黑色的A 变成灰色的,A需要重写被扫描(CMS的重新扫描阶段解决的就是这个)

3. SATB

当B-》C 断开,A-》C连上的时候, 将黑色的 C 变成灰色的,这样子C就不会被回收了,如果C是没有用的,那么C就是浮动垃圾,下一次被回收,但是这一次肯定不会被回收

image.png

4. 总结下

增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。

原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)

SATB, 就是 记录下当时的引用状态, 让灰色的可以重新触碰到 白色的 把白色的变成黑色的 活过当前的gc

CMS 中 是把黑色的变成灰色的()

G1 中 是把 白色的变成 黑色的

5. 相关问题

为什么G1用SATB?CMS用增量更新?

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

补充

image-20210511213815753.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享