Java中的垃圾回收
  # Java中的垃圾回收
# 垃圾回收
# 前言
“程序员都了解初始化的重要性,但常常会忘记同样也重要的清理工作。毕竟,谁需要清理一个int呢?但在使用程序库时,把一个对象用完后就“弃之不顾”的做法并非总是安全的(因为它会始终占据内存,造成内存泄漏)。
当然,Java有垃圾回收器负责回收无用对象占据的内存资源。但也有特殊情况:假定你的对象(并非使用new)获得了一块“特殊”的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。
为了应对这种情况,Java允许在类中定义一个名为finalize()的方法。它的工作原理“假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以要是你打算用finalize(),就能在垃圾回收时刻做一些重要的清理工作。”
上述这段话是来自《Java编程思想》,那么什么叫“垃圾回收器只知道释放那些经由new分配的内存”,还有其他分配的类型吗?
这所谓的finalize(),又是什么?
垃圾回收的过程是什么样的,它是怎么判断哪些对象需要回收的?
下面就一起来看看吧。
# finalize方法
finalize() 是 Java 早期设计中一个比较“理想化但失败”的机制 —— 它确实和本地方法(native code)有关系,但现在已经被官方明确标记为过时(deprecated)。
# 它是什么
finalize() 是 java.lang.Object 类中的一个方法:
protected void finalize() throws Throwable { }
 它的作用是:
当一个对象**即将被垃圾回收(GC)*时,JVM 会在释放该对象的内存前*调用一次
finalize()方法。
简单说:
- 它是一个对象“临终前的回调函数”。
 
# 设计初衷
提供一个“最后的机会”来:
- 释放非 Java 堆资源(如文件句柄、网络连接、本地内存等)
 - 执行清理逻辑
 
在 Java 早期(90年代),设计者想模仿 C++ 的析构函数(destructor),让开发者能在对象销毁时清理资源:
public class FileHandler {
    private FileDescriptor fd;
    @Override
    protected void finalize() throws Throwable {
        try {
            if (fd != null) {
                close(); // 关闭文件句柄
            }
        } finally {
            super.finalize(); // 保证父类也能清理
        }
    }
}
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 本地方法
之前说它和本地方法有关,什么叫本地方法?
在 Java 中,本地方法(Native Method) 是指 由非 Java 语言(通常是 C 或 C++)实现的方法,其具体实现代码位于 Java 虚拟机(JVM)外部。Java 通过 JNI(Java Native Interface,Java 本地接口) 机制来调用这些本地方法。
下面是本地方法的特征:
- 使用
 native关键字声明- 只有方法声明,没有方法体(以分号
 ;结尾)- 实际实现在 JVM 外部(如动态链接库
 .dll、.so、.dylib中)
在 Java 中,一些系统级资源是通过 JNI(Java Native Interface)调用 C/C++ 实现的本地代码管理的,例如:
- 文件句柄(FileDescriptor)
 - 网络 Socket
 - 图形资源(窗口句柄)
 - 操作系统级内存分配
 
这些资源不是 JVM 自己分配的内存,所以 GC 无法自动回收。
为什么需要本地方法?
| 场景 | 说明 | 
|---|---|
| 访问操作系统底层功能 | 如文件系统、硬件设备、系统调用(Java 标准库未封装的部分) | 
| 提升性能 | 对计算密集型任务(如图像处理、加密算法),C/C++ 比 Java 更高效 | 
| 复用已有 C/C++ 代码 | 无需重写,直接集成遗留系统或高性能库(如 OpenCV、FFmpeg) | 
| 实现 Java 无法直接完成的操作 | 如直接内存操作、特定 CPU 指令等 | 
# 为什么后来被废弃?
finalize() 的设计看起来优雅,但在实践中带来了严重问题:
1.不确定性(最大问题)
- GC 什么时候运行?你无法预测。
 - 也就是说:
finalize()什么时候被调用,不可控。 - 有时甚至永远不会被调用(程序在 GC 前就结束了)。
 
2.性能问题
- 为了支持 
finalize(),JVM 必须做额外处理:把带有finalize()的对象放入一个“终结队列”,延迟释放。 - 这会拖慢 GC 效率。
 
3.可能导致对象“复活”
- 在 
finalize()方法中,如果你让这个对象重新被引用,就会“复活”,导致 GC 无法释放它: 
4.难以调试 & 资源泄漏风险大
因为调用时机不确定,很多资源可能迟迟不被释放(比如打开文件过多导致系统资源枯竭)。
它的调用流程是这样的:
对象创建 → 使用完毕 → 变成垃圾(不可达)
        ↓
第一次 GC 发现它是垃圾
        ↓
检查是否重写了 finalize()
        ↓ 是
放入 Finalizer 队列
        ↓
Finalizer 线程调用 finalize() ←──┐
        ↓                         │
(在 finalize() 中做资源清理)     │
        ↓                         │
本次 GC 结束                    │
        ↓                         │
第二次 GC 再次检查该对象         │
        ↓                         │
如果仍不可达 → 回收内存          │
如果可达(复活)→ 不回收 ────────┘
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
虽然机制如此,但:
- 你无法控制 GC 何时发生 → 资源可能长时间不释放
 - Finalizer 线程优先级很低 → 
finalize()可能延迟很久才执行 - 性能极差 → 对象回收需要至少两个 GC 周期
 - Java 9+ 已废弃 → 未来可能移除
 
# 总结与推荐
一句话总结:
finalize()是早期 Java 模仿 C++ 析构函数的尝试,用来清理 native 资源,但由于时机不可控、容易出错,已经被废弃,现代 Java 推荐使用try-with-resources来安全地释放资源。
历史演进时间线总结
| 阶段 | 行为 | 状态 | 
|---|---|---|
| Java 1.0–8 | finalize() 存在并常用 |  ☑️ 设计用来释放本地资源 | 
| Java 9 | 被标记为 @Deprecated |  ⚠️ 不推荐使用 | 
| Java 18+ | 建议使用 Cleaner 或 PhantomReference 替代 |  ✅ 更安全、可控 | 
# GC运行时期
前面,为什么说gc运行无法控制?
在Java中,GC(Garbage Collection)指的是垃圾回收机制,它是Java虚拟机(JVM)自动管理内存的核心功能,用于检测和回收不再使用的对象所占用的内存空间,从而避免内存泄漏和程序崩溃
Java中的GC何时会去回收垃圾,通过时间判断吗?比如一个对象长时间没调用,就给回收掉?何时运行为什么无法预测?
它何时运行,不是通过时间判断的,而是 通过“可达性分析(Reachability Analysis)”判断对象是否还“活着”。
# GC的作用和机制
GC(Garbage Collector)负责做两件事:
- 自动识别不再被使用的对象(即“垃圾”);
 - 回收它们所占用的内存空间,以便后续再利用。
 
识别:
那它如何进行可达性分析?什么是“可达”?
从一组称为 GC Roots 的对象出发,能通过引用链访问到的对象,就是可达的。
在 JVM 中,以下对象被视为 GC Roots(根节点):
| GC Root 类型 | 举例说明 | 
|---|---|
| 当前栈中引用的对象 | 局部变量、方法参数 | 
| 方法区中类的静态引用 | static 变量引用的对象 | 
| 常量引用 | 被 final 修饰的常量对象 | 
| JNI(native 代码)引用 | 本地代码中持有的引用(JNI 全局引用) | 
我们用一个直观图解释:
GC Roots
   ↓
  A → B → C
        ↓
        D
   ↑
   X
还有一个孤立对象 Y
 2
3
4
5
6
7
8
9
10
- 从 GC Roots 出发,能访问到 A、B、C、D、X;
 - 但访问不到 Y —— 那么 Y 被认为“不可达”;
 - GC 就会认为 Y 是垃圾对象,等待回收。
 
那gc,在程序运行时,就是一直在不停的判断各种对象是否可达吗?还是说有时间间隔?发现不可达之后,就立即回收吗?
# 何时回收
Java 的垃圾回收(GC)并不是“一直在不停地判断所有对象是否可达”,也不是简单地“每隔固定时间扫描一次”。它的执行是事件驱动 + 分代策略 + 内存压力触发的,具有高度的按需性和阶段性。
GC 什么时候会触发?有人说,GC 通常会在内存不足或特定阈值时触发。这是正确的,但不完全。
触发时机由 JVM GC 策略(如 G1、ZGC、Shenandoah 等)决定,但通常包括以下情况:
| 触发场景 | 说明 | 
|---|---|
| 堆内存不足 | 对象太多、分配空间不够,触发 GC | 
| 新生代(Eden 区)满了 | Minor GC(快速、频繁) | 
| 老年代满了 | Major GC 或 Full GC(慢但彻底) | 
显式调用 System.gc() |  建议 GC(不保证立即执行) | 
| 元空间/方法区资源不足 | 特殊情况下触发 Full GC | 
# 总结
一句话总结:
Java 的 GC 不看时间,它看“引用关系”。
当一个对象不再被任何地方引用(不可达)时,它才会被判定为垃圾;而何时回收,由 JVM 自行决定。