本文基于 OpenJDK17 进行讨论,垃圾回收器为 ZGC。
提示: 为了方便大家索引,特将在上篇文章 《以 ZGC 为例,谈一谈 JVM 是如何实现 Reference 语义的》 中讨论的众多主题独立出来。
大家在网上或者在其他讲解 JVM 的书籍中多多少少会看到这样一段关于 SoftReference 的描述 ------ "当 SoftReference 所引用的 referent 对象在整个堆中没有其他强引用的时候,发生 GC 的时候,如果此时内存充足,那么这个 referent 对象就和其他强引用一样,不会被 GC 掉,如果此时内存不足,系统即将 OOM 之前,那么这个 referent 对象就会被当做垃圾回收掉"。
当然了,如果仅从概念上理解的话,这样描述就够了,但是如果我们从 JVM 的实现角度上来说,那这样的描述至少是不准确的,为什么呢 ? 笔者先提两个问题出来,大家可以先思考下:
-
内存充足的情况下,SoftReference 所引用的 referent 对象就一定不会被回收吗 ?
-
什么是内存不足 ?这个概念如何量化,SoftReference 所引用的 referent 对象到底什么时候被回收 ?
下面笔者继续以 ZGC 为例,带大家深入到 JVM 内部去探寻下这两个问题的精确答案~~
1. JVM 无条件回收 SoftReference 的场景
经过前面第五小节的介绍,我们知道 ZGC 在 Concurrent Mark 以及 Concurrent Process Non-Strong References 阶段中处理 Reference 对象的关键逻辑都封装在 ZReferenceProcessor
中。
在 ZReferenceProcessor 中有一个关键的属性 ------ _soft_reference_policy,在 ZGC 的过程中,处理 SoftReference 的策略就封装在这里,本小节开头提出的那两个问题的答案就隐藏在 _soft_reference_policy 中。
c
class ZReferenceProcessor : public ReferenceDiscoverer {
// 关于 SoftReference 的处理策略
ReferencePolicy* _soft_reference_policy;
}
那下面的问题就是如果我们能够知道 _soft_reference_policy 的初始化逻辑,那是不是关于 SoftReference 的一切疑惑就迎刃而解了 ?我们来一起看下 _soft_reference_policy 的初始化过程。
在 ZGC 开始的时候,首先会创建一个 ZDriverGCScope 对象,这里主要进行一些 GC 的准备工作,比如更新 GC 的相关统计信息,设置并行 GC 线程个数,以及本小节的重点,初始化 SoftReference 的处理策略 ------ _soft_reference_policy。
c
void ZDriver::gc(const ZDriverRequest& request) {
ZDriverGCScope scope(request);
..... 省略 ......
}
c
class ZDriverGCScope : public StackObj {
private:
GCCause::Cause _gc_cause;
public:
ZDriverGCScope(const ZDriverRequest& request) :
_gc_cause(request.cause()),
{
// Set up soft reference policy
const bool clear = should_clear_soft_references(request);
ZHeap::heap()->set_soft_reference_policy(clear);
}
在 JVM 开始初始化 _soft_reference_policy 之前,会调用一个重要的方法 ------ should_clear_soft_references,本小节的答案就在这里,该方法就是用来判断,ZGC 是否需要无条件清理 SoftReference 所引用的 referent 对象。
-
返回 true 表示,在 GC 的过程中只要遇到 SoftReference 对象,那么它引用的 referent 对象就会被当做垃圾清理,SoftReference 对象也会被 JVM 加入到 _reference_pending_list 中等待 ReferenceHandler 线程去处理。这里就和 WeakReference 的语义一样了。
-
返回 false 表示,内存充足的时候,JVM 就会把 SoftReference 当做普通的强引用一样处理,它所引用的 referent 对象不会被回收,但内存不足的时候,被 SoftReference 所引用的 referent 对象就会被回收,SoftReference 也会被加入到 _reference_pending_list 中。
c
static bool should_clear_soft_references(const ZDriverRequest& request) {
// Clear soft references if implied by the GC cause
if (request.cause() == GCCause::_wb_full_gc ||
request.cause() == GCCause::_metadata_GC_clear_soft_refs ||
request.cause() == GCCause::_z_allocation_stall) {
// 无条件清理 SoftReference
return true;
}
// Don't clear
return false;
}
这里我们看到,在 ZGC 的过程中,只要满足以下三种情况中的任意一种,那么在 GC 过程中就会无条件地清理 SoftReference 。
-
引起 GC 的原因是 ------
_wb_full_gc
,也就是由WhiteBox
相关 API 触发的 Full GC,就会无条件清理 SoftReference。 -
引起 GC 的原因是 ------
_metadata_GC_clear_soft_refs
,也就是在元数据分配失败的时候触发的 Full GC,元空间内存不足,情况就很严重了,所以要无条件清理 SoftReference。 -
引起 GC 的原因是 ------
_z_allocation_stall
,在 ZGC 采用阻塞模式分配 Zpage 页面的时候,如果内存不足无法分配,那么就会触发一次 GC,这时 GC 的触发原因就是 _z_allocation_stall,这种情况下就会无条件清理 SoftReference。
ZGC 非阻塞模式分配 Zpage 的时候如果内存不足、就直接抛出 OutOfMemoryError,不会启动 GC 。
c
ZPage* ZPageAllocator::alloc_page(uint8_t type, size_t size, ZAllocationFlags flags) {
EventZPageAllocation event;
retry:
ZPageAllocation allocation(type, size, flags);
// 判断是否进行阻塞分配 ZPage
if (!alloc_page_or_stall(&allocation)) {
// 如果非阻塞分配 ZPage 失败,直接 Out of memory
return NULL;
}
}
在我们了解了这个背景之后,在回头来看下 _soft_reference_policy 的初始化过程 :
参数 clear 就是 should_clear_soft_references 函数的返回值
c
void ZReferenceProcessor::set_soft_reference_policy(bool clear) {
static AlwaysClearPolicy always_clear_policy;
static LRUMaxHeapPolicy lru_max_heap_policy;
if (clear) {
log_info(gc, ref)("Clearing All SoftReferences");
_soft_reference_policy = &always_clear_policy;
} else {
_soft_reference_policy = &lru_max_heap_policy;
}
_soft_reference_policy->setup();
}
ZGC 采用了两种策略来处理 SoftReference :
-
always_clear_policy : 当 clear 为 true 的时候,ZGC 就会采用这种策略,在 GC 的过程中只要遇到 SoftReference,就会无条件回收其引用的 referent 对象,SoftReference 对象也会被 JVM 加入到 _reference_pending_list 中等待 ReferenceHandler 线程去处理。
-
lru_max_heap_policy :当 clear 为 false 的时候,ZGC 就会采用这种策略,这种情况下 SoftReference 的存活时间取决于 JVM 堆中剩余可用内存的总大小,也是我们下一小节中讨论的重点。
下面我们就来看一下 lru_max_heap_policy 的初始化过程,看看 JVM 是如何量化内存不足的 ~~
2. JVM 如何量化内存不足
LRUMaxHeapPolicy 的 setup()
方法主要用来确定被 SoftReference 所引用的 referent 对象最大的存活时间,这个存活时间是和堆的剩余空间大小有关系的,也就是堆的剩余空间越大 SoftReference 的存活时间就越长,堆的剩余空间越小 SoftReference 的存活时间就越短。
c
void LRUMaxHeapPolicy::setup() {
size_t max_heap = MaxHeapSize;
// 获取最近一次 gc 之后,JVM 堆的最大剩余空间
max_heap -= Universe::heap()->used_at_last_gc();
// 转换为 MB
max_heap /= M;
// -XX:SoftRefLRUPolicyMSPerMB 默认为 1000 ,单位毫秒
// 表示每 MB 的剩余内存空间中允许 SoftReference 存活的最大时间
_max_interval = max_heap * SoftRefLRUPolicyMSPerMB;
assert(_max_interval >= 0,"Sanity check");
}
JVM 首先会获取我们通过 -Xmx
参数指定的最大堆 ------ MaxHeapSize,然后在通过 Universe::heap()->used_at_last_gc()
获取上一次 GC 之后 JVM 堆占用的空间,两者相减,就得到了当前 JVM 堆的最大剩余内存空间,并将单位转换为 MB
。
现在 JVM 堆的剩余空间我们计算出来了,那如何根据这个 max_heap
计算 SoftReference 的最大存活时间呢 ?
这里就用到了一个 JVM 参数 ------ SoftRefLRUPolicyMSPerMB,我们可以通过 -XX:SoftRefLRUPolicyMSPerMB
来指定,默认为 1000 , 单位为毫秒。
它表达的意思是每 MB 的堆剩余内存空间允许 SoftReference 存活的最大时长,比如当前堆中只剩余 1MB 的内存空间,那么 SoftReference 的最大存活时间就是 1000 ms,如果剩余内存空间为 2MB,那么 SoftReference 的最大存活时间就是 2000 ms 。
现在我们剩余 max_heap 的空间,那么在本轮 GC 中,SoftReference 的最大存活时间就是 ------ _max_interval = max_heap * SoftRefLRUPolicyMSPerMB
。
从这里我们可以看出 SoftReference 的最大存活时间 _max_interval,取决于两个因素:
-
当前 JVM 堆的最大剩余空间。
-
我们指定的
-XX:SoftRefLRUPolicyMSPerMB
参数值,这个值越大 SoftReference 存活的时间就越久,这个值越小,SoftReference 存活的时间就越短。
在我们得到了这个 _max_interval 之后,那么 JVM 是如何量化内存不足呢 ?被 SoftReference 引用的这个 referent 对象到底什么被回收 ?让我们再次回到 JDK 中,来看一下 SoftReference 的实现:
java
public class SoftReference<T> extends Reference<T> {
// 由 JVM 来设置,每次 GC 发生的时候,JVM 都会记录一个时间戳到这个 clock 字段中
private static long clock;
// 表示应用线程最近一次访问这个 SoftReference 的时间戳(当前的 clock 值)
// 在 SoftReference 的 get 方法中设置
private long timestamp;
public SoftReference(T referent) {
super(referent);
this.timestamp = clock;
}
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
// 将最近一次的 gc 发生时间设置到 timestamp 中
// 用这个表示当前 SoftReference 最近被访问的时间戳
// 注意这里的时间戳语义是 最近一次的 gc 时间
this.timestamp = clock;
return o;
}
}
SoftReference 中有两个非常重要的字段,一个是 clock ,另一个是 timestamp。clock 字段是由 JVM 来设置的,在每一次发生 GC 的时候,JVM 都会去更新这个时间戳。具体一点的话,就是在 ZGC 的 Concurrent Process Non-Strong References 阶段处理完所有 Reference 对象之后,JVM 就会来更新这个 clock 字段。
c
void ZReferenceProcessor::process_references() {
ZStatTimer timer(ZSubPhaseConcurrentReferencesProcess);
// Process discovered lists
ZReferenceProcessorTask task(this);
// gc _workers 一起运行 ZReferenceProcessorTask
_workers->run(&task);
// Update SoftReference clock
soft_reference_update_clock();
}
在 soft_reference_update_clock()
中 ,JVM 会将 SoftReference 类中的 clock 字段更新为当前时间戳,单位为毫秒。
c
static void soft_reference_update_clock() {
const jlong now = os::javaTimeNanos() / NANOSECS_PER_MILLISEC;
java_lang_ref_SoftReference::set_clock(now);
}
而 timestamp 字段用来表示这个 SoftReference 对象有多久没有被访问到了,应用线程越久没有访问 SoftReference,JVM 就越倾向于回收它的 referent 对象。这也是 LRUMaxHeapPolicy 策略中 LRU
的语义体现。
应用线程在每次调用 SoftReference 的 get 方法时候,都会将最近一次的 GC 时间戳 clock 更新到 timestamp 中,这样一来,如果一个 SoftReference 被频繁的访问,那么 clock 和 timestamp 的值一直是相等的。
如果一个 SoftReference 已经很久没有被访问了,timestamp 就会远远落后于 clock,因为在没有被访问的这段时间内可能已经发生好几次 GC 了。
在我们了解了这些背景之后,再来看一下 JVM 对于 SoftReference 的回收过程,在本文 5.1 小节中介绍的 ZGC Concurrent Mark 阶段中,当 GC 遍历到一个 Reference 类型的对象的时候,会在 should_discover 方法中判断一下这个 Reference 对象所引用的 referent 是否被标记过。如果 referent 没有被标记为 alive , 那么接下来就会将这个 Reference 对象放入 _discovered_list 中,等待后续被 ReferenHandler 处理,referent 也会在本轮 GC 中被回收掉。
c
bool ZReferenceProcessor::should_discover(oop reference, ReferenceType type) const {
// 此时 Reference 的状态就是 inactive,那么这里将不会重复将 Reference 添加到 _discovered_list 重复处理
if (is_inactive(reference, referent, type)) {
return false;
}
// referent 还被强引用关联,那么 return false 也就是说不能被加入到 discover list 中
if (is_strongly_live(referent)) {
return false;
}
// referent 现在只被软引用关联,那么就需要通过 LRUMaxHeapPolicy
// 来判断这个 SoftReference 所引用的 referent 是否应该存活
if (is_softly_live(reference, type)) {
return false;
}
return true;
}
如果当前遍历到的 Reference 对象是 SoftReference 类型的,那么就需要在 is_softly_live
方法中根据前面介绍的 LRUMaxHeapPolicy 来判断这个 SoftReference 引用的 referent 对象是否满足存活的条件。
c
bool ZReferenceProcessor::is_softly_live(oop reference, ReferenceType type) const {
if (type != REF_SOFT) {
// Not a SoftReference
return false;
}
// Ask SoftReference policy
// 获取 SoftReference 中的 clock 字段,这里存放的是上一次 gc 的时间戳
const jlong clock = java_lang_ref_SoftReference::clock();
// 判断是否应该清除这个 SoftReference
return !_soft_reference_policy->should_clear_reference(reference, clock);
}
通过 java_lang_ref_SoftReference::clock()
获取到的就是前面介绍的 SoftReference.clock
字段 ------ timestamp_clock。
通过 java_lang_ref_SoftReference::timestamp(p)
获取到的就是前面介绍的 SoftReference.timestamp
字段。
如果 SoftReference.clock 与 SoftReference.timestamp 的差值 ------ interval,小于等于
前面介绍的 SoftReference 最大存活时间 ------ _max_interval,那么这个 SoftReference 所引用的 referent 对象在本轮 GC 中就不会被回收,SoftReference 对象也不会被放到 _reference_pending_list 中被 ReferenceHandler 线程处理。
c
// The oop passed in is the SoftReference object, and not
// the object the SoftReference points to.
bool LRUMaxHeapPolicy::should_clear_reference(oop p,
jlong timestamp_clock) {
// 相当于 SoftReference.clock - SoftReference.timestamp
jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
// The interval will be zero if the ref was accessed since the last scavenge/gc.
// 如果 clock 与 timestamp 的差值小于等于 _max_interval (SoftReference 的最大存活时间)
if(interval <= _max_interval) {
// SoftReference 所引用的 referent 对象在本轮 GC 中就不会被回收
return false;
}
// interval 大于 _max_interval,这个 SoftReference 所引用的 referent 对象就会被回收
// SoftReference 也会被放到 _reference_pending_list 中等待 ReferenceHandler 线程去处理
return true;
}
如果 interval 大于
_max_interval,那么这个 SoftReference 所引用的 referent 对象在本轮 GC 中就会被回收,SoftReference 对象也会被 JVM 放到 _reference_pending_list 中等待 ReferenceHandler 线程处理。
总结
从以上过程中我们可以看出,SoftReference 被 ZGC 回收的精确时机是,当一个 SoftReference 对象已经很久很久没有被应用线程访问到了,那么发生 GC 的时候这个 SoftReference 就会被回收掉。
具体多久呢 ? 就是 _max_interval 指定的 SoftReference 最大存活时间,这个时间由当前 JVM 堆的最大剩余空间和 -XX:SoftRefLRUPolicyMSPerMB
共同决定。
比如,发生 GC 的时候,当前堆的最大剩余空间为 1MB,SoftRefLRUPolicyMSPerMB 指定的是 1000 ms ,那么当一个 SoftReference 对象超过 1000 ms 没有被应用线程访问的时候,就会被 ZGC 回收掉。