JVM专题之垃圾回收器

一 如何寻找垃圾对象

1.1 引用计数法

每一个对象有一个计数器,当被别的对象引用的时候,则会递增计数器;当别人释放或者引用失效的时候则递减计数器,如果在垃圾回收的时候,引用计数器为0,则表示这个对象没有被引用,是垃圾对象。

优点: 简单
缺点: 循环引用,导致对方计数器都不是0

1.2 可达性分析法

确定一些对象作为GC Roots根节点对象,从这些根节点对象为出发点,沿着引用链向下递归,当一个对象到GC Roots没有任何引用链,则属于垃圾对象,可以回收。但是,哪些对象可以作为GC Roots呢?
第一:虚拟机栈帧中指向堆中对象的引用

public void foo() {
	Configuration conf = new Configuration();
}

第二:本地方法栈中引用的对象
第三:堆中静态变量指向的堆中的对象或者指向的字符串常量池中的引用
比如:

private static String text = "hello,world";
private static Address address = new Address();

第四:堆中指向字符串常量池中
比如:

private static Address address = new Address();

在这里插入图片描述

二 垃圾回收算法

2.1 标记-清除算法(Mark-Sweep)

在这里插入图片描述

#1 标记要回收的对象
#2 启动一个线程来清除这些对象

缺点: 带来内存碎片问题

2.2 复制算法(Copying)

在这里插入图片描述

堆内存等分为2个部分,一部分用于存放对象,一部分空着;当发生GC的时候将存放对象部分还活着的对象,拷贝到另一个空着的部分,然后将当前部分置空,就这样如此往复。
如果存活对象较多,那么需要复制的存活对象就多;如果垃圾对象多,那么需要复制的存活对象就少。所以比较适合新生代。

#1 堆内存分为2块,每次只使用其中一块
#2 将存活的对象复制到另一块上去,然后指针移到另外一块内存,进行操作
#3 对已经使用那块内存进行清理

优点:解决了内存碎片问题
缺点:内存利用率太低,给内存空间带来了浪费

2.3 标记-压缩算法(Mark-Compact)

在这里插入图片描述

#1 标记需要被回收的对象
#2 将存活的对象压缩到或者尽量移到内存的另一端(会存在放不下吗?,肯定不会)
#3 清理边界之外的所有空间
这样,即能避免内存使用的浪费,有没有内存碎片的产生,性价比比较高。

2.4 分代算法(Generation)

根据内存区域对象的不同特点,比如有的存活时间长,有的短等,将内存区域分为几块,每一块根据对象特点使用不同垃圾回收算法。比如新生代的对象一般存活时间较短,GC频繁,适合复制算法,标记算法相对来说效率没有复制算法高;但是老年代的对象存活时间很长,如果使用复制算法,就有可能会复制大量的对象,这时候可以对老年代使用标记压缩算法。

三 垃圾回收器

3.1 新生代垃圾回收器

3.1.1 Serial GC

在这里插入图片描述

#1 所有的应用线程全部停掉,即所谓的stop-all-world
#2 启动一个单线程开始进行垃圾回收
#3 使用复制算法进行垃圾回收
#4 通过-XX:+UseSerialGC开启,则年轻代使用Serial GC,老年代使用Serial Old GC

3.1.2 ParNew GC

它跟Serial工作方式差不多,只不过是Serial回收器的多线程版本.
在这里插入图片描述

#1 所有的应用线程全部停掉,即所谓的stop-all-world
#2 启动多个线程开始进行垃圾回收
#3 使用复制算法进行垃圾回收
#4 通过-XX:+UseParNewGC或者-XX:+UseConcMarkSweepGC年轻代启用ParNew GC。但是老年代他们俩是不一样的,
-XX:+UseParNewGC老年代使用Serial Old GC, -XX:+UseConcMarkSweepGC老年代使用CMS GC
#5 -XX:ParallelGCThreads指定线程数,一般最好与CPU数量相当,默认情况下,当CPU 个数小于8的时候,线程数为CPU个数;如果大于8,则(5 *CPU个数)/8+3,比如 8核就是8,16核就是13。
#6 ParNew只有年轻代回收器,所以它需要Serial Old GC回收老年代垃圾

3.1.3 Parallel GC

在这里插入图片描述![在这里插入图片描述](https://img-blog.csdnimg.cn/20210425150119560.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3poYW5nbGgwNDY=,size_16,color_FFFFFF,t_70

#1 所有的应用线程全部停掉,即所谓的stop-all-world
#2 启动多个线程开始进行垃圾回收
#3 使用复制算法进行垃圾回收
#4 通过-XX:+UseParallelGC或者-XX:+UseParallelOldGC参数启用Parallel GC。但是老年代他们俩是不一样的,-XX:+UseParallelGC老年代使用Serial Old GC,-XX:+UseParallelOldGC老年代使用Parallel Old GC
#5 通过参数-XX:MaxGCPauseMills,设置GC最大暂停时间,如果垃圾比较多,在暂停时间内没有回收完,那么有可能触发多次GC,所以这个参数最要不谨慎使用,最好不用。通过参数-XX:GCTimeRatio可以设置某个时间段内垃圾回收时间占比,值是0-100内的整数,假设GCTimeRatio=N,N表示应用程序执行时间,那么系统将花费不超过1/(1+N)的时间用于垃圾回收,比如N=19,在一个小时时间段,垃圾回收的时间不应该超过 60 * 1/(1+19) = 5,即1个小时垃圾回收时间不应该超过5分钟。
#6 和ParNew GC工作流程差不多,只不过它更加关注吞吐量而已,比如控制GC暂停时间或者GC时间占比的问题
注意: Parallel GC最好不使用这种方式进行垃圾回收。

3.2 老年代垃圾回收器

3.2.1 Serial Old GC

#1 所有的应用线程全部停掉,即所谓的stop-all-world
#2 启动一个单线程开始进行垃圾回收
#3 使用标记压缩算法进行垃圾回收
#4 通过-XX:+UseSerialGC或者-XX:+UseParNewGC或者-XX:+UseParallelGC开启

3.2.2 Parallel Old GC

#1 所有的应用线程全部停掉,即所谓的stop-all-world
#2 启动多个线程开始进行垃圾回收
#3 使用标记压缩算法进行垃圾回收
#4 通过-XX:+UseParallelOldGC参数启用

3.2.3 CMS(Concurrent Mark-Sweep) 并发标记清除算法

3.2.3.1 CMS简述和工作原理
3.2.3.1.1 什么CMS? CMS出现的背景是什么?

CMS是一个基于标记-清除算法的实现的垃圾收集器。主要目的是为了避免在回收老年代的时候长时间的暂停。
我们知道其他分戴老年代垃圾回收器,比如Serial Old GC和Parallel Old GC使用的是标记-压缩算法,虽然算法效率不错,但是在收集垃圾阶段会暂停所有应用线程,如果内存大一点的应用程序,可能暂停时间有点长。所以为了减少在收集阶段暂停时间长的问题,出现了CMS。

3.2.3.1.2 CMS工作原理

CMS是怎么解决传统GC暂停时间过长的问题?
在CMS垃圾回收时候,有多个阶段是和应用程序并发执行,即便有暂停应用线程的阶段,那都是暂停时间很短的操作。

CMS如何解决不应该回收的对象被回收?
那既然和应用线程并发执行,那么应用线程完全有可能修改对象的引用,可能会造成本不应该回收的对象被回收。所以我们需要一种机制,知道哪些对象的引用发生了修改,然后需要对它引用链重新跟踪,并进行标记。但是如何知道哪些对象被修改了呢?

CMS引入了Card 、Card Table数据结构,以及使用写屏障技术实现了这一点。将内存划分为多个Card, 分配的对象就在Card中,并且维护了一个全局的Card Table状态表;另外通过写屏障,使得每次对象发生引用修改,则将对象所在Card在Card Table中标记为dirty,在某一个阶段遍历Card Table, 找到所有dirty card, 跟踪dirty card中的对象,重新标记。

3.2.3.2 Card 和 Card Table

在标记老年代存活对象的时候,因为是和应用线程并发执行的,所以标记完了之后,完全有可能被应用线程修改了引用,导致一些本不应该回收的对象被回收了。所以CMS使用Card 和Card Table以及写屏障来解决这个问题。
CMS 将整个老年代空间分为多个Card,每一个Card代表一个512字节的内存块,分配的对象会按照内存块进行分配。然后通过Card Table来维护每一个Card的状态,因为一个Card可能里面分配多个对象,如果当这个Card内的某个对象发生了引用改变,则会在Card Table中对应的Card状态置为dirty,表示这个Card已经有对象发生引用改变了。
在重新标记的时候会扫描整个Card Table,遍历那些状态为dirty的Card所有对象,如果可达就进行标记,遍历完之后,将这个Card状态置为clean。
在这里插入图片描述

3.2.3.3 CMS触发时机

CMS可以通过参数-XX:CMSInitiatingOccupancyFraction来确定什么时候开始进行CMS垃圾回收。-XX:CMSInitiatingOccupancyFraction默认值是68,即表示当老年代的堆使用率达到68%会执行一次CMS回收。

注意:如果内存增长很快,可能CMS还没有回收完,又要开始进行下一次CMS回收,因为最终可能导致内存不足,此时CMS回收失败,则会切换到Serial Old GC,暂停所有应用线程,开始回收垃圾。当然代价就是停顿的时间比较长。所以针对内存增长情况,可以适当调整这个值。内存增长慢的话,可以把这个值适当的调大些;如果内存增长快,可以把这个值调的小一些。具体情况需要不断测试。

3.2.3.4 CMS 垃圾收集过程
3.2.3.4.1 初始标记(Initial Mark)

第一:暂停所有应用线程,即stop-the-world
第二:GC Roots直接关联的老年代对象和被年轻代存活的对象引用的老年代对象进行标记。这个过程一般很快,标记完成后恢复暂停的应用线程。
在这里插入图片描述

3.2.3.4.2 并发标记(Concurrent Mark)

第一:多个线程和应用线程并发运行
第二:跟踪或者递归并发标记的对象,对处于引用链上的对象进行标记
第三:因为和应用线程并发运行,很有可能标记的对象被修改了引用,不处于引用链上或者之前不处于引用链上的对象现在被应用线程修改了引用,该对象处于引用链上。CMS在引用发生修改的时候,通过写屏障,会修改引用的对象所在的card在card table中 标记为dirty
在这里插入图片描述
在这里插入图片描述

3.2.3.4.3 并发预清理(Concurrent Preclean)

第一:多个线程和应用线程并发运行
第二:从card table遍历哪些是dirty card, 然后获取这些dirty card,然后跟踪dirty card中的对象,对处于引用链上的就进行标记
在这里插入图片描述

在这里插入图片描述

3.2.3.4.4 重新标记(Remark)

第一:暂停所有的应用线程
第二:扫描新生代对象、GC Roots以及dirty card对应的老年代对象
第三:因为暂停所有的应用线程,所以这个阶段的标记是干净的
备注:
因为新生代可能有对象引用老年代对象,这部分老年代也是存活的,所以需要标记GC Roots可能直接引用的对象发生了修改,所以也需要重新标记dirty card 虽然在预清理阶段清理过,但是因为预清理阶段是和应用线程并发执行的,所以有可能还会产生一些dirty card
在这里插入图片描述

3.2.3.4.5 并发清理(Concurrent Clean)

将没有标记的对象作为垃圾回收掉,这个阶段也是和应用线程并发工作的。

在这里插入图片描述

3.2.3.5 CMS 参数
3.2.3.5.1开启方式

CMS 通过-XX:+UseConcMarkSweepGC参数启用,年轻代使用ParNew GCS回收垃圾, 老年代使用CMS GC回收垃圾,比如:
java -XX:+UseConcMarkSweepGC com…MyExecutableClass

3.2.3.5.2 触发时机

-XX:CMSInitiatingOccupancyFraction:确定什么时候开始进行CMS垃圾回收,其默认值是-1。如果我们没显式设置这个参数,那么这个参数将由-XX:MinHeapFreeRatio和-XX:CMSTriggerRatio计算决定,计算公式:-XX:CMSInitiatingOccupancyFraction = (1 – MinHeapFreeRatio) + (CMSTriggerRatio * MinHeapFreeRatio / 100) = 92, 即老年代堆内存占用了92%就触发CMS GC。否则如果指定0~100之间的数,则按照显示指定的比例进行计算,老年代达到这个比例就进行CMS GC。

注意:如果内存增长很快,可能CMS还没有回收完,又要开始进行下一次CMS回收,因为最终可能导致内存不足,此时CMS回收失败,则会切换到Serial Old GC,暂停所有应用线程,开始回收垃圾。当然代价就是停顿的时间比较长。所以针对内存增长情况,
可以适当调整这个值。内存增长慢的话,可以把这个值适当的调大些;如果内存增长快,可以把这个值调的小一些。具体情况需要不断测试。

3.2.3.5.3 设置并发线程数

CMS在并发标记、并发预清理阶段和并发清理阶段都是启动多个线程和应用线程并发执行,那么这个垃圾回收并发线程数怎么置呢?

设置STW的并发线程数量
-XX:ParallelGCThreads: 设置STW阶段并发线程数量。-XX:ParallelGCThreads = (cpu cores <= 8) ? cpu cores : (5 * cpu cores) / 8 + 3

设置并发标记时候并发线程数量
-XX: ConcGCThreads:指的是在并发标记阶段,并发执行标记的线程数,一般设置为
-XX: ConcGCThreads = (-XX:ParallelGCThreads + 3) / 4,几乎等于-XX:ParallelGCThreads的1/4。而且注意,最多不允许超过-XX:ParallelGCThreads指定的线程数量。

3.2.3.5.4 解决内存碎片问题

可以通过-XX:UseCMSCompactAtFullCollection参数指示在CMS垃圾收集完后进行一内存碎片整理,这个过程会暂停应用线程。
可以通过-XX:CMSFullGCsBeforeCompaction设定在多少次CMS Full GC之后才进行压缩,默认为0,表示每一次Full GC都会进行压缩

3.3 G1 GC

3.3.1 G1出现的背景

传统的年轻代(新生代)垃圾回收器不论是Serial GC 、ParNew GC 还是 Parallel GC都会暂停应用线程,所以性能不好;老年代垃圾回收器Serial Old GC、Parallel Old GC也存在暂停应用线程收集垃圾的问题,虽然CMS针对暂停应用线程有所优化,但是CMS是基于标记-清除算法,这种算法的特点就是会产生很多内存碎片,导致内存利用率低。

3.3.2 G1有哪些新特性

第一:G1 GC 不是真正基于物理分代的垃圾回收器,而是基于逻辑分代的垃圾回收器
第二:G1 GC 既可以回收年轻代,也可以回收老年代,无需两种类型的垃圾回收器配合使用
第三:G1 GC 停止应用线程可以通过指定时间控制暂停时间
第四:G1 GC垃圾回收的时候有的阶段垃圾回收线程可以和应用线程并发执行

3.3.3 G1 GC中名词或者数据结构

3.3.3.1 Region

G1 将堆分成大小相等的多个区间(Region),我们在JVM启动的时候可以指定Region数量,必须是2的指数倍,默认Region数量是2048,可以通过参数-XX:G1HeapRegionSize=2048指定。简而言之就是堆内存分成指定数量、大小相等的内存块。每一个Region大小如何确定呢?如果JVM启动指定了堆内存参数-Xms8G 那么每一个Region就是8 * 1024 / 2048 = 4M, 即每一个Region是4M大小。
在这里插入图片描述

3.3.3.2 写入屏障

G1有一种机制,在对象引用关系发生变化的时候,这里会进行一些操作:
第一: 判断字段指向的对象是否在同一个Region, 如果在同一个Region,则没必要记录
第二: 如果Region不同,则获取Card, 更新Card在Card Table中的状态为1,表示Card已经dirty,引用发生过修改
第三: G1维护一个dirty card queue, 将Card放入dirty card queue。等待年轻代回收的时候,G1会对dirty card queue中所有的card进行处理,启动一个线程,从而更新RSet

3.3.3.3 Card & Card Table

为了方便垃圾回收,G1将Region划分为了多个卡片(Card),每一个Card默认512字节,并且G1维护了一个全局的Card Table, 即所有的Card状态都会位于Card Table中,通过0和1表示,Card Table的长度 = 堆的大小(字节) / 512(字节),假设堆大小是512M,那么Card Table长度 = 512 * 1024 * 1024 / 512 = 1048576。这里的Card实现原理和CMS的Card都差不多,但是G1的Card是在Region上分配的,而CMS中是在堆上分配的。
在这里插入图片描述

3.3.3.4 Remembered Set(RSet)
3.3.3.4.1 为什么需要RSet

其实就是记录哪些Region的什么Card引用了我,这样很方便定位到引用当前Region中对象位于什么Region中。如果没有RSet,需要遍历整个堆才可以知道有哪些Region引用了当前Region,否则无法知道有哪些Region引用了当前Region。
所以,为了避免对整个堆内存扫描,跟踪引用当前Region中的对象,从而引入了RSet

3.3.3.4.2 为什么是老年代需要记录引用,而不是新生代呢?

因为每次Young GC的时候,Young区所有的Region都会被遍历,因为Eden中的存活对象要移到Survivor中空余地方,还需要对Survivor的age递增。
另外一个是年轻代的对象引用变化很大,如果都需要记录下来成本会很高。

3.3.3.4.3 RSet 是什么

RSet的数据结构其实是一个hash table,key是引用当前Region的Region的起始地址,value是一个集合,集合内容是引用当前Region的Card在Card Table中的索引,因为引用当前Region的Region可能有多个引用指向当前Region,这些引用位于不同的Card,所以需要用集合表示。
在这里插入图片描述

3.3.3.4.3 对象修改引用记录到RSet的过程分析

假设:
A a = new A();
B b = new B();
b.a = a; // 修改指针
当指针修改后,会执行一个write barrier,这时候就需要在a所在的Region上记录b所在的Card.
#1 如果是相同的Region,则没必要记录
#2 如果是不同的Region,则读取当前所在的Card Table,如果当前Card 在Card Table中已经是dirty了,那么就不需要更新了;如果是clean,则需要置为dirty。
#3 每一个Region 都有一个dirty card queue(remembered set log) , 这个dirty card放入dirty card queue中

#4 dirty card queue如果的达到一定数量,则开启线程refinement线程处理,更新到所引用的对象的所在的Region的RSet
更新过程如下:
首先: 将这个跟Card在Card Table中状态置为clean
其次: 检查Card中所有对象的field指针,如果这个指针指向外部的Region,则在外部Region的RSet上记录这个Card在Card Table中的索引,这时候才算真正更新RSet

3.3.3.4.4 Collection Set(CSet)

CSet是Collection Set的缩写,它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。比如YGC的时候,根据目标暂停时间,只允许收集2个垃圾比较多的Region, 选择下图虚线红框内的Region放到CSet,表示本次需要回收的Region的集合:
在这里插入图片描述

3.3.3.4.5 G1中的分代情况

G1将堆划分为多个Region,默认是2048。当然这个数量是可以设置的。
Region可以分为E(Eden区),S(Survivor区)存活,O(Old Generation)老年代,H(Humongous区)以及未分配区
#1 Eden和Survivor和其他垃圾回收器一样,都视为Young区
#2 这些Region不是一开始就标记好了,是根据需要和GC回收情况来确定这个Region是不是Eden或者Survivor或者Old Generation等. 比如第一次肯定都是分配的E区,当发生一次Young GC,则会产生S区,如果需要分配老年代中,则会产生O区,有大对象泽会产生H区
#3 大对象如果一个H Region装不下,则会分配连续的H Region来分配。 一般来说超过Region的50%就算大对象了,当然我们也可以自己定义这个比例
#4 Eden、Survivor、Old Generation区和以往旧的垃圾收集器相比,不要求连续。
在这里插入图片描述

3.3.4 G1的垃圾回收类型

3.3.4.1 Young GC(年轻代回收)(Minor GC)
3.3.4.1.1 触发时机

年轻代满了或者达到指定的阈值才会触发Minor GC, 那阈值是通过什么设定呢?G1提供了-XX:G1NewSizePercent和-XX:G1MaxNewSizePercent 2个参数,-XX:G1NewSizePercent表示年轻代占据堆内存的初始比例,默认5%; -XX:G1MaxNewSizePercent表示年轻代占据堆内存的最大比例,默认60%, 达到这个比例就会强制触发YGC。

3.3.4.1.2 回收范围

第一:年轻代回收范围,包括整个年轻代,即Eden区域+Survivor区域,Eden和Survivor区域占比-XX:SurvivorRatio依然有效。
第二:大对象在年轻代和老年代都会参与回收
第三:年轻代回收是否是把所有年轻代的Region中的垃圾都回收掉?不是的,因为这要取决于设置的暂停时间-XX:G1MaxPauseMills,G1 会根据暂停时间,选择清理Region之后效益是最高Region去回收,这样效率更高。

3.3.4.1.3 工作流程
3.3.4.1.3.1 暂停应用线程,启动多个垃圾回收线程开始收集垃圾

年轻代回收,需要暂停应用线程,此时会根据目标停顿时间动态选择部分垃圾多的Region回收,然后加入到CSet中。所以,YGC并不一定每次都会把垃圾回收完。

3.3.4.1.3.2 扫描GC Roots

根据虚拟机栈、本地方方法栈、类的静态变量等引用的堆中的对象就是GC Roots,根据GC Roots遍历,找到可以从GC Root直接到达CSet中Region里面的对象。也就是先找到要回收集合中CSet哪些对象是GC Root可以直接到达的。

3.3.4.1.3.3 根据G1维护的dirty card queue,更新RSet

G1会对dirty card queue中所有的card进行处理,启动一个线程更新RSet。更新过程如下:
检查card内部所有对象的字段指针,哪些字段指针指向了外部Region。如果没有则忽略;如果有则更新对应Region维护的RSet,如果已经有这个Region了,则更新value,即card在card table中的索引;如果没有以这个Region地址作为key,然后card在card table 中的索引放入到value中

3.3.4.1.3.4 扫描RSet

扫描CSet中的region的RSet,如果引用的region是年轻代中的region,则可以不管,因为年轻代无论怎么样都是会全部扫描的;
主要是为了确定哪些region是老年代的,因为RSet就是为了避免扫描整个老年代的一个辅助结构。确定了哪些Region是老年代的region,然后获取这些region中的card,然后在这些card中找字段对象引用了CSet中的Region中哪些对象。如果有则这些CSet中region中的对象会拷贝到survivor区中region中去。

3.3.4.1.3.5 存活对象拷贝到新的region中去(也就是survivor)

拷贝GC Root可达的对象和RSet中老年代region可达的CSet中region中的存活对象到年轻代survivor的region中去

3.3.4.1.3.6 释放CSet,清理card table,将card table中对应的card的dirty状态置为clean

清空垃圾回收集合CSet、将card table中的card状态置为clean

3.3.4.2 Mixed GC(混合GC)

因为在这个阶段既会进行Young GC,又会对老年代进行回收,所以叫混合回收。

3.3.4.2.1 触发时机

当年轻代回收之后或者大对象分配之后,会检测堆内存使用率。当整个堆内存使用率达到一定阀值的时候,启动对老年代的回收过程。这个阀值可以通过参数-XX:InitiatingHeapOccupancyPercent设置,阀值默认是45%。这个参数和CMS触发时机参数很类似,CMS的触发参数是-XX:CMSInitiatingOccupancyFraction, 默认值92%。

3.3.4.2.2 回收范围

Mixed GC会触发一次Young GC, 所以年轻代肯定是处于回收范围,另外会回收部分老年代对象。

3.3.4.2.3 回收过程
3.3.4.2.3.1 并发标记阶段
  • 3.3.4.2.3.1.1初始标记(Initial Mark)

初始标记阶段会触发YGC,初始标记阶段会利用YGC的暂停,进行GC Roots直接可达对象扫描,压入扫描栈。完事之后应用线程恢复。

  • 3.3.4.2.3.1.2根区域扫描(Root Region Scan)

第一:和用户线程并发执行
第二:从survivor区中找到年轻代对老年代的跨代引用,加入到标记栈
注意:年轻代会做一次YGC,把存活对象转移到Survivor区,那么存活区有可能存在对老年代引用的对象,如果存在表示老年代对象不是垃圾,应该被标记。所以这里

  • 3.3.4.2.3.1.3 并发标记(Concurrent Mark)

第一:和用户线程并发执行
第二:根据GC Root直接可达的对象和survivor区中新生代对老年代引用的对象作为入口,跟踪并且递归标记引用链上的对象
第三:因为是和应用程序并发执行,所以当如果这时候有Young GC的时候,就会暂停,等待Young GC执行完毕
第四:因为应用程序在并发运行,所以存在老年代对象引用发生变化的情况,会出现dirty card的情况,这个会在重新标记阶段处理。

  • 3.3.4.2.3.1.4 重新标记(最终标记)

第一:暂停所有应用程序线程
第二:根据card table找到dirty card, 然后重新跟踪dirty card中的对象,重新标记。
第三:之前已经被标记的,但是因为引用改变,不是可达的对象,将会产生浮动垃圾。

  • 3.3.4.2.3.1.5清理阶段

第一:统计老年代堆中的存活对象,这样就可以知道哪些region的垃圾多,哪些region垃圾少
第二:根据预期的GC效率对这些region进行排序,识别可供混合回收的区域

3.3.4.2.3.2 混合回收阶段

第一:在全局并发标记之后,明确知道哪些老年代region中垃圾多,哪些region中垃圾少,G1会优先回收垃圾比较多的region
第二:默认情况下,老年代region会分为8次回收完本次需要回收region,可以通过-XX:G1MixedGCCountTarget参数设置最多回收次数(先暂停应用线程,然后回收;然后恢复应用线程,暂停回收,重复8次,这样也可以让系统间歇性的运行一下);另外还需要根据GC暂停时间MaxGCPauseMillis来确定需要回收多少,如果有1000个可以回收,但是规定的暂停时间是200毫秒,但是200毫秒只能回收800个,则只能回收800个,剩余的200个Region只有下一次回收了。
#3 -XX:G1MixedGCLiveThresholdPercent控制需要回收的Region是否需要参加本次回收,默认值85%,存活对象不得低于此比例,如果低于此比例才需要被回收,否则还可以躲过本次回收
#4 所以混合回收的回收集CSet只包含本次老年代1/8的Region,以及需要回收的Eden Region和Survivor Region和Humongous Region
#5 混合回收不一定需要进行8次,有一个参数-XX:G1HeapWastePercent,默认10%,允许堆内存有10%的空间浪费,有可回收的垃圾比例低于这个比例,则不再回收
#6 暂停所有线程,开始把CSet里所有的Region拷贝到新空闲Region,然后把CSet里的Region全部清理掉

3.3.4.3 Full GC(全量GC)
3.3.4.3.1 触发时机

第一:转移对象的空间不够分配
转移对象到survivor分区,如果survivor分区没有足够的region或者空间
第二:新的对象没有空间分配,新生代和老年代都没有(分配没有空闲Region)
新的对象没有空间分配,新生代和老年代都没有(分配没有空闲Region)
Full GC会暂停所有线程,采用单线程进行标记清理和压缩,即所谓的串行回收器,这个过程是及其缓慢(回收没有空闲Region)

3.3.4.3.2 回收范围

Full GC是对整个堆进行串行回收

3.3.4.3.3 回收过程

暂停所有线程,采用单线程进行标记清理和压缩,然后进行垃圾回收

3.3.5 G1 GC 参数

3.3.5.1 开启参数

通过使用-XX:+UseG1GC参数,开启G1 GC

3.3.5.2 指定目标暂停时间

通过-XX:MaxGCPauseMillis=200参数来指定目标最大暂停时间

3.3.5.3 指定处理dirty card queue的refinement线程数量

-XX:G1ConcRefinementThreads

3.3.5.4 指定年轻代堆初始大小以及堆最大大小

堆初始大小:就是初始化一次性分配多少内存。通过-XX:G1NewSizePercent
参数指定,默认5%
堆最大大小:超过这一阈值就开始触发年轻代垃圾回收。通过
-XX:G1MaxNewSizePercent参数指定,默认60%,即新生代占用堆的内存达到60%则开始触发垃圾回收

3.3.5.5 指定G1并发线程数量

并行垃圾收集阶段并发线程数: 通过参数-XX:ParallelGCThreads指定。如果大于8,则会通过一个公式计算((5*CPU)/8)+3;如果小于8则以指定的值为准

并发标记阶段并发线程数:通过参数-XX:ConcGCThreads指定,默认为参数-XX:ParallelGCThreads的1/4,指定的-XX:ConcGCThreads参数不能超过参数-XX:ParallelGCThreads的大小

3.3.5.6 新生代相关参数

-XX:NewRatio=n
新生代与老年代(new/old generation)的大小比例(Ratio). 默认值为 2.
-XX:SurvivorRatio=n eden/survivor
空间大小的比例(Ratio). 默认值为 8.
-XX:MaxTenuringThreshold=n
提升年老代的最大临界值(tenuring threshold). 默认值为 15

3.3.5.7 G1调试相关参数

-XX:+G1SummarizeRSetStats:
这个也是一个VM的调试信息。如果启用,会在VM推出的时候打印出RSets的详细总结信息。
-XX:G1SummaryRSetStatsPeriod
就会阶段性地打印RSets信息
-XX:+G1TraceConcRefinement
这个也是一个VM的调试信息。如果启用,并行回收阶段的日志就会被详细打印出来;

3.4 CMS GC和 G1 GC 比较

3.4.1 是否可以跨代收集

CMS 使用 ParNew GC进行年轻代回收;CMS只对老年代进行回收,不能跨代回收
G1 可以同时对年轻代和老年代进行混合回收

3.4.2 基于并发的算法不一样

CMS基于的是增量算法
G1基于的是STAB(Snapshot-At-The-Beginning)算法

3.4.3 如何处理跨代引用问题

简单的将:CMS基于的是Card和Card Table来处理;G1基于的是Region,Card和RSet来处理

CMS在并发标记后,会根据对象引用发生改变的Card记录到Card Table中,Card Table,记录的是我自己发生了变化。
Card Table维护Card状态,如果对象引用发生变化,则会将对应的Card Table的对应Card状态置为dirty,完了之后扫描这个Card Table中dirty对应的Card,遍历这个Card所有对象,进行标记工作

G1 引入了Region,然后Region又是多个Card组成的,并且引入了Remembered Set,每一个Region 对应一个Remembered Set,
Remembered Set和CMS中Card Table还稍微有一点不一样,他记录的是谁引用的我,是一个哈希表结构,存储引用当前Region中对象的Region的索引和一个Card Table数组,在年轻代GC的时候,就会更新RSet,并且会去RSet里的Card信息遍历Card内对象,然后开始递归标记。

3.4.4 并发标记过称不一样

CMS:在初始化标记阶段不会主动进行Young GC
G1: 在初始化标记阶段会主动进行Young GC;

G1使用STAB算法,先对初始标记的对象进行快照,即GC Roots关联的直接对象和新生代中关联的直接对象是存活的,其余都是垃圾对象;另外在并发标记过程中产生的新的对象默认都是存活的,留到下一次处理;引用关系发生变化,将这个脏的Card放入一个队列;在重新标记的时候会以这个队列中所有Card的对象为Root递归向下遍历,标记存活对象
CMS: 在并发标记阶段只是对GC Root直接关联的对象和新生代中直接饮用的对象作为Root向下递归,标记存活对象;然后在预清理阶段重新对新生代进行标记并且会根据Card Table中的dirty card获取其内部的对象作为root,向下递归,标记存活对象;重新标记的时候找到新生代对老年代的引用对象,GC Roots关联的老年代直接对象以及 Card Table中的dirty card中的对象,然后以他们为入口,递归向下遍历,只要引用链上可达,就标记为存活对象

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

莫言静好、

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

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

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

打赏作者

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

抵扣说明:

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

余额充值