news 2026/2/25 12:42:30

技术演进中的开发沉思-328 JVM:垃圾回收(上)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
技术演进中的开发沉思-328 JVM:垃圾回收(上)

在 JVM 的内存管理中,“判定对象是否存活” 是 GC 的核心前提 —— 如果把 GC 比作 JVM 的 “垃圾清洁工”,那可达性分析算法就是 “清洁工的判定标准”,引用类型就是 “给对象贴的不同标签”:有的对象(强引用)再占内存也不能清,有的对象(软引用)内存不够时再清,有的对象(弱引用)一打扫就清。我早年做电商缓存系统时,因不懂软引用,用强引用存储商品图片缓存,导致堆内存持续上涨触发 OOM;后来改用软引用,内存不足时 GC 自动回收缓存,系统稳定性大幅提升。读懂可达性分析和引用类型,是从 “被动应对 GC” 到 “主动利用 GC” 的关键,也是写出低内存泄漏代码的核心。

一、可达性分析算法

JVM 判定对象是否存活,不靠 “对象是否被使用” 这种模糊的标准,而是靠可达性分析算法—— 这是一种 “根溯源” 的判定逻辑,也是现代 JVM(HotSpot、J9 等)的标配算法。

1. 核心逻辑

我常把可达性分析比作 “树的枝干”:

  • GC Roots(GC 根节点):是树的 “主根”,绝对不会被 GC 回收;
  • 对象引用链:是从主根延伸出的 “枝干”,对象是 “叶子”;
  • 判定规则:如果一个对象能通过任意引用链连接到 GC Roots,就是 “可达对象”(存活);如果没有任何引用链连接到 GC Roots,就是 “不可达对象”(标记为可回收)。

简单说:可达 = 存活,不可达 = 可回收(注意:不可达不代表立刻回收,还需经过 “标记 - 清除” 等 GC 流程)。

2. GC Roots 的具体类型

GC Roots 不是 “随便的对象”,必须是 JVM 认定的 “绝对存活” 的对象,主要包含以下 4 类(新手记这 4 类就够):

GC Roots 类型典型例子实战说明
虚拟机栈中引用的对象方法的局部变量(如User user = new User()线程正在执行的方法中的对象,绝对存活
静态属性引用的对象public static User INSTANCE = new User()类的静态变量,类加载后一直存活
常量引用的对象public static final User CONST = new User()运行时常量池中的常量对象
本地方法栈中 Native 方法引用的对象JNI 调用的 C++ 方法中的对象如调用System.currentTimeMillis()的底层对象

3. 实战场景

很多内存泄漏的本质,是对象 “看似无用,实则可达”(伪不可达):比如一个订单对象,业务逻辑已经处理完,但还被某个线程的局部变量(虚拟机栈)引用,GC 判定它 “可达”,不会回收,最终堆积导致堆 OOM。

import java.util.ArrayList; import java.util.List; // 内存泄漏:无用对象仍被GC Roots(静态集合)引用,判定为可达,无法回收 public class GCRootsLeakDemo { // 静态集合(GC Roots):持有大量无用User对象的引用 private static final List<User> USER_LIST = new ArrayList<>(); static class User { private String id; public User(String id) { this.id = id; } } public static void main(String[] args) { // 循环添加100万个User对象到静态集合 for (int i = 0; i < 1000000; i++) { USER_LIST.add(new User("user-" + i)); } // 业务逻辑处理完,理论上这些User对象已无用,但仍被USER_LIST引用 // GC Roots(静态属性)→ USER_LIST → User对象,引用链可达,无法回收 System.out.println("添加完成,User对象数量:" + USER_LIST.size()); // 即使手动置空局部变量,静态集合的引用仍存在 User temp = null; } }

这个例子中,User 对象明明已经无用,但因被静态集合(GC Roots)引用,可达性分析判定为 “可达”,GC 不会回收,最终导致堆内存溢出。我早年做用户会话系统时,就是因为用静态 Map 存储会话对象却不清理,触发了这种泄漏 —— 解决方法是:业务完成后,手动从静态集合中移除对象(USER_LIST.clear()),切断引用链,让对象变为 “不可达”。

二、引用类型

JDK 1.2 后,Java 将引用分为 4 种类型,核心目的是让程序员能主动控制对象的回收时机—— 不同引用类型的对象,即使都 “可达”,GC 的回收策略也完全不同。这是新手和资深开发者的核心区别:新手只会用强引用,资深开发者会根据场景选择合适的引用类型,最大化利用内存。

1. 强引用(Strong Reference)

这是最常见的引用类型,也是默认的引用方式 —— 只要对象被强引用关联,GC 就绝对不会回收它,哪怕内存不足抛出 OOM,也不会回收强引用对象。

核心特点:
  • 语法:User user = new User()(普通的对象赋值);
  • GC 策略:永不回收,内存不足时抛 OOM;
  • 适用场景:业务核心对象(如订单、用户信息),必须保证存活的对象。
实战踩坑:强引用导致内存泄漏

新手最易踩的坑:用强引用存储缓存对象,即使缓存过期,GC 也无法回收,最终导致 OOM。

// 错误:用强引用存储图片缓存,过期后仍无法回收 private Map<String, byte[]> imageCache = new HashMap<>(); // 存储缓存(强引用) public void putImage(String key, byte[] data) { imageCache.put(key, data); } // 即使业务上标记过期,强引用仍存在,GC无法回收 public void expireImage(String key) { // 仅标记过期,未移除引用,对象仍可达 expiredKeys.add(key); }

解决方法:要么手动移除引用(imageCache.remove(key)),要么改用软引用 / 弱引用。

2. 软引用(Soft Reference)

软引用是 “弹性引用”—— 如果对象只有软引用关联,内存充足时 GC 不回收,内存不足时 GC 会主动回收。这是做 “内存敏感型缓存” 的最佳选择(如图片缓存、临时数据缓存)。

核心特点:
  • 语法:需借助java.lang.ref.SoftReference类;
  • GC 策略:内存充足→不回收,内存不足→回收;
  • 适用场景:缓存对象(允许内存不足时丢失,不影响核心业务)。
软引用实现图片缓存(避免 OOM)
import java.lang.ref.SoftReference; import java.util.HashMap; import java.util.Map; // 正确:用软引用存储图片缓存,内存不足时自动回收 public class SoftReferenceCache { // 缓存容器:key=图片ID,value=软引用(指向图片字节数组) private Map<String, SoftReference<byte[]>> imageCache = new HashMap<>(); // 存储缓存:封装为软引用 public void putImage(String imageId, byte[] imageData) { imageCache.put(imageId, new SoftReference<>(imageData)); } // 获取缓存:检查软引用是否有效(对象未被回收) public byte[] getImage(String imageId) { SoftReference<byte[]> softRef = imageCache.get(imageId); if (softRef != null) { return softRef.get(); // 返回对象,若已回收则返回null } return null; // 缓存已被回收,需重新加载 } }

这个例子中,当堆内存不足时,GC 会自动回收软引用指向的图片字节数组,避免 OOM;内存充足时,缓存又能正常使用。我做电商 APP 的商品图片缓存时,就用这种方式,既保证了图片加载速度,又避免了内存溢出。

3. 弱引用(Weak Reference)

弱引用是 “临时引用”—— 如果对象只有弱引用关联,只要 GC 触发(不管内存是否充足),就会立刻回收该对象。弱引用的生命周期比软引用更短,适合存储 “临时使用、随时可回收” 的对象。

核心特点:
  • 语法:需借助java.lang.ref.WeakReference类;
  • GC 策略:只要 GC 扫描到,就回收(无论内存是否充足);
  • 适用场景:临时数据(如 ThreadLocal 的 key、缓存的临时中间结果)。
弱引用实现 ThreadLocal 的 key(核心源码逻辑)

ThreadLocal 能实现 “线程私有变量”,底层就是用弱引用存储 key—— 避免 ThreadLocal 对象被回收后,key 仍强引用导致 Entry 泄漏:

// ThreadLocal的核心源码(简化) public class ThreadLocal<T> { // ThreadLocal的key是弱引用 static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; // key是ThreadLocal对象,用弱引用封装 Entry(ThreadLocal<?> k, Object v) { super(k); // 弱引用指向ThreadLocal value = v; } } } }

如果 key 用强引用,当 ThreadLocal 对象被threadLocal = null置空后,key 仍强引用 ThreadLocal,导致 Entry 无法回收 —— 这就是 ThreadLocal 内存泄漏的核心原因(即使 key 是弱引用,value 仍需手动移除)。

弱引用存储临时数据
import java.lang.ref.WeakReference; // 弱引用存储临时数据,GC触发即回收 public class WeakReferenceDemo { public static void main(String[] args) { // 步骤1:创建对象,用弱引用关联 Object tempData = new Object(); WeakReference<Object> weakRef = new WeakReference<>(tempData); // 步骤2:切断强引用,仅保留弱引用 tempData = null; // 步骤3:触发GC(手动调用,仅用于测试) System.gc(); System.runFinalization(); // 步骤4:检查弱引用是否已回收(返回null) System.out.println("弱引用对象是否回收:" + (weakRef.get() == null)); // true } }

运行结果为true,说明 GC 触发后,弱引用关联的对象被立刻回收 —— 这是弱引用的核心特性。

4. 虚引用(Phantom Reference)

虚引用是 “最弱的引用”—— 它的唯一作用是跟踪对象的回收状态,无法通过虚引用获取对象实例,也无法阻止对象被回收。虚引用必须和ReferenceQueue配合使用,当对象被 GC 回收时,虚引用会被加入队列,程序员可通过队列感知 “对象已回收”,做一些清理工作(如释放直接内存)。

核心特点:
  • 语法:需借助java.lang.ref.PhantomReference类,且必须指定ReferenceQueue
  • GC 策略:随时回收,无法通过get()获取对象(get()永远返回 null);
  • 适用场景:跟踪对象回收、释放堆外内存(如 NIO 的直接内存)。
虚引用跟踪对象回收,释放直接内存
import java.lang.ref.PhantomReference; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.nio.ByteBuffer; import java.nio.ByteOrder; // 虚引用跟踪直接内存对象的回收,手动释放堆外内存 public class PhantomReferenceDemo { // 引用队列:存储被回收的虚引用 private static final ReferenceQueue<ByteBuffer> QUEUE = new ReferenceQueue<>(); public static void main(String[] args) throws InterruptedException { // 步骤1:分配直接内存(堆外内存) ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024 * 1024) .order(ByteOrder.nativeOrder()); // 步骤2:创建虚引用,关联直接内存对象和队列 PhantomReference<ByteBuffer> phantomRef = new PhantomReference<>(directBuffer, QUEUE); // 步骤3:切断强引用,仅保留虚引用 directBuffer = null; // 步骤4:触发GC,回收对象 System.gc(); System.runFinalization(); // 步骤5:从队列中获取虚引用,感知对象已回收,释放直接内存 Reference<? extends ByteBuffer> ref = QUEUE.remove(); if (ref != null) { System.out.println("对象已被GC回收,开始释放直接内存"); // 此处可调用底层方法释放直接内存(简化示例,仅打印) ref.clear(); } } }

虚引用的核心价值不是 “使用对象”,而是 “感知对象的回收时机”——NIO 的直接内存回收、自定义类加载器的资源清理,都会用到虚引用。

5. 四种引用类型对比表(核心总结)

引用类型语法特征GC 回收策略核心场景实战避坑点
强引用普通赋值(obj = new Obj永不回收,内存不足抛 OOM核心业务对象避免用于缓存,易导致泄漏
软引用SoftReference内存充足不回收,不足时回收内存敏感型缓存(图片)需检查get()是否为 null
弱引用WeakReferenceGC 触发即回收(无论内存是否充足)临时数据、ThreadLocal key不能存储核心数据,易丢失
虚引用PhantomReference随时回收,get()永远返回 null跟踪回收、释放堆外内存必须配合 ReferenceQueue 使用

三、引用类型的选择原则

二十余年的实战经验,我总结出选择引用类型的 3 个核心原则:

  1. 核心数据用强引用:订单、用户、支付等核心业务对象,必须用强引用,保证绝对存活;
  2. 缓存数据用软引用:图片、临时报表等非核心缓存,用软引用,内存不足时自动回收,避免 OOM;
  3. 临时数据用弱引用:ThreadLocal key、中间计算结果等,用弱引用,GC 触发即回收,减少内存占用;
  4. 堆外内存用虚引用:NIO 直接内存、JNI 对象,用虚引用跟踪回收,手动释放堆外资源。

最后小结

核心回顾

  1. 可达性分析算法:以 GC Roots 为起点,引用链可达的对象存活,不可达的标记为可回收;内存泄漏的核心是 “无用对象仍可达”;
  2. 引用类型决定 GC 策略:强引用永不回收,软引用内存不足时回收,弱引用 GC 触发即回收,虚引用仅跟踪回收;
  3. 实战选择:核心对象用强引用,缓存用软引用,临时数据用弱引用,堆外内存用虚引用。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/22 12:18:55

家庭服务器部署Qwen:24小时在线儿童绘画助手搭建教程

家庭服务器部署Qwen&#xff1a;24小时在线儿童绘画助手搭建教程 你是否试过陪孩子画小猫、小熊、小兔子&#xff0c;画到一半他突然问&#xff1a;“妈妈&#xff0c;能画一只穿宇航服的熊猫吗&#xff1f;”——然后你卡在了“宇航服褶皱怎么画”上&#xff1f;别担心&#…

作者头像 李华
网站建设 2026/2/18 2:15:28

MinerU提取速度慢?GPU算力瓶颈分析与优化教程

MinerU提取速度慢&#xff1f;GPU算力瓶颈分析与优化教程 你是不是也遇到过这样的情况&#xff1a;PDF文档刚拖进MinerU&#xff0c;命令敲下去&#xff0c;结果光是“加载模型”就卡住半分钟&#xff0c;等真正开始解析时&#xff0c;一页A4纸要花15秒以上&#xff1f;更别提…

作者头像 李华
网站建设 2026/2/20 10:36:47

YOLO26轻量部署方案:Nano版本嵌入式设备实战

YOLO26轻量部署方案&#xff1a;Nano版本嵌入式设备实战 YOLO26是目标检测领域最新一代轻量化模型&#xff0c;其Nano版本专为资源受限的嵌入式设备设计——在保持高精度的同时&#xff0c;模型体积压缩至不足3MB&#xff0c;推理延迟低于15ms&#xff08;ARM Cortex-A72平台实…

作者头像 李华
网站建设 2026/2/24 23:17:47

Qwen-Image-Edit-2511使用心得:提示词编写技巧总结

Qwen-Image-Edit-2511使用心得&#xff1a;提示词编写技巧总结 Qwen-Image-Edit-2511 是当前图像编辑领域中功能非常强大的一个模型版本&#xff0c;作为 Qwen-Image-Edit-2509 的增强版&#xff0c;它在多个关键能力上实现了显著提升。无论是减轻图像漂移、改进角色一致性&am…

作者头像 李华
网站建设 2026/2/7 9:11:02

Z-Image-Turbo开源生态分析:ModelScope平台集成优势详解

Z-Image-Turbo开源生态分析&#xff1a;ModelScope平台集成优势详解 1. 为什么Z-Image-Turbo值得开发者重点关注 你有没有试过等一个文生图模型下载30GB权重文件&#xff0c;结果网速卡在98%、显存爆满、环境报错连环出现&#xff1f;这种体验&#xff0c;在Z-Image-Turbo的M…

作者头像 李华
网站建设 2026/2/24 3:48:18

MinerU日志记录规范:操作审计与问题追踪方法

MinerU日志记录规范&#xff1a;操作审计与问题追踪方法 1. 引言&#xff1a;为什么需要规范的日志记录 在使用 MinerU 2.5-1.2B 进行复杂 PDF 文档提取的过程中&#xff0c;我们面对的不仅是多栏排版、嵌套表格、数学公式和图像识别等技术挑战&#xff0c;还有实际应用中难以…

作者头像 李华