深入理解垃圾回收器

〇、遗留的问题

分代复制算法的指针移动原理

数据发生了移动,为什么依然能够访问?

底层原理:
在这里插入图片描述

找OS要160B的内存
一个cell是8B,需要20个cell

五五分
一半用,一般空闲

JVM中的对象最小是16B

其实底层算法都是基于规律来的

指针压缩:基于8字节对齐

可用内存列表:
1、申请8B内存
2、申请16B内存

计算移动内存地址公式: From的起始地址 + Cell的起始位置 + 对齐字节数

假设有内存如下:
在这里插入图片描述
O-A、O-C是from区GC前的对象,假设本次GC这两个对象不被回收。那么这两个对象转到To区之后,N-A、N-C的内存地址怎么计算,才能使得对象可以正常使用呢?

O-A: 0 + 0 * 8
O-C: 0 + 1 * 8

N-A: 11 + 0 * 8
N-C: 11 + 1 * 8

From区切To区,指针为什么还能用

第一种情况: From区的前两个对象不会被gc
空间交换
数据移动

指针其实没有移动,指针是根据规律在用的时候实时计算出来的

1、将to区切换为from区
2、将from区中剩余的对象拷贝过来(从原来的to区,现在的from区,从头部开始分配)
首先会在to区申请一块新的内存,然后将数据拷贝过来
3、将原from区中的内存清空 memrest

第二种情况:
在这里插入图片描述
O-A被清理了,O-C没有被清理

一、垃圾回收器

Stop The World。即GC线程与用户线程无法并发运行,GC线程执行期间需要暂停用户线程。
比如:你妈给你打扫房间,需要把你从房间赶出去,不然她一般打扫垃圾,你一边制造垃圾,没完没了了。那接下来可能要家法伺候了。
垃圾收集器
目前JVM中的收集器有九种,了解5个,详解2个。因为并发、分区管理式的收集器才是未来的趋势。
注意:标记阶段标记的是存活对象,回收未被标记的对象。

1、Serial收集器

串行垃圾收集器,即GC线程与用户线程先后运行,即GC时需要STW(暂停所有用户线程),直至GC结束才恢复用户线程的运行
专注于收集年轻代,底层是复制算法

相关参数:-XX:+UseSerialGC
在这里插入图片描述

2、ParNew收集器

Serial收集器的多线程版本。唯一能与CMS收集器搭配使用的新生代收集器。

相关参数:
-XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器
-XX:+UseParNewGC:强制指定使用ParNew
-XX:ParallelGCThreads:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同
在这里插入图片描述

3、Parallel收集器

关注吞吐量的收集器
吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)

相关参数:
• -XX:MaxGCPauseMillis:是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。不过大家不要异想天开地认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
• -XX:GCTimeRatio:一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1 /(1+19)),默认值为99,就是允许最大1%(即1 /(1+99))的垃圾收集时间。
• -XX:+UseAdaptiveSizePolicy:一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)
在这里插入图片描述

4、Serial Old收集器

Serial收集器的老年代版本。基于标记-整理算法实现,
有两个用途:
1、与Serial收集器、Parallel收集器搭配使用
2、作为CMS收集器的后备方案

5、Parallel Old收集器

Parallel收集器的老年代版本。基于标记-整理算法实现。

6、CMS收集器

聚焦低延迟。基于标记-清除算法实现。
由于CMS收集器是并发收集器,即在运行阶段用户线程依然在运行,会产生对象,所以CMS收集器不能等到老年代满了才触发,而是要提前触发,这个阈值是92%。这个阈值可以通过参数-XX:CMSInitiatingOccupancyFraction设置

相关参数:
-XX:+UseConcMarkSweepGC:手动开启CMS收集器
-XX:+CMSIncrementalMode:设置为增量模式
-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收
-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
在这里插入图片描述

CMS收集器工作分四个步骤:

1、初始标记
会STW。只标记GC Roots直接关联的对象。

2、并发标记
不会STW。GC线程与用户线程并发运行。
会沿着GC Roots直接关联的对象链遍历整个对象图。可想而知需要的时间较长,但因为是与用户线程并发运行的,除了能感知到CPU飙升,不会出现卡顿现象。

3、重新标记
会STW。
CMS垃圾收集器通过写屏障+增量更新记录了并发标记阶段新建立的引用关系,重新标记就是去遍历这个记录。

4、并发清除
GC线程与用户线程并发运行,清理未被标记到的对象
默认启动的回收线程数 = (处理器核心数 + 3) / 4
显然CMS收集器依然不是完美的,不然后面就不会出现G1、ZGC等。那有哪些缺点呢?
1、运行期间会与用户线程抢夺CPU资源。当然,这是所有并发收集器的缺点
2、无法处理浮动垃圾(标记结束后创建的对象)
3、内存碎片

7、G1收集器

在这里插入图片描述

G1收集器与之前的所有收集器都不一样,它将堆分成了一个一个Region,这些Region用的时候才被赋予角色:Eden、from、to、humongous。一个region只能是一个角色,不存在一个region既是Eden又是from。

每个region的大小可通过参数-XX:G1HeapRegionSize设置,取值范围是2-32M。
一个对象的大小超过region的一半则被认定为大对象,会用N个连续的region来存储。

G1名字的由来
回收某个region的价值大小 = 回收获得的空间大小 + 回收所需时间
G1收集器会维护一个优先级列表,每个region按价值大小排序存放在这个优先级列表中。收集时优先收集价值更大的region,这就是G1名字的由来。
四个步骤:

1、初始标记
会STW。
做了两件事:
1、修改TAMS的值,TAMS以上的值为新创建的对象,默认标记为存活对象,即多标
2、标记GC Roots能直接关联到的对象

2、并发标记
耗时较长。GC线程与用户线程并发运行。
从GC roots能直接关联到的对象开始遍历整个对象图

3、最终标记
遍历写屏障+SATB记录下的旧的引用对象图

4、筛选回收
更新region的统计数据,对各个region的回收价值进行计算并排序,然后根据用户设置的期望暂停时间的期望值生成回收集。
然后开始执行清除操作。将旧的region中的存活对象移动到新的Region中,清理这个旧的region。这个阶段需要STW。

**相关参数:
-**XX:G1HeapRegionSize:设置region的大小
-XX:MaxGCPauseMillis:设置GC回收时允许的最大停顿时间(默认200ms)
-XX:+UseG1GC:开启g1
-XX:ConcGCThreads:设置并发标记、并发整理的gc线程数
-XX:ParallelGCThreads:STW期间并行执行的gc线程数

缺点:
1、需要10%-20%的内存来存储G1收集器运行需要的数据,如不cset、rset、卡表等
2、运行期间会与用户线程抢夺CPU资源。当然,这是所有并发收集器的缺点
查看默认收集器
java -XX:+PrintFlagsFinal -version | grep GC

G1能管理的最大堆空间是
4G - 64G
到了64G,指针压缩就没法用了。

大对象如何存储
会申请几个连续的region,叫做humongous

G1非常耗时间
20% - 30%的内存空间存储一些数据结构,使用空间换时间

现在垃圾回收器的发展趋势

模块化(region)+ 支持并发

JDK 8 不调优的话,默认使用的垃圾回收器是:
Parallel Scavenge
Parallel Old

二、三色标记

垃圾回收器运行方式

1、串行垃圾回收器
用户线程STW,一个gc线程运行
用户线程和gc线程交互执行

2、并行
用户线程STW,多个gc线程运行

3、并发
不需要STW,用户线程、gc线程并发执行

三色标记概念

把遍历对象过程中遇到的对象,按照“是否访问过”这个条件标记成三种颜色:
• 白色:尚未访问过。
• 黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。
• 灰色:本对象已访问过,但是本对象 引用到 的其他对象 尚未全部访问完。全部访问后,会转换为黑色。

JVM目前的垃圾判断算法是可达性分析算法,会找到GC roots, 标记存活对象,回收未被标记的。

如果对象被标记了,就是灰色;如果对象没有被标记,就是白色;如果对象被标记了,并且对象下面的引用都被访问过了,就是黑色。

标记类型

• 初始标记:标记GC root直接关联的对象

• 并发标记:

• 重新标记

那么,为什么会使用三色标记呢?

如果垃圾回收器是串行或者并行,就不需要三色标记。

如果是并发执行,就需要三色标记。并发的情况下,用户线程和gc线程同时运行

一个对象的标记过程

A-白 =》B-白 =》C-白

1、开始标记:
A-灰 =》B-白 =》C-白

2、继续扫描A中的引用B,对B进行标记
A-灰 =》B-灰 =》C-白
此时A中的引用对象全部标完,将A标记成黑色
A-黑 =》B-灰 =》C-白

3、继续扫描B中的引用,将C标记成灰色
A-黑 =》B-灰 =》C-灰
此时B和B中引用都被标记,将B标记成黑色
A-黑 =》B-黑 =》C-灰

4、将C标记成黑色
A-黑 =》B-黑 =》C-黑

这是一个比较正常的标记过程

并发标记带来的三个问题

标记阶段以后,清除程序会清除掉白色的对象

1、多标 —> 浮动垃圾

GC线程已经标记了B,此时用户代码中A断开了对B的引用,但此时B已经被标记成了灰色,本轮GC不会被回收,这就是所谓的多标,多标的对象即成为浮动垃圾,躲过了本次GC。

多标对程序逻辑是没有影响的,唯一的影响是该回收的对象躲过了一次GC,造成了些许的内存浪费。
在这里插入图片描述
如果断掉引用? A.ref_B = NULL 或者 A.ref_B = D (指向其他对象,此时B对象在堆中没有引用)

2、少标 —> 浮动垃圾

标记程序的运行的过程中,用户线程依然会创建新的对象。新创建的对象默认是黑色。

并发标记开始后创建的对象,都视为黑色,本轮GC不清除。

这里面有的对象用完就变成垃圾了,就可以销毁了,这部分对象即少标环境中的浮动垃圾。

这些对象躲过了这次gc,下次gc可能被释放掉。

在这里插入图片描述

3、漏标 —> 程序会出错

漏标是如何产生的呢?

标记程序运行过程中,引用连发生改变。

GC把B标记完,准备标记B引用的对象,这时用户线程执行代码,代码中断开了B对D的引用,改为A对D的引用。但是A已经被标记成黑色,不会再次扫描A,而D还是白色,执行垃圾回收逻辑的时候D会被回收,程序就会出错了。即A.ref_D 会运行出错!
在这里插入图片描述
发生gc时,所有的对象视为白色
发生gc后,新创建的对象全部视为黑色

黑色不会再被标记!

为什么会发生漏标现象

条件一:灰色对象 断开了 白色对象的引用;即灰色对象 原来成员变量的引用 发生了变化。
条件二:黑色对象 重新引用了 该白色对象;即黑色对象 成员变量增加了 新的引用。

读写屏障

读屏障:
pre_read(JVM)
read(data)
post_read

写屏障
pre_write()
write(p, val)
post_write()

如果解决漏标问题

我们知道,多标或者少标的对象,在下一次gc时会被回收掉。但是漏标的对象,会使得程序调用出错。那我们怎么解决这个问题呢?
1、增量更新(incremental update)
在标记程序运行过程中发生了引用链的变动
通过写屏障将这个变动记录下来

写屏障 + 增量更新(IU)
这种方式解决的是条件二,即通过写屏障记录下更新,具体做法如下:
对象A对D的引用关系建立时,将D加入带扫描的集合中等待扫描。

将上述例子中的漏标的D加入到一个oopMap中。那么在最终标记的时候,会将这个map中的对象拿出来再重新标记一次。

2、原始快照 (SATB)

写屏障 + 原始快照(SATB)
这种方式解决的是条件一,带来的结果是依然能够标记到D,具体做法如下:
对象B的引用关系变动的时候,即给B对象中的某个属性赋值时,将之前的引用关系记录下来。
标记的时候,扫描旧的对象图,这个旧的对象图即原始快照。

将原先的引用关系记录下来假如oopMap
最终标记的时候遍历这个map

标记阶段做的事情

1、初始阶段:
]
在这里插入图片描述
只标记和gc roots直接关联的对象,所以只将A和E标成灰色;
耗时很少,此阶段也会STW

2、并发标记阶段:

不需要STW
耗时很久
将gc roots直接关联的对象的所有引用连全部跑一遍

通过A找到B,A此时标记成黑色,B标记成灰色;然后根据B去找C,B标成黑色,C标成灰色;然后遍历C的引用,发现C没有引用,直接将C标成黑色;
在这里插入图片描述

回收的永远是白色

三、强、软、弱、虚引用

在这里插入图片描述

1、强引用

我们平时写的代码如Test obj = new Test();这种引用关系就是强引用
就算会OOM也不会回收

2、软引用

内存不足的情况下才会回收
如果发生了gc但是内存充足,依然不会回收

3、弱引用

只有发生gc就会回收

4、虚引用

形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

四、记忆集与卡表

记忆集(Remember Set)

记忆集是用来解决跨代引用问题的。

新生代对老年代的引用

gc的时候,新生代的对象被释放了也没问题。
gc的时候如果新生代的对象被回收了,那么老年代的对象实际就是失去了引用。

发生gc后,新生代的对象被回收了,但是程序还能继续运行。

老年代对新生代的应用

gc的时候,新生代的对象被清理了

此时,老年代去访问新生代的对象,会报错

以上的信息需要有数据结构来记录,这个数据结构就是记忆集

卡表

记忆集与卡表的关系

记忆集是理论,卡表是具体实现

卡表的实现原理

在这里插入图片描述

卡表(card table)与卡页(card page)的关系

卡表中有2048个卡页

一个卡页对应一个Region
一个卡页是512B

工作原理

卡页中的一个字节用于表示Region中的4KB
4KB = 2M / 512 (2M是regison的空间大小,卡页中总共有512个,每个1B,这是JVM中写死的)

在这里插入图片描述

4KB的空间中如果存在对象对年轻代的引用,这个标志位就是1

如果不这样做,需要遍历所有属性为老年代的region中的所有对象

关系总结

  • Region:g1垃圾收集器对应的堆区有2048个region,每个region默认大小是2M
  • 跨代引用:新生代对老年代的引用,老年代对新生代的引用。 记忆集与卡表是解决这个问题的,记忆集与卡表记录了新生代和老年代的引用关系。如果不记录的话,需要遍历所有属性是老年代的region,然后遍历所有region中的所有对象。
  • 记忆集:是理论
  • 卡表:是具体实现。卡表中的卡页数量与region的数量是相同的。卡表中有2048个卡页
  • 卡页:一个卡页有512B。卡页中的每个字节对应region中的4KB (4KB = 2M / 512). 这个4KB中只要有一个对象存在对新生代的引用,这个标记就是1
    在这里插入图片描述
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值