一、前文回顾
在 细说Java 引用(强、软、弱、虚)和 GC 流程(一) 我们对Java 引用有了总体的认识,本文将继续深入分析 Java 引用在 GC 时的一些细节。
还是从我们在前文中提到的引用流程图里说起,这里不清楚的请回 细说Java 引用(强、软、弱、虚)和 GC 流程(一) 中查阅。
本文将重点关注图示{3}这部分细节:
- GC线程是如何把Reference对象收集到pending队列的?
- Reference对象和我们正常的对象有什么不同,为啥{1}是强引用,{2}是非强引用?
- 按照GC可达性算法,就算{1}不存在,但通过{2}依然可以找到我们的对象,那对象就是可达的啊?
图 0-1—— GC可达性分析图
我们先来看引用对象和正常对象里字段的区别,即上图对象weakReference
里的referent
和 xx_oo
里的 _oo
的秘密?
二、引用可达
按照GC可达性算法,【图 0-1—— GC可达性分析图】中,对象oo
和 o
均是可达的,但是GC时,GC线程扫描到引用对象weakReference
时,会跳过实例变量referent
的扫描,从而导致对象o
不可达;接下来让我们一探究竟。
1.1、 OopMapBlock 简析
1.1.1、 OopMapBlock 概述
GC扫描实例对象时,会通过一个叫做OopMapBlock
的类(C++写的),这个类里存放了我们java类的引用类型(基本类型不会存放)的成员变量;换言之,OopMapBlock
类里存在变量X,那就可以顺藤摸瓜找到X对象,否则,X就是不可达;
我们的Reference
类对应的OopMapBlock
类中就跳过了变量referent
;
实际使用时若我们在一个java类里定义了成百上千的引用类型变量,那OopMapBlock
岂不是也得存放成百上千的引用型变量,这还了得;所以为了避免这种情况,OopMapBlock
里只有2个变量:
- _offset:连续实例变量中第一个实例变量位置;
- _count:连续实例变量个数;
注意,这里只记录了一个java类里连续的实例变量(静态变量本来就是GC-Root,所以不需要记录),如下图,直接跳过静态变量,递归遍历实例变量,就可以知道java对象是否可达。
如果实例变量被静态变量隔开,那就再来一个OopMapBlock
,把所有OopMapBlock
放入数组即可;所以一个java类中存在一个OopMapBlock
数组。这个数组是JVM加载类时动态分析后生成的,然后把这个数组存放在了 InstanceKlass
对象中(可以查阅 JVM层面的JAVA类和实例(Klass-OOP) 了解InstanceKlass
的知识)。
java 实例对象头部中有 Klass 类型指针;这样,GC线程扫描堆中实例对象时就可以通过InstanceKlass
对象找到这个OopMapBlock
,并据此构造引用链,标记对象是否可达。
- java中普通类在JVM层面对应的是
InstanceKlass
对象;- java.lang.ref.Reference对应的是
InstanceRefKlass
对象;- 以上区分就是为了针对类
java.lang.ref.Reference
做定制化的OopMapBlock
,从而跳过变量referent
的引用扫描;InstanceKlass
对象和InstanceRefKlass
对象均存放再JVM方法区中;
1.1.2、 OopMapBlock 代码
// Describes where oops are located in instances of this klass.
class OopMapBlock {
public:
// Byte offset of the first oop mapped by this block.
int offset() const { return _offset; }
void set_offset(int offset) { _offset = offset; }
// Number of oops in this block.
uint count() const { return _count; }
void set_count(uint count) { _count = count; }
private:
int _offset;
uint _count;
};
InstanceKlass
使用了 OopMapBlock
:
1.2、 GC时引用对象收集流程
解决完引用对象的可达性问题,我们来看引用对象是怎么被发现和收集到 pendling
队列中的 ,即文章开始提到的【图 0-0—— 引用流程图】中图示{3}的细节。
1.2.1、标记阶段
这个阶段中,GC线程和应用线程并发执行,并没有产生 STW(Stop The world);这个阶段主要做的是找到可以回收的引用对象,并全部收集起来。
-
如下图所示,我们假设有2个GC-ROOT,分别为
GC-ROOT_1
和GC-ROOT_2
; -
一个
_discovered_list
队列用于临时存放 GC 线程在并发标记过程中发现需要回收的Reference
对象;每一个 GC 线程都有一个
_discovered_list
;并发标记结束之后,这些 GC 线程就会将各自在
_discovered_list
中收集到的Reference
对象统一转移到 pending 队列中,以便后续ReferencHandler
线程消费; -
_discovered_list入队条件:
- Reference 对象引用的 referent 没有被 GC 标记过,图示 obj_c;
- Reference 对象的状态不能是 inactive, 也就是说这个 Reference 对象还没有被应用线程处理过,Reference 之前没有加入过 _discovered_list,图示WeakReference_x;
- referent 不存在任何强引用链,图示 obj_c,referent指的就是obj_c;
- 内存充足的前提下,referent 不存在任何软引用(若内存不足,就忽略这条);
-
_nonstatic_oop_maps
是InstanceKlass
对象的变量,存放就是我们之前提到的OopMapBlock
数组;
- GC线程从
GC-ROOT_1
出发标记对象;- 标记对象
obj_a
、obj_b
状态为存活;obj_c
、obj_p
、obj_q
、obj_d
、obj_f
对象均不可达;- 遍历到引用对象
SofeReference_a
,发现SofeReference_a
的referent
即对象obj_b
为存活,所以放弃将SofeReference_a
加入到_discovered_list
队列中;- 遍历到引用对象
WeakReference_b
,发现WeakReference_b
的referent
即对象obj_c
还未标记,先将SofeReference_b
加入到_discovered_list
队列中,这里采用头插法;同理,引用对象WeakReference_z
、WeakReference_x
也加入到_discovered_list
队列;WeakReference_y
不会遍历到,已经是垃圾了,不会入队到_discovered_list
队列;- GC线程从
GC-ROOT_2
出发标记对象;FinalReference_q
对象因为后续要执行obj_q.finalize()
方法;所以需要将obj_q
对象重新标记为复活状态;同时将FinalReference_q
对象加入到_discovered_list
队列;
1.2.2、二次确认阶段
本阶段将进一步确认阶段一中的_discovered_list
,将标记错误Reference
对象的移出队列,标记正确的Reference
对象将其referent
置为null
,即断开与obj
对象的连接;
注意FinalReference对象 :
FinalReference
对象不会断开与obj的连接,方便后面执行obj.finalize()方法;- 当后面执行完
obj.finalize()
方法后,referent 才会被置为 null , 在下一轮 GC 的时候, 这个FinalReference
对象以及它的 referent (obj_q)对象就会被 GC 掉;
- 如下图所示,惊不惊喜,意不意外,
SofeReference_a
对象竟然在我们的队列里,这明显有问题啊,问题是上面我们说了放弃将SofeReference_a
入队啊,怎么回事?当阶段一中,图示{3} 标记快于 {1},遍历到
SofeReference_a
时发现obj_b
还未标记为可达,有可能进入_discovered_list
队列; WeakReference_x
对象也是标记错误的,因为阶段一中应用线程并未暂停,所有应用线程有可能将WeakReference_x
自己处理了,我们收集引用对象的目的就是为了给应用线程后续处理,既然应用线程提前处理了,那GC线程没必要多此一举;
1.2.3、汇总阶段
在阶段一我们提到过每一个 GC 线程都有一个_discovered_list
,所以需要将这些线程的_discovered_list
统一收集到一起放在_pending_list
中,然后再将数据转移到_reference_pending_list
,腾出_pending_list
空间,方便下次GC使用。
至此,我们已经可以回答文章开始提到的问题了。
1.3、 软引用GC时的处理
我们都知道,在内存不足时,只有软引用的对象才会被回收,那什么才是内存不足?或者说只有软引用的对象在什么情况下一定会被回收?我们需要有一个量化标准:
-
发生Full GC时一定会回收软引用,这很明显,毋庸置疑;
-
只有软引用的对象存活时间达到我们设定的生命周期阈值;
JVM提供了参数
-XX:SoftRefLRUPolicyMSPerMB
可以设置每 MB 的堆内存剩余空间允许只有软引用的对象存活的最大时长,默认为 1000 , 单位为毫秒(MS);参数
-XX:SoftRefLRUPolicyMSPerMB
中有LRU
,说明这个参数可以按照LRU(Least Recently Used,即最近最少使用)策略调整,即并非所有的软引用对象一起被GC掉;参数
-XX:SoftRefLRUPolicyMSPerMB
中有PerMB
,所以我们GC时需要计算剩余空间,二者乘积就是我们要的最终结果;
举例:-XX:SoftRefLRUPolicyMSPerMB=2000
,剩余空间为20MB;则存活时间为40秒(20 * 2000);
1.3.1、 软引用LRU策略
如上图所示,SoftReference
中有两个字段:
- clock:clock 字段是由 JVM 来设置的,在每一次发生 GC 的时候,JVM 都会去更新这个时间戳。
- timestamp:每次调用
get()
方法获取referent
时,更新为上次GC的时间;
对于当前只有软引用的对象而言,如果 clock - timestamp >= 剩余空间 *
SoftRefLRUPolicyMSPerMB
时,则当前只有软引用的对象就可以直接回收了,也就是可以加入到我们在1.2.1小节中提到的_discovered_list
队列中了;