SoftReference 到底在什么时候被回收 ? 如何量化内存不足 ?
本文基于 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 中。
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。
void ZDriver::gc(const ZDriverRequest& request) {
ZDriverGCScope scope(request);
..... 省略 ......
}
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 中。
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 。
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 函数的返回值
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 的存活时间就越短。
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 的实现:
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 字段。
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 字段更新为当前时间戳,单位为毫秒。
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 中被回收掉。
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 对象是否满足存活的条件。
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 线程处理。
// 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 回收掉。
SoftReference 到底在什么时候被回收 ? 如何量化内存不足 ?的更多相关文章
- 160930、Javascript的垃圾回收机制与内存管理
一.垃圾回收机制-GC Javascript具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存. 原理:垃圾收集器会定期(周期性 ...
- 你不知道的JavaScript--Item28 垃圾回收机制与内存管理
1.垃圾回收机制-GC Javascript具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存. 原理:垃圾收集器会定期(周期性 ...
- Java垃圾回收算法和内存分配策略
垃圾回收算法和内存分配策略 Java垃圾回收 垃圾收集,也就是GC并不是Java的伴生物,而对于GC的所需要完成任务主要就是: 1.哪些内存是需要回收的? 2.何时去回收这些内存? 3.以何种方式去回 ...
- javascript的垃圾回收机制与内存管理
一.垃圾回收机制—GC Javascript具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存. 原理:垃圾收集器会定期(周期性 ...
- java 垃圾回收(堆内存)、以及栈内存的释放
一.Java的垃圾回收机制———解疑 Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间. 需要注意的是:垃圾回收回收的是无任何引用的 ...
- python垃圾回收,判断内存占用,手动回收内存,二
以下为例子,判断计算机内存并释放程序内存. # coding=utf8 import time import psutil, gc, commands,os from logger_until imp ...
- Java垃圾回收机制和内存分配
收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现 自动内存管理解决的是:给对象分配内存 以及 回收分配给对象的内存 为什么我们要了解学习 GC 与内存分配呢? 在 JVM 自动内存管理机制的 ...
- Chrome V8系列--浅析Chrome V8引擎中的垃圾回收机制和内存泄露优化策略
V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制.因此,V8 将内存(堆)分为新生代和老生代两部分. 一.前言 V8的垃圾回收机制:JavaScript使用垃圾回收机制来自动管理内存.垃圾 ...
- JavaScript的垃圾回收机制与内存泄漏
常用的两种算法: 引用计数(新版浏览器已弃用,弃用原因:会出现循环引用的情况,无法进行垃圾回收,导致内存泄漏) 标记清除 引用计数法 引用计数,顾名思义一个对象是否有指向它的引用,即看栈中是否有指向要 ...
- JS基础-垃圾回收机制与内存泄漏的优化
[V8引擎]浅析Chrome V8引擎中的垃圾回收机制和内存泄露优化策略 垃圾回收机制 如何判断回收内容 如何确定哪些内存需要回收,哪些内存不需要回收,这是垃圾回收期需要解决的最基本问题.我们可以这样 ...
随机推荐
- 云上安全保护伞--SLS威胁情报集成实战
简介: 威胁情报是某种基于证据的知识,包括上下文.机制.标示.含义和能够执行的建议. 什么是威胁情报 根据Gartner对威胁情报的定义,威胁情报是某种基于证据的知识,包括上下文.机制.标示.含义和能 ...
- WPF 字体 FontStyle 的 Italic 和 Oblique 的区别
本文介绍在 WPF 里面的字体属性 FontStyle 的 Italic 和 Oblique 的斜体差别 本文的图片和知识来自: #265 – Specifying Values for FontSt ...
- dotnet C# 通过 Vortice 使用 Direct2D 特效入门
本文将告诉大家如何通过 Vortice 使用 D2D 的特效 本文属于 DirectX 系列博客,更多 DirectX 和 D2D 以及 Vortice 库的博客,请参阅我的 博客导航 上一篇: Di ...
- dotnet OpenXML 读取形状轮廓线条样式序号超过主题样式列表数
在 OpenXML 中,默认的形状可以通过指定 LineReference 让形状使用文档主题里面的样式.文档主题里面包含多个样式,在形状里面指定样式通过的是序号的方法,如果在形状里面指定的序号超过了 ...
- RK 平台安装 ubuntu 系统
一.简介 之前有介绍到 ARM 平台移植 ubuntu 的操作流程,在 RK 系列的平台同样适用,所以这里就不介绍怎么一步步的去对 ubuntu 进行移植,而是怎么将移植的过程编写成脚本,这样便可以在 ...
- B/S 结构系统的 缓存机制(Cookie) 以及基于 cookie 机制实现 oa 十天免登录的功能
B/S 结构系统的 缓存机制(Cookie) 以及基于 cookie 机制实现 oa 十天免登录的功能 @ 目录 B/S 结构系统的 缓存机制(Cookie) 以及基于 cookie 机制实现 oa ...
- ansible系列(1)--ansible基础
目录 1. ansible概述 1.1 ansible的功能 1.2 ansible的特性 1.3 ansible的架构 1.4 ansible注意事项 1. ansible概述 Ansible 是一 ...
- Oracle中ALTER TABLE的五种用法(三)
首发微信公众号:SQL数据库运维 原文链接:https://mp.weixin.qq.com/s?__biz=MzI1NTQyNzg3MQ==&mid=2247485212&idx=1 ...
- GEOJSON 的渲染实例
createGeojson:function(arr) { let geoArr=[]; for(let i=0;i<arr.length;i++) { let obj={ "type ...
- MySQL如何快速获取binlog的开始时间和结束时间
之前写过一篇文章MySQL如何获取binlog的开始时间和结束时间[1],文章里面介绍了如何获取MySQL数据库二进制日志(binlog)的开始时间与结束时间的一些方法.实际应用当中,我们可能还会遇到 ...