【从零学习JVM|第八篇】深入探寻堆内存
前言:
堆回收主要作用是清理Java堆中不再被引用的对象,释放内存空间,避免内存泄漏。它通过标记可回收对象,再用合适算法回收,保障内存高效利用。同时,堆回收机制影响程序性能,了解其原理能优化内存管理,提升应用稳定性和运行效率。
堆是 Java 运行时环境的基石,其高效管理直接决定了应用的性能、稳定性和可扩展性。深入理解堆内存机制(如分代策略、GC 算法)是 Java 开发者必备的核心技能。
接下来让我们来深入探寻堆的内存机制。
引用计数法和可达性分析法
java堆有一个最大的特点就是,自动垃圾回收,这一部分不需要我们程序员来操心,那它怎么知道什么时候把垃圾清理掉,它怎么知道哪部分是垃圾?
在Java中的对象是否可以被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明改对象还要使用,不能回收。
引用计数法
核心思想: 每个对象都自带一个“小计数器”。这个计数器记录着当前有多少个“用户”正在引用(使用)它。当一个新的用户开始使用它时,计数器加 1;当一个用户用完它时,计数器减 1。每次减一后java虚拟机都会进行扫描,当发现当计数器归零时,意味着没有任何用户需要它了,系统就可以立刻把它回收。
优点:
实时性:每次执行减一操作之后,都会进行扫描,一旦发现为0,立即回收。没有延迟!这对于需要及时释放资源(如文件句柄、数据库连接、网络端口)的场景非常有利。
实现相对简单:核心逻辑就是计数器加减和归零判断,概念清晰易懂。
缺点:
频繁的计数器更新开销:每次引用和取消引用都会维护计数器,对系统的性能会有一定的影响。
计数器存储开销:每个对象都会存储一个计数器,如果对象很多,那计数器也很多。
循环引用问题 (致命弱点!):比如,在堆中有两个对象,然后这两个对象互相引用,那他们的计数器永远都不会归零。永远都是1,它们就永远不可能被回收。
可达性分析算法
可达性分析算法(Reachability Analysis)是自动内存管理的核心算法,用于判定堆内存中的对象是否存活。其核心思想是:分为两个对象一个叫GC Roots,一个叫普通对象。通过一系列称为“GC Roots”的根对象作为起点,遍历对象引用链,所有能被遍历到的对象是“存活”的,不能被遍历到的对象是“垃圾”。
GC Roots 是垃圾回收的起点,以下对象可作为 GC Roots:
-
虚拟机栈中的引用对象(如方法局部变量)
理由:方法执行时栈帧中的变量正在使用,其引用的对象不能回收。 -
方法区中类静态属性引用的对象
理由:静态变量随类加载存在,生命周期长,引用的对象需保留。 -
方法区中常量引用的对象
理由:常量池中的常量引用的对象(如字符串常量)不能被回收。 -
本地方法栈中 JNI 引用的对象
理由:本地代码(如 C++)正在使用的 Java 对象不能被回收。 -
活跃线程的引用对象比如
理由:线程正在执行,其引用的对象不能回收。
一句话GC Roots 就是“程序当前绝对离不开的对象”,绝对不会被回收。
我们一直提到的有没有被引用,其实引用也分有类型。
五种引用类型
强引用 (Strong Reference):
Object obj = new Object()这种最常见的引用,obj引用了Object 在堆中的对象,只要强引用还在,就不可能被回收。如果obj=null,这个时候堆中的对象就会被回收了。
实现方式:普通对象引用
// 创建强引用
Object obj = new Object();// 解除强引用(使对象可回收)
obj = null;
软引用 (Soft Reference):
它是相对于强引用弱一点的引用关系,软引用指向的对象,在 内存不足,会被垃圾回收器回收。然后把这部分内存分给其他对象。
软引用本身作为一个对象,也会被回收,但其回收条件与普通对象类似:当没有任何强引用指向该软引用对象时,就会被垃圾回收器回收。
实现类:java.lang.ref.SoftReference
// 创建软引用
SoftReference<Object> softRef = new SoftReference<>(new Object());// 获取对象(可能返回 null)
Object obj = softRef.get(); // 使用引用队列(可选)
ReferenceQueue<Object> queue = new ReferenceQueue<>();
SoftReference<Object> softRefWithQueue = new SoftReference<>(new Object(), queue);
软引用在 Java 中用于实现内存敏感的缓存,适用于以下场景:
- 图片缓存:存储图片资源,当内存不足时自动释放,避免 OOM。
- 网页缓存:缓存网页内容,在内存紧张时优先回收,提升性能。
- 大对象缓存:缓存计算结果、数据库查询结果等,减少重复计算。
- 内存敏感的中间数据:存储临时计算结果,在内存不足时优雅降级。
核心优势:在保证应用正常运行的前提下,最大限度利用内存,提升性能。
软引用通常和一个引用队列一起联用
引用队列 (ReferenceQueue) 的作用:
-
创建软引用时传入一个引用队列
-
当软引用指向的对象 被垃圾回收器回收后,这个 软引用对象本身会被 JVM 自动放入关联的队列中。
-
程序可以定期检查这个队列,知道哪些软引用持有的对象已经被回收了,从而进行一些清理工作(例如从缓存映射中移除对应的键)。
弱引用 (Weak Reference):
弱引用和软引用的本质区别就是,不管内存够不够它所关联的对象在垃圾回收时都会被回收。它完全不影响它所关联对象存活。
弱引用通常和引用队列搭配使用,当对象被回收时,弱引用会被放入队列,程序可借此感知对象的生命周期。
- 核心特点:弱引用关联的对象,在没有强引用时会被立即回收,适合需要 “自动过期” 的场景。
- 与软引用的区别:软引用在内存不足时才会回收对象,而弱引用只要 GC 运行就会回收(不管内存是否充足)。
实现类:java.lang.ref.WeakReference
// 创建弱引用
WeakReference<Object> weakRef = new WeakReference<>(new Object());// 获取对象(可能很快变 null)
Object obj = weakRef.get();// 典型应用:WeakHashMap
WeakHashMap<Key, Value> weakMap = new WeakHashMap<>();
为什么要用弱引用?
-
缓存场景:防止内存泄漏
- 例子:HashMap 存储图片对象时,若用强引用,即使图片不再被使用,也无法被回收,导致内存占用过高。而用弱引用存储键或值,当图片不再被其他地方引用时,会被 GC 自动清理,避免缓存 “占着内存不放”。
-
解决回调导致的内存泄漏
- 场景:GUI 程序中,窗口(Window)持有监听器(Listener)的强引用,若监听器又反向引用窗口,会形成循环引用。用弱引用定义监听器对窗口的引用,可让窗口在关闭后正常被回收。
-
Map 结构的弱键设计
- 比如
WeakHashMap
,它的键是弱引用:当键对象不再被强引用时,GC 会自动清理对应的键值对,适合实现 “临时关联” 的数据结构(如缓存自动过期)。
- 比如
弱引用的特性是 “对象没有强引用时会被 GC 直接回收”,但程序无法主动知道 GC 何时回收对象。引用队列的作用就是:
- 当 GC 回收弱引用关联的对象时,JVM 会自动把这个弱引用对象放入队列;
- 程序通过监听队列,就能得知 “对象已被回收”,从而执行后续处理(如删除缓存、更新状态等)。
虚引用 (Phantom Reference):
虚引用是所有引用类型中最弱的,必须和引用队列(ReferenceQueue)一起用,对象被回收前,虚引用会被放入队列,此时程序可以做后续处理,但对象本身已经 “必死无疑” 了。
- 存在的唯一目的:只是为了在对象被垃圾回收时,收到一个 “通知”,但完全不影响对象的生命周期。
- 使用场景:常用于管理堆外内存(比如 NIO 的 DirectBuffer)。当虚引用关联的对象被回收时,JVM 会把这个虚引用加入到一个队列里,程序可以通过监听这个队列,手动释放堆外资源(因为堆外内存不受 JVM 直接管理)。
实现类:java.lang.ref.PhantomReference
// 必须配合引用队列使用
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);// 获取对象总是返回 null
Object obj = phantomRef.get(); // 始终为 null// 监控回收通知
Reference<?> ref = queue.remove(); // 阻塞直到有引用入队
if (ref == phantomRef) {// 对象已被回收,执行清理操作
}
虚引用和引用队列:
- 虚引用必须和引用队列绑定:创建虚引用时,必须传入一个引用队列实例。
- 对象回收时的通知机制:当虚引用关联的对象被垃圾回收器判定为可回收时,JVM 不会直接销毁这个对象,而是先把对应的虚引用放入引用队列。
- 程序的响应时机:程序可以通过轮询引用队列,一旦发现队列中有虚引用,就知道关联的对象即将被回收,此时可以执行后续操作(如释放堆外内存)。
- 避免资源泄漏:如果没有引用队列,虚引用就无法通知程序对象被回收的事件,导致程序无法及时处理堆外资源(因为堆外内存不归 JVM 管理),从而造成资源泄漏。
终结器引用 (Final Reference) :
当对象没有强引用指向,被判定为可回收时,JVM 会先调用它finalizer方法(若重写了该方法)。这相当于让对象在 “被销毁前” 有机会执行一些清理操作,比如:
- 释放本地资源(如 C 语言层面的句柄);
- 断开与外部资源的连接(虽然不推荐,但早期有程序这么用);
- 甚至通过重新建立强引用 “自救”,避免被回收。
实现机制:JVM 内部管理
// 创建引用队列
ReferenceQueue<Object> queue = new ReferenceQueue<>();// 与引用配合使用
WeakReference<Object> ref = new WeakReference<>(new Object(), queue);// 检查队列(非阻塞)
Reference<?> polledRef = queue.poll();// 阻塞等待
try {Reference<?> removedRef = queue.remove(); // 阻塞直到有引用入队
} catch (InterruptedException e) {Thread.currentThread().interrupt();
}
它的工作过程:
- 当垃圾回收器检测到对象仅被终结器引用(即没有其他强引用指向该对象)时,会将其放入终结队列(Finalizer Queue)。
- JVM 会启动一个低优先级的守护线程(Finalizer 线程),从队列中取出对象并调用其
finalize()
方法。 - 执行完
finalize()
后,对象在下一次 GC 时才会被真正回收。
但是在实际运用中不推荐
- 执行时机不可控:JVM 何时调用
finalize()
完全不确定 —— 可能在对象可回收后很久才执行,甚至因 JVM 退出而不执行,导致资源长时间无法释放(比如文件句柄占用)。 - 执行顺序混乱:对象的
finalize()
执行顺序与创建顺序无关,若多个对象存在依赖关系(如 A 依赖 B),可能出现 B 先被回收而 A 后回收的情况,导致逻辑错误。 - 性能开销大:JVM 需要维护终结器引用队列,处理
finalize()
方法的调用,尤其当大量对象重写finalize()
时,会显著影响垃圾回收效率。
并且我们需要注意的是finalize方法仅仅会被执行一次。
在常规开发中虚引用和终结器引用不会使用,了解即可。
软引用和弱引用可以不使用引用队列,但是虚引用必须使用。
总结
堆内存是Java虚拟机中用于存储对象实例和数组的内存区域,是内存管理的核心部分。其主要作用包括:为对象实例分配内存空间,所有new创建的对象都在堆中存储;是垃圾回收的主要区域,JVM通过垃圾回收机制自动管理堆中对象的生命周期,回收不再被引用的对象以释放内存;支持动态内存分配,根据程序运行时的需求动态调整内存使用,满足对象创建和销毁的动态变化。堆内存的大小可通过JVM参数(如-Xms、-Xmx)配置,合理设置能优化内存使用和程序性能。
创作不易,如果这篇文章对你有帮助请点赞收藏加转发,感谢你的阅读,你的支持就是我最大的动力。