对象引用
要对对象进行回收,就必须判断对象是否“存活”,判断对象是否“存活”有一下两种办法
引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用,计数器加一;若引用失效,则计数器减一;当计数器为0的时候就是对象不再被使用的时刻。
**缺陷:**无法解决循环引用
/**
* vm option: -XX:+PrintGCDetails 打印GC
*/
public Object instance = null;
private static final int _1MB = 1024 *1024;
private byte[] bigSeze = new byte[_1MB *2];
public static void testGc() {
ReferenceCountGC referenceCountGCA = new ReferenceCountGC();
ReferenceCountGC referenceCountGCB = new ReferenceCountGC();
referenceCountGCA.instance = referenceCountGCB;
referenceCountGCB.instance = referenceCountGCA;
referenceCountGCA = null;
referenceCountGCB = null;
System.gc();
}
public static void main(String[] args) throws InterruptedException {
testGc();
Thread.sleep(5000);
}
可达性分析算法
通过一系列的GC Roots 的根对象作为起始节点集, 从这些对象开始,根据引用关系向下搜索,搜索的路径为“引用链”如果某个对象到GC Roots间没有任何“引用链”,也就是从GC Roots到这个对象不可达,则说明此对象不可能再被使用
对象引用
引用类型
- 强引用:如
Object obj = new Object()
,只要引用关系存在,就不会回收被引用的对象 - 软引用:描述一些还有用,但非必要的对象,在系统将要内存溢出前,会把这些对象进行第二次回收。
SoftReference
类 - 弱引用:描述那些非必须的对象,被弱引用关联的对象只能生存到下一次GC之前。
WeakReference
类 - 虚引用:一个对象是否有虚引用不会对其生存造成影响,无法通过虚引用获取实例。
PhantomReference
类
回收方法区
方法区的垃圾回收主要是:废弃的常量和不再使用的类型。
判断一个类型是否不再被使用:
- 该类的所有实例都已被回收
- 加载该类的类加载器被回收
- 该类的java.lang.Class对象没有引用
垃圾回收算法
分代收集理论
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次垃圾收集过程的对象就越难消亡
- 跨代引用假说:跨代引用相对于同代引用来说仅占极少数
由此:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
针对于跨代引用,在新生代上建立一个全局数据结构,该结构被称为记忆集,这个结构把老年代划分为若干小块,标识出老年代的哪一块内存会存在跨代引用,此后当发生Minor GC时只有包含了跨带引用的小块内存里的对象才会被加入到GC Roots进行扫描。
- 部分收集(Partial GC)指目标不是完整的Java堆的垃圾收集
- 新生代收集(Minor GC/Young GC)指目标只是新生代的垃圾收集
- 老年代收集(Major GC/Old GC)指目标值是老年代的垃圾收集,目前只有CMS收集器会有单独收集老年代的行为
- 混合收集(Mixed GC)指目标是整个新生代以及部分老年代,目前只有G1有
- 整堆收集(Full GC)收集整个Java堆和方法区
标记清除算法(Mark Sweep)
首先标记出所有需要 回收/存活 的对象,然后统一回收掉所有 被标记/未被标记 的对象。
缺点
空间碎片化问题
标记复制算法
最初的半区复制算法:
将内存按容量划分为两块相等区域,在分配时只使用其中的一块。当其中一块内存用完时,就把还活着的对象复制到另一块内存区域上。
改进算法:
把新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生GC时,将Eden和Survivor中存活的对象复制到另一块Survivor,然后清理掉使用过的Eden和Survivor。
优点
实现简单,运行高效
缺点
当存活的对象占大多数,复制开销太大
标记整理算法(Mark Compact)
先执行一次标记,然后将所有存活的对象都向内存空间一端移动,然后直接清除掉边界以外的内存区域。
对象标记实现细节
以下内容仅针对HotSpot虚拟机
根节点枚举
可作为GC Roots的对象有以下:
- 在虚拟机栈中的引用对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻异常对象
- 所有被同步锁(Synchronized关键字)持有的对象
- 记忆集
所有收集器在跟节点枚举这一步骤必须是暂停用户线程的。对于收集器,去遍历所有的GC Roots是耗时的。在HotSpot中,引入了OopMap的数据结构,一旦类加载完成, HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来。
安全点
HotSpot虚拟机并不会为每一条指令都生成OopMap,它只会在特定的位置(安全点)记录这些信息。有了安全点的设定,虚拟机要求用户程序必须执行到安全点之后才能够暂停。常见的安全点生成指令有方法调用,循环跳转,异常跳转等。
为了让垃圾收集器发生时,所有的线程都运行到最近的安全点停顿下来,有两种方案可以采用:
-
抢先式中断
在发生垃圾收集时,系统先把所有用户线程全部中断,如果发现有用户线程不在安全点上,就恢复这条线程的执行,直到跑到安全点上
-
主动式中断
当要发生垃圾收集时不直接对线程操作,仅设置一个标志位,各线程执行过程中,会不断的轮询这个标志,一旦发现标志为真时,就在最近的安全点上主动挂起
安全区域
有一种场景,当用户线程不执行的时候,无法快速运行到安全点。如:用户线程处于Sleep状态或Blocked状态时,线程无法响应虚拟机的中断请求。此时虚拟机也不能等待用户线程恢复执行。为此,引入了安全区域。
安全区域指:能够确保某一段代码片段中,引用关系不会发生变化。因此虚拟机在这个片段中的任意地方开始垃圾收集都是安全的
当用户线程执行到安全区域中的代码时,首先会标志自己已经进入安全区域。当线程要离开安全区域时,首先检查虚拟机是否已经完成了根节点枚举。如果未完成,则等待。
记忆集与卡表
为了解决跨代或跨Region引用,引入了记忆集数据结构,可以避免把整个老年代加入GC Roots扫描范围。
不是上图当Region1中有对象引用Region2,并且Region3中也有对象引用Region2时,Region2的Remember Set就会有指向Region1以及Region3,这样当回收Region2,就会把Region1以及Region3单独加入到GC Roots的扫描范围。
垃圾收集场景中收集器只需通过记忆集判断出一块非收集区域是否存在有指向着收集区域的指针即可。
目前最常使用的记忆集实现方式为卡表(Remember Set)
卡表的简洁形式如下:
CARD_TABLE [this address >> 9] = 0
卡表中的每一个元素对应着一块固定大小的内存块(卡页)。
只要卡宴内有一个对象(或多个)的字段存在着跨代指针,那就将对应卡表元素的值标识为1,称这个元素变脏。在垃圾收集时,只需要筛选出卡表中变脏的元素,就能得出对应包含跨代指针的内存块,将之加入GC Roots中扫描
写屏障
查表元素在其他分在区域中对象引用了本区域对象时,其对应卡表元素就变脏。
在HotSpot虚拟机中通过写屏障去维护卡标状态。写屏障可以看作在虚拟机层面对引用类型字段赋值的AOP切面,在引用对象赋值时产生一个环形通知,示例代码如下
void oop_field_store(oop* field, oop new_value) {
// 引用字段赋值
*field = new_value;
// 写后屏障,在此完成卡表状态更新
post_write_barrier(field, new_value);
}
采用写屏障,有以下2个问题:
-
无条件写屏障带来的性能开销
每次对引用的更新,无论是否更新了老年代对新生代对象的引用,都会进行一次写屏障操作。
-
高并发下虚共享带来的性能开销
在高并发情况下,频繁的写屏障很容易发生伪共享。
假设CPU缓存行大小为64字节,由于一个卡表项占1个字节,这意味着,64个卡表项将共享同一个缓存行。
HotSpot每个卡页为512字节,那么一个缓存行将对应64个卡页一共64*512=32KB。
如果不同线程对对象引用的更新操作,恰好位于同一个32KB区域内,这将导致同时更新卡表的同一个缓存行,从而造成缓存行的写回、无效化或者同步操作,间接影响程序性能。
为了解决伪共享,一种简单的方法是先检查卡表标记,只有当卡表元素未被标记时,才将其标记为变脏
JDK7之后,为了解决无条件卡表更新,增加了-XX:+UseCondCardMark
,开启会增加一次额外判断的开销。
并发的可达性分析
为了分析并发的可达性,引入三色标记作为工具进行辅助推导,把遍历对象过程中遇到的对象,按照"是否访问过"这个条件标记为以下三种颜色
- 白色:表示对象尚未被垃圾收集器访问过
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少还有一个引用未被扫描
具体案例如下表
图片 | 示例 |
---|---|
初始状态只有GC Roots是黑色的 图中的箭头指引用是有方向的。对象只有被黑色对象引用才能存活 | |
扫描过程中,以灰色为波峰的波从黑向白推进 | |
扫描顺利完成,此时黑色对象就是存活对象,白色对象是以消亡可回收的对象 | |
但是如果用户现场在标记中修改了引用关系,如: 在扫描的灰色对象的一个引用被切断,同时这个对象与经被扫描过的黑对象建立了引用关系 | |
又比如这种切断后重新被黑色对象引用的对象,可能是原本应用链中的一部分, 由于黑色对象不会重新扫描,这将导致扫描结束后出现两个被黑色对象引用的对象仍是白色 |
“对象消失”需要同时满足以下两个条件
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用
- 赋值器删除了全部从灰色对象到白色对象的直接或间接引用
为了解决并发扫描时的对象消失问题,产生了两种解决方案:增量更新和原始快照
-
增量更新:
当黑色对象插入新的白色对象引用关系时,将这条引用记录下来,等并发扫描结束后,再以这些记录过的黑色为根,重新扫描一次
-
原始快照:
当灰色对象要删除指向白色对象的引用时,将这个要删除的引用记录下来,等并发扫描结束之后,再以这些记录过的灰色对象为根重新扫描一次
经典垃圾收集器
Serial收集器
Serial新生代采用标记复制算法、串行回收和“STW”机制
Serial Old采用标记整理算法、串行回收和“STW”机制
Serial Old是Client模式下老年代的默认GC
Serial Old在Server模式下主要有①与新生代的Parallel Scavenge配合使用②作为老年代的CMS的后备垃圾回收方案
运行示意图:
优点:算法简单,内存占用少,不用切换进程的开销
缺点:GC阶段的STW时间较长
参数总结
参数 | 使用 |
---|---|
-XX:+UseSerialGC | 指定新生代和老年代都是用Serial |
ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并发版本
对于新生代,使用ParNew多线程回收效率更高
在单核心收集器环境中效果并不会比Serial好
运行示意
参数总结
参数 | 使用 |
---|---|
-XX:+/-UseParNewGC | 指定使用 |
-XX:ParallelGCThreads | 限制线程数量,默认和CPU核心数相同 |
Parallel Scavenge收集器
Parallel Scavenge的目标是达到一个可控制的吞吐量
同样采用复制算法、并行回收和STW机制
高吞吐量更适合在后台运算不需要太多交互的任务
Parallel从1.6提供了老年代的回收器Parallel Old,用来替代Serial Old
运行示意图
参数总结
参数 | 使用 |
---|---|
-XX:+UseParallelGC | 新生代和老年代都使用Parallel |
-XX:ParallelGCThreads | 限制线程数量,默认和CPU核心数相同 |
-XX:MaxGCPauseMillis | 控制最大垃圾收集器停顿时间 |
-XX:GCTimeRatio | 设置吞吐量大小(0,100) |
-XX:+UseAdaptiveSizePolicy | 设置后无需人工置顶新生代Eden与Survivor区的比例 |
-XX:GCTimeRatio使用示例:
若-XX:GCTimeRatio设置为19,则表示允许最大垃圾收集时间占总时间的5%,即:(1/(1+19))
CMS收集器
CMS(Concurrent Mark Sweep)是一种以获取最短停顿时间为目标的收集器。
工作阶段如下:
- 初始标记(CMS initial mark):标记GC Roots能直接关联到的对象
- 并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始遍历整个对象图
- 重新标记(CMS remark):修正并发标记期间,因用户程序继续
- 并发清除(CMS concurrent sweep):清理删除掉标记阶段已经判定死亡的对象,由于不需要移动存活对象,所以可以与用户线程同时并发
CSM至少有以下三个缺点:
-
CMS对处理器资源非常敏感
在并发阶段会占用一部分线程导致应用程序变慢,降低总吞吐量。
CMS默认启动的回收线程数是(CPU核心数+3)/4
-
CMS无法处理"浮动垃圾"(CMS在并发标记和并发清理阶段,由于用户线程在继续运行,同时会伴随着部分垃圾对象的产生,这部分垃圾无法在本次收集中清除,只有等待下一次GC),如果浮动垃圾过多,则可能会出现"Concurrent Mode Failure",将会启动Serial Old备案,导致STW的Full GC
-
在清除阶段采用"标记-清除"算法将可能出现垃圾收集后产生大量空间碎片。在分配大对象时,可能触发Full GC
运行示意
参数总结
参数 | 使用 |
---|---|
-XX:+UseConcMarkSweepGC | 使用CMS收集器 |
-XX:ParallelCMSThreads | 设置CMS的线程数量 |
-XX:CMSInitiatingOccupancyFraction | 设置老年代内存的使用率的阈值,一但达到该阈值,则启用CMS进行回收 JDK6以后,CMS收集器的启动阈值默认提升至92%,即当老年代使用了92%的空间后就会激活垃圾回收 |
-XX:+UseCMSCompactAtFullCollection | 用于指定在Full GC之后进行一次空间整理,由于整理过程无法并发,因此停顿时间更长(JDK9之后开始废弃) |
-XX:CMSFullGCsBeforeCompaction | 用于指定CMS收集器,在执行N次不整理空间的Full GC之后,进入下一次Full GC前进行碎片整理 |
Garbage First收集器
G1收集器将连续的Java堆划分为多个大小相等的独立区域(Region),每个Region都可以根据需要扮演新生代Eden空间、Survivor空间或者老年代空间。
在Region中还有一类特殊的Humongous区域,专门用来存储大对象。只要对象超大小过0.5个Region容量即可判定为大对象
G1有计划的避免在整个Java堆中进行全区域的垃圾回收,G1跟踪每个region中的回收价值的大小(回收所获得的空间大小以及回收所花费的时间经验值),在后台维护一个优先级列表,根据每次允许的回收时间,从列表中优先回收价值最大的Region。
G1回收器有以下特点:
-
并行与并发
垃圾回收过程多线程并行
部分工作可与应用进行并发执行
-
分代收集
G1仍然属于分代的垃圾回收器
-
可预测的停顿时间模型
-
空间整合
从Region上看是标记复制算法,从整体上看是标记整理算法
G1垃圾回收过程
当年轻代Eden空间分配完时开始Yong GC(并发、独占),在Yong GC期间,暂停用户线程,多线程年轻代回收,然后将存活对象移动到Survivor区或老年代区或两者皆涉及
当堆内存占用达到一定值(默认45)时,启动老年代并发标记过程
标记完成后开始Mixed GC,对于老年代,一次回收部分Region
-
Yong GC
-
第一阶段:扫描GC Roots,连同记忆集(变脏的卡表)
-
第二阶段:更新Remember Set
处理变脏的元素,记忆集可以反应老年代对所处内存段中对象的引用
-
第三阶段:处理Remember Set
识别老年代指向Eden空间的对象,Eden空间中被老年代引用的对象可看作存活对象
-
第四阶段:复制对象
Eden空间中存活的对象会被复制到Survivor Region或Old Region,若Survivor中的空闲空间不足以存放存活对象,则对象担保机制会将该对象晋升到老年代空间
-
第五阶段:处理引用
处理 Soft、Weak、 Phantom、Final、JNI Weak 等引用,最终Eden空间数据为空,GC停止
-
-
并发标记过程
-
第一阶段:初始标记阶段
标记从GC Roots可直达的对象,在此过程中STW,且会触发一次Yong GC
-
第二阶段:根区域扫描(Root Region Scanning)
扫描Survivor区直接可达Old区的对象,并标记被引用的对象,这一过程需要在Yong GC之前完成
-
第三阶段:并发标记
在整个堆中进行并发标记,此过程可能被Yong GC中断。在此过程中,若发现某一区域对象全是垃圾对象,则会直接回收该区域。在并发标记过程中,会计算每个Region的对象活性(区域中存活对象的比例)
-
第四阶段:重新标记
此阶段STW,修正并发标记阶段用户线程更新的引用
-
第五阶段:独占清理
计算各个区域存活对象与GC回收比例,并排序。识别可以混合回收的区域,STW
此阶段并不会实际回收
-
第六阶段:并发清理
识别并清理空闲的区域
-
-
Mixed GC
回收整个年轻代以及部分老年代空间
-
Full GC
导致Full GC有以下两种可能:
- 回收时没有足够的空间存放晋升对象
- 并发处理中空间耗尽
G1收集器分区示意图
参数总结
参数 | 使用 |
---|---|
-XX:+UseG1GC | 使用G1垃圾收集器 |
-XX:G1HeapRegionSize | 指定每个Region的大小,取值范围:1MB-32MB,且应为2的N次幂 |
-XX:MaxGCPauseMillis | 设置期望的最大GC停顿时间(JVM会尽力实现,但不保证达到) |
-XX:ParallelGCThread | 设置STW工作线程数,最多设置为8 |
-XX:ConcGCThreads | 设置并发标记的线程数,值应为ParallelGCThread的1/4 |
-XX:InitiatingHeapOccupancyPercent | 设置触发并发GC周期的堆占用率,超过此值就触发GC,默认45 |