关于JVM的垃圾收集我准备这样讲:
首先把相关的算法给大家讲明白
然后将几款垃圾回收器结合算法给大家讲明白
最后等大家把C++学完带着大家读读垃圾收集器的源码
Java程序在运行过程中会产生大量的对象,但是内存大小是有限的,如果光用而不释放,那内存迟早被耗尽。如C、C++程序,需要程序猿手动释放内存,Java则不需要,是由垃圾回收器去自动回收。
垃圾回收器回收内存至少需要做两件事情:标记垃圾、回收垃圾。于是诞生了很多算法。本节课的重点就是深入理解这些算法。下节课就是探讨各种垃圾收集器。
垃圾收集器涉及到的算法如何不学明白,垃圾回收器是学不明白的。同学们要重视算法的学习!学会了,你就超越了很多同龄人。
垃圾判断算法
即判断JVM中的所有对象,哪些对象是存活的,哪些对象可回收的算法。
引用计数算法
最简单的垃圾判断算法。
在对象中添加一个属性用于标记对象被引用的次数,每多一个其他对象引用,计数+1,当引用失效时,计数-1,如果计数=0,表示没有其他对象引用,就可以被回收。
这个算法无法解决循环依赖的问题。
可达性分析算法
通过一系列被称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系链向下搜索,如果某个对象无法被搜索到,则说明该对象无引用执行,可回收。相反,则对象处于存活状态,不可回收。
JVM中的实现是找到存活对象,未打标记的就是无用对象,GC时会回收。
哪些对象可以作为GC Root呢:
- 所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
- VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。
- JNI handles,包括global handles和local handles
- (看情况)所有当前被加载的Java类
- (看情况)Java类的引用类型静态变量
- (看情况)Java类的运行时常量池里的引用类型常量(String或Class类型)
- (看情况)String常量池(StringTable)里的引用
内存池
具体细节见课堂上操作实战
垃圾回收算法
具体细节见课堂上操作实战
1、标记-清除算法
2、标记-整理算法
3、标记-复制算法
三色标记与读写屏障
所有的垃圾回收算法都要经历标记阶段。如果GC线程在标记的时候暂停所有用户线程(STW),那就没三色标记什么事了。但是这样会有一个问题,用户线程需要等到GC线程标记完才能运行,给用户的感觉就是很卡,用户体验很差。
现在主流的垃圾收集器都支持并发标记。什么是并发标记呢?就是标记的时候不暂停或少暂停用户线程,一起运行。这势必会带来三个问题:多标、少标、漏标。垃圾收集器是如何解决这个问题的呢:三色标记+读写屏障。
三色标记
把遍历对象过程中遇到的对象,按照“是否访问过”这个条件标记成三种颜色:
- 白色:尚未访问过。
- 黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。
- 灰色:本对象已访问过,但是本对象 引用到 的其他对象 尚未全部访问完。全部访问后,会转换为黑色。
有个问题同学们可以自己先想想:对象上的标记是何时清除的?
读写屏障
有点像Spring的AOP
1、读屏障
即在读前增加屏障做点事情
读屏障() 读操作
2、写屏障
即在写的前后增加屏障做点事情
写前屏障() 写操作 写后屏障()
详解三个问题
1、多标 浮动垃圾
GC线程已经标记了B,此时用户代码中A断开了对B的引用,但此时B已经被标记成了灰色,本轮GC不会被回收,这就是所谓的多标,多标的对象即成为浮动垃圾,躲过了本次GC。
多标对程序逻辑是没有影响的,唯一的影响是该回收的对象躲过了一次GC,造成了些许的内存浪费。
2、少标 浮动垃圾
并发标记开始后创建的对象,都视为黑色,本轮GC不清除。
这里面有的对象用完就变成垃圾了,就可以销毁了,这部分对象即少标环境中的浮动垃圾。
3、漏标 程序会出错
漏标是如何产生的呢?GC把B标记完,准备标记B引用的对象,这时用户线程执行代码,代码中断开了B对D的引用,改为A对D的引用。但是A已经被标记成黑色,不会再次扫描A,而D还是白色,执行垃圾回收逻辑的时候D会被回收,程序就会出错了。
如何解决漏标问题
先分析下漏标问题是如何产生的:
条件一:灰色对象 断开了 白色对象的引用;即灰色对象 原来成员变量的引用 发生了变化。
条件二:黑色对象 重新引用了 该白色对象;即黑色对象 成员变量增加了 新的引用。
知道了问题所在就知道如何解决了
1、读屏障 + 重新标记
在建立A对D的引用时将D作为白色或灰色对象记录下来,并发标记结束后STW,然后重新标记由D类似的对象组成的集合。
重新标记环节一定要STW,不然标记就没完没了了。
2、写屏障 + 增量更新(IU)
这种方式解决的是条件二,即通过写屏障记录下更新,具体做法如下:
对象A对D的引用关系建立时,将D加入带扫描的集合中等待扫描
3、写屏障 + 原始快照(SATB)
这种方式解决的是条件一,带来的结果是依然能够标记到D,具体做法如下:
对象B的引用关系变动的时候,即给B对象中的某个属性赋值时,将之前的引用关系记录下来。
标记的时候,扫描旧的对象图,这个旧的对象图即原始快照。
4、实际应用
CMS:写屏障 + 增量更新
G1:写屏障 + SATB
记忆集与卡表
这个东西过于复杂,课堂上不准备讲,我之前写过文章,感兴趣的可以看看,有问题找我探讨
练习
1、针对课堂上讲的所有算法自己写成笔记或文章
2、可以试着用java实现基础的GC算法:标清、标整、分代+赋值算法(不难,别怕)