SoftReference 到底在什么时候被回收 ? 如何量化内存不足 ?

本文基于 OpenJDK17 进行讨论,垃圾回收器为 ZGC。

提示: 为了方便大家索引,特将在上篇文章 《以 ZGC 为例,谈一谈 JVM 是如何实现 Reference 语义的》 中讨论的众多主题独立出来。


大家在网上或者在其他讲解 JVM 的书籍中多多少少会看到这样一段关于 SoftReference 的描述 —— “当 SoftReference 所引用的 referent 对象在整个堆中没有其他强引用的时候,发生 GC 的时候,如果此时内存充足,那么这个 referent 对象就和其他强引用一样,不会被 GC 掉,如果此时内存不足,系统即将 OOM 之前,那么这个 referent 对象就会被当做垃圾回收掉”。

image

当然了,如果仅从概念上理解的话,这样描述就够了,但是如果我们从 JVM 的实现角度上来说,那这样的描述至少是不准确的,为什么呢 ? 笔者先提两个问题出来,大家可以先思考下:

  1. 内存充足的情况下,SoftReference 所引用的 referent 对象就一定不会被回收吗 ?

  2. 什么是内存不足 ?这个概念如何量化,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 policyconst 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 causeif (request.cause() == GCCause::_wb_full_gc ||request.cause() == GCCause::_metadata_GC_clear_soft_refs ||request.cause() == GCCause::_z_allocation_stall) {// 无条件清理 SoftReferencereturn true;}// Don't clearreturn false;
}

这里我们看到,在 ZGC 的过程中,只要满足以下三种情况中的任意一种,那么在 GC 过程中就会无条件地清理 SoftReference 。

  1. 引起 GC 的原因是 —— _wb_full_gc ,也就是由 WhiteBox 相关 API 触发的 Full GC,就会无条件清理 SoftReference。

  2. 引起 GC 的原因是 —— _metadata_GC_clear_soft_refs,也就是在元数据分配失败的时候触发的 Full GC,元空间内存不足,情况就很严重了,所以要无条件清理 SoftReference。

  3. 引起 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);// 判断是否进行阻塞分配 ZPageif (!alloc_page_or_stall(&allocation)) {// 如果非阻塞分配  ZPage 失败,直接 Out of memoryreturn 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 :

  1. always_clear_policy : 当 clear 为 true 的时候,ZGC 就会采用这种策略,在 GC 的过程中只要遇到 SoftReference,就会无条件回收其引用的 referent 对象,SoftReference 对象也会被 JVM 加入到 _reference_pending_list 中等待 ReferenceHandler 线程去处理。

  2. 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();// 转换为 MBmax_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,取决于两个因素:

  1. 当前 JVM 堆的最大剩余空间。

  2. 我们指定的 -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 listsZReferenceProcessorTask task(this);// gc _workers 一起运行 ZReferenceProcessorTask_workers->run(&task);// Update SoftReference clocksoft_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 的值一直是相等的。

image

如果一个 SoftReference 已经很久没有被访问了,timestamp 就会远远落后于 clock,因为在没有被访问的这段时间内可能已经发生好几次 GC 了。

image

在我们了解了这些背景之后,再来看一下 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 SoftReferencereturn false;}// Ask SoftReference policy// 获取 SoftReference 中的 clock 字段,这里存放的是上一次 gc 的时间戳const jlong clock = java_lang_ref_SoftReference::clock();// 判断是否应该清除这个 SoftReferencereturn !_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.timestampjlong 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 回收掉。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/27902.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

关于QTcreator,19年大学时写的文章了,之前写在印象笔记现在拉过来,往事如烟呐

1.初来乍到&#xff0c;先按照书本写一个基础列程理解一下原理。 这里创建工程的时候选择Qdialog基类&#xff0c;dialog.h头文件&#xff0c;并且勾选了创建界面 &#xff08;勾选之后可以通过手动添加组块并且可以自生成他们的函数定义&#xff0c;如果没有勾选&#xff0c;…

聊天页面样式

聊天页面样式 代码&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><link rel"styleshee…

C++程序员笔试训练

面试题1&#xff1a;使用库函数将数字转换位字符串 考点&#xff1a;c语言库函数中数字转换位字符串的使用 char *gcvt(double number, int ndigit, char *buf);参数说明&#xff1a; number&#xff1a;待转换的double类型数值。 ndigit&#xff1a;保留的小数位数。 buf&am…

数智教育创新如何向未来?腾讯云与你探索革新之路

引言 随着科技革命的快速发展&#xff0c;掀起教育领域的变革&#xff0c;新理念、新技术、新模式、新应用正不断涌现&#xff0c;正塑造着教育的未来形态。未来科技还将如何赋能教育创新&#xff1f; 5月31日&#xff0c;由腾讯云TVP 与西安电子科技大学联合举办的「数智教育的…

LC1020:飞地的数量

题目 给你一个大小为 m x n 的二进制矩阵 grid &#xff0c;其中 0 表示一个海洋单元格、1 表示一个陆地单元格。 一次 移动 是指从一个陆地单元格走到另一个相邻&#xff08;上、下、左、右&#xff09;的陆地单元格或跨过 grid 的边界。 返回网格中 无法 在任意次数的移动…

C++访问Private,Protecd的一些方法总结

前言 在编写C程序中 我们偶尔会碰到这样的三种特殊修改变量值的需求&#xff1a; [1]在不修改类原本的实现下&#xff0c;访问修改类的Private变量 [2]在不修改类原本的实现下&#xff0c;修改类的Protected变量 Private变量访问 public类模版函数特化 这种办法利用了类模…

CMS与AI的融合:构建万能表单小程序系统

引言&#xff1a; 随着人工智能技术的飞速发展&#xff0c;MyCMS作为一款功能强大的内容管理系统&#xff0c;通过集成AI技术&#xff0c;进一步拓展了其应用范围和智能化水平。本文将探讨如何利用MyCMS结合AI技术&#xff0c;构建一个能够将用户提交的万能表单数据转化为智能提…

【Ardiuno】实验使用ESP32单片机根据光线变化控制LED小灯开关(图文)

今天小飞鱼继续来实验ESP32的开发&#xff0c;这里使用关敏电阻来配合ESP32做一个我们平常接触比较多的根据光线变化开关灯的实验。当白天时有太阳光&#xff0c;则把小灯关闭&#xff1b;当光线不好或者黑天时&#xff0c;自动打开小灯。 int value;void setup() {pinMode(34…

前端老古董execCommand——操作 选中文本 样式

文章目录 ⭐前言⭐exe command api用法&#x1f496; example示例&#x1f496; 测试效果 ⭐execommand和getSelection 的联系⭐总结⭐结束 ⭐前言 大家好&#xff0c;我是yma16&#xff0c;本文分享关于 前端老古董execCommand——操作选中文本。 execommand 当一个 HTML 文…

基于CentOS Stream 9平台安装Redis7.0.15

1. 官方下载地址 https://redis.io/downloads/#redis-downloads 1.1 下载或上传到/opt/coisini目录下&#xff1a; mkdir /opt/coisini cd /opt/coisini wget https://download.redis.io/releases/redis-7.0.15.tar.gz2. 解压 tar -zxvf redis-7.0.15.tar.gz 3. 创建软连接 或…

算法:分治(快排)题目练习

目录 题目一&#xff1a;颜色分类 题目二&#xff1a;排序数组 题目三&#xff1a;数组中的第k个最大元素 题目四&#xff1a;库存管理III 题目一&#xff1a;颜色分类 给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums &#xff0c;原地对它们进行排序&#xff0c;…

【回文 马拉车】214. 最短回文串

本文涉及知识点 回文 马拉车 LeetCode214. 最短回文串 给定一个字符串 s&#xff0c;你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。 示例 1&#xff1a; 输入&#xff1a;s “aacecaaa” 输出&#xff1a;“aaacecaaa” 示…

【单元测试】Spring Boot 的测试库

Spring Boot 的测试库 1.了解回归测试框架 JUnit2.了解 assertThat3.了解 Mockito4.了解 JSONPath5.测试的回滚 单元测试&#xff08;unit test&#xff09;是为了检验程序的正确性。一个单元可能是单个 程序、类、对象、方法 等&#xff0c;它是应用程序的最小可测试部件。 单…

[大模型]XVERSE-7B-chat Transformers 推理

XVERSE-7B-Chat为XVERSE-7B模型对齐后的版本。 XVERSE-7B 是由深圳元象科技自主研发的支持多语言的大语言模型&#xff08;Large Language Model&#xff09;&#xff0c;参数规模为 70 亿&#xff0c;主要特点如下&#xff1a; 模型结构&#xff1a;XVERSE-7B 使用主流 Deco…

用于每个平台的最佳WordPress LMS主题

你已选择在 WordPress 上构建学习管理系统 (LMS)了。恭喜&#xff01; 你甚至可能已经选择了要使用的 LMS 插件&#xff0c;这已经是成功的一半了。 现在是时候弄清楚哪个 WordPress LMS 主题要与你的插件配对。 我将解释 LMS 主题和插件之间的区别&#xff0c;以便你了解要…

如何打开pak文件-翻译pak语言包

最近碰到一些程序的语言包是pak格式&#xff0c;用Notepad打开全是乱码&#xff0c;百度搜索了一下&#xff0c;pak是一种少见的压缩文件格式&#xff0c;是pak Quake系列游戏所采用的一种特殊压缩包格式&#xff0c;由Quake游戏公司开发&#xff0c;用高版本的winrar可以打开&…

测试 halcon算子 derivate_gauss 高斯一阶导数卷积

参上了 matlab fileexchange 有人上传了高斯 dx,dy一阶导卷积代码 卷积核的计算我修改成了核元素绝对值求做分母 归一化 和halcon的 derivate_gauss算子的计算结果对别如下 还是不知道怎么做到两者结果一致. 测试图像: 我的: halcon的: 获取两份图像的灰度值到数组并做对应位…

即时聊天系统

功能描述 该项目是一个前后端分离的即时聊天项目&#xff0c;前端采用vue2、后端使用springboot以mysql8.0作为数据库。 项目功能包含了单聊、群聊功能。在此基础上增加了对好友的功能操作&#xff0c;如备注设为通知、视频聊天、语音聊天、置顶、拉入黑名单、清空聊天记录等。…

【面试干货】Integer 和 int 的区别

【面试干货】Integer 和 int 的区别 1、基本类型与包装类型2、内存占用3、自动装箱与拆箱4、null 值5、常量池6、总结 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在Java中&#xff0c;Integer 和 int 是两种不同类型的变量&#xff0c;…

leetcode LRU 缓存

leetcode: LRU 缓存 LRU 全称为 Least Recently Used&#xff0c;最近最少使用&#xff0c;常常用于缓存机制&#xff0c;比如 cpu 的 cache 缓存&#xff0c;使用了 LRU 算法。LRU 用于缓存机制时&#xff0c;关键的是当缓存满的时候有新数据需要加载到缓存的&#xff0c;这个…