一、对象是否已死
1. 引用计数
在对象中添加一个引用计数器,每当有一个地方引用它时就加一,失效时减一。
优点
:实现简单;
缺点
:无法解决循环引用;
2. 可达性分析
通过GCRoot作为起点集向下遍历调用链,不在调用链上的对象就是不可能再被使用的对象。
GCRoots包括以下几种:
1. 栈中
引用的对象,即栈中局部变量表LocalVariableTable;
2. 方法区中
static和final属性引用的对象,例如常量池;
3. 本地方法栈中
Native方法引用的对象;
4. JVM内部
需要使用的常驻对象,例如基本数据类型对应的Class对象、系统类加载器、异常对象、JMXBean等等;
5. synchronized
持有的对象;
引用类型:
1. 强引用
程序代码中普遍存在的引用赋值;
2. 软引用
正常情况下不到回收,内存溢出前回收;
3. 弱引用
垃圾收集时发现它就会回收;
4. 虚引用
弱引用的一种特殊情况,只是为了被回收时进行通知回调设计的,例如堆外内存的回收
方法区回收:
1. 常量
没有任何对象引用常量池中该常量时可能会被回收;
2. Class
回收需要同时满足三个条件:堆中不存在该Class的实例 && 该类加载器已经被回收 && Class没有被任何地方引用(排除正在反射的情况);可以看到Class被回收的条件是很苛刻的,现在第三方框架大量使用反射、动态代理、CGLIB的生成Class,给Metaspace带来压力;
二、垃圾收集算法
1. 标记清除
1960年提出,标记出所有需要回收的对象,然后统一回收掉;
优点:
简单;不需要移动存活对象;
缺点:
执行效率跟堆中对象数量成反比,对象非常多时效率低;清除后产生大量内存碎片;
2. 标记复制
1969年提出,为了解决标记清除算法大量对象效率低的问题;新生代98%熬不过第一轮,不需要按照1:1划分新生代内存空间,1989年提出新生代分为一块较大的Eden区和两块较小的Survivor区,每次把Eden和Survivor中仍存活的对象一次性复制到另外一块Survivor上;
优点:
没有碎片;分配内存时只需要移动堆顶指针按顺序分配,简单高效;
缺点:
空间浪费;需要移动存活的对象;
3. 标记整理
1974年提出,为了解决标记复制浪费空间的问题;先标记存活对象然后让所有存活的对象移动到一端,然后直接清理掉另一端内存;
标记清除和标记整理区别是前者不需要移动存活对象属于停顿时间优先,分配时需要找到可分配区域,相对麻烦;回收时直接清理,相对简单;后者正好相反属于吞吐量优先;
优点:
没有空间浪费;没有内存碎片;
缺点:
需要移动存活对象;
三、垃圾收集器
1. Serial / Serial Old收集器
单线程收集器,新生代Serial采用复制算法,老年代Serial Old采用整理算法;
优点:
简单,额外内存消耗最小;
缺点:
STW;
2. Parallel Scavenge / Parallel Old收集器
吞吐量优先的多线程收集器,新生代Parallel Scavenge采用复制算法,老年代Parallel Old采用整理算法;新增两个参数用于精确控制吞吐量,最大垃圾收集停顿时间:-XX:MaxGCPauseMillis,吞吐量大小:-XXGCTimeRatio;
3. ParNew收集器
新生代收集器采用复制算法,Serial的多线程版本,可以与CMS配合;
4. CMS收集器(重点)
老年代收集器采用清除算法,过程分为四个步骤:
初始标记:
单线程标记GC Roots能直接关联到的对象,需要STW但是速度很快;
并发标记:
多线程从初始标记的对象开始遍历整个对象图,过程较长,但是可以跟用户线程并行执行;
重新标记:
多线程对并发标记期间产生变动的对象进行重新标记,过程比初始标记长,但是远比并发标记短;
并发清除:
清理被标记为死亡的对象,不需要移动存活对象,因此可以跟用户现场并行执行;
4.1 如何进行标记?
三色标记法把遍历对象图过程中遇到的对象,按照“是否被垃圾收集访问过”这个条件标记成以下三种颜色:
白色:
尚未被垃圾收集访问过,开始时都是白色,最后剩下的白色就是不可达的可回收对象;
黑色:
已经被垃圾收集访问过,并且这个对象所有的引用都已经扫描过,表示可达的存活对象;
灰色:
对象本身已经被垃圾收集访问过,但是对象上还有未被访问过的引用;
4.2 三色标记产生的问题
1.
浮动垃圾
如图已经扫描过标记为黑色的对象引用断开,此时这个被断开的黑色对象这一轮垃圾回收时就不会再被扫描到并且一直是黑色,这一轮不会被回收,就产生了浮动垃圾。浮动垃圾理论上下一轮回收就会被回收掉,这个问题不大。
2.
对象消失
如图灰色对象到白色对象的引用突然效时,并且已经扫描过的黑色对象又指向了该白色对象,由于黑色对象本轮垃圾回收已经被扫描过,导致该白色对象不会变黑,到并发清理阶段会被当作垃圾清除掉。这种存活的对象被回收会产生严重的后果。
4.3 对象消失解决方案
1.
增量更新
当黑色对象插入新的指向白色对象的引用关系时,就将这个引用记录下来,等并发扫描之后再对记录的这些引用重新扫描。(相当于黑色引用白色时就变为灰色)CMS采用这种方案。
2.
原始快照
当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发标记结束后再将这些记录过的引用关系中的灰色对象为根,重新扫描。(相当于无论引用关系删除与否,都会按照最初开始扫描那一刻的对象图快照来进行扫描)G1、Shenandoah采用这种方案。
解决方案有了,那如何实现呢,即引用记录在哪儿?又如何记录引用?
3.
记录在哪儿?-记忆集与卡表
记忆集
是用来记录从非收集区指向收集区的指针集合的抽象数据结构,这种记录无论空间占用还是维护成本都相当高昂,为了降低成本通常有三种记录粒度:字节精度(32/64位)、对象精度(精确到每一个对象)、卡精度(精确到一块内存区域);
卡表
就是用来实现卡精度记忆集的数据结构,卡表含有多个卡页,一个卡页又包含多个对象,只要卡页中有一个跨代应用就重新扫描整个卡页,这样在跨代引用概率很低时可以节约维护成本。
卡表的思想可以类比现在新冠检测采取的策略:在已知感染者概率很低的前提下,让一批人公用一份检测试剂,如果该试剂结果呈阴性说明这一批人都没有被感染;如果呈阳性,那需要让这一批人全部重新单独进行检测;
4.
如何记录?-写屏障
HotSpot虚拟机采用写屏障技术维护卡表状态,写屏障可以看作在JVM层面对引用类型字段赋值这个操走的AOP切面,在赋值前的操作叫写前屏障,之后的叫写后屏障。在写前屏障中来对卡表进行维护。
4.4 CMS优缺点
优点
划分步骤的思想具有划时代的意义;高并发低停顿;
缺点
1.
跟用户线程并行的阶段占用处理器资源导致应用程序变慢;
2.
浮动垃圾的问题并未解决,不能等到老年代快满了再收集,需要预留组构内存空间给用户线程使用(预留空间不足时会STW,临时启用Serial Old收集器);
3.
由于采用清除算法,那么就会产生内存碎片,在fullgc时会对碎片进行整理,整理时会移动存活对象需要STW,这样碎片问题解决了但停顿时间又会变长;
5. G1收集器(重点)
G1采用基于Region的堆内存布局,堆内存不再是固定大小的新生代/老年代,而是由若干个Region组成,每个Regin都可以扮演新生代、老年代、大对象区(存放超过一定大小的大对象)的角色。G1收集器会评估每个Region的回收价值维护一个优先级列表中,每次收集时以Region为单位回收多个Region。
G1回收步骤也分4步:
初始标记:
标记GC Roots能直接关联到的对象,并修改TAMS指针值(为了跟用户线程并行执行,每个Region通过TAMS指针划分出来用于并发回收过程中新对象分配),该阶段停顿时间极短;并发标记:
从GC Roots关联的对象开始扫描整个对象图,该阶段耗时较长可以并发执行;扫描完后还要重新处理STAB(原始快照)记录下的并发时有引用变动的对象;最终标记:
短暂的STW,用于处理并发阶段结束后仍遗留下来的最后少量的SATB记录;筛选回收:
负责更新Region的统计数据,对各个Region价值进行排序,决定回收哪些Region;回收时把待清理Region中剩余对象全量复制到空Region中,然后清空Region;该阶段涉及存活对象的移动,需要STW,但是可以多个线程并行操作;
5.1 G1优缺点
优点:
1.
可以通过指定期望停顿时间,在不同场景下在吞吐量和延迟时间之间做平衡,因为G1追求内存分配速率,因此不同的停顿时间只要分配速度大于回收速度就可以;
2.
分Region回收,可以按照收益动态确定回收哪个;
3.
没有内存碎片;
缺点:
1.
需要通过卡表存储各个Region之间的引用,额外内存占用10%-20%;
2.
需要记录Region之间的双向应用,处理Region之间的引用更加复杂;
3.
除了需要通过写后屏障更新卡表外,还要使用写前屏障跟踪并发时的指针变化情况;相比增量更新算法,原始快照能减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。
----------over---------