臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

核心结论

Java GC 主要通过可达性分析判断对象是否还能被使用:从 GC Roots 出发,找不到任何引用路径连接到的对象,就具备被回收的条件。

二、🧭 理论:它是什么

2.1 可达性分析

可以把对象想成房间,把引用想成连接房间的路,GC Roots 是几个固定入口。

  • 从入口还能走到的房间:对象仍然存活。
  • 从任何入口都走不到的房间:对象不可达,可以被 GC 回收。

这个类比中:

类比Java 中的真实含义
入口GC Roots,例如线程栈中的局部变量引用
房间堆中的对象
道路对象之间的引用关系

最常见的情况确实可以理解为“从线程栈走到堆”:

User user = new User();
user.setAddress(new Address());

对应的引用链是:

线程栈中的 user 引用(GC Root)
        ↓
堆中的 User 对象
        ↓
堆中的 Address 对象

GC 可以沿着 user 找到 User 和 Address,所以它们暂时不能被回收。不过,入口不只有线程栈,还包括静态字段引用和 JVM 内部引用等 GC Roots。

类比只用于帮助理解;真实判断依据是 JVM 中对象之间的引用关系。

三、⚙️ 理论:它是怎么工作的

3.1 GC Roots 是什么

GC Roots 是 JVM 进行可达性分析时使用的一组起点。它不是某个具体的内存区域,也不是一种特殊的对象类型。

JVM 会先把当前肯定还需要使用的一些引用当作“根”,然后沿着引用关系查找堆中的对象:

GC Roots
    ↓
堆中的对象 A
    ↓
堆中的对象 B

只要对象能从任意 GC Root 到达,它就不能被回收;从所有 GC Roots 都无法到达,才具备被回收的条件。

一句话记忆

GC Roots 就是垃圾回收查找存活对象时的几个可信起点。

3.2 GC Roots 有哪些

面试中记住这些常见来源即可:

3.2.1 活动线程栈中的引用

方法参数和局部变量都可能保存对象引用:

public void process() {
    // user 是线程栈中的局部变量,指向堆中的 User 对象。
    User user = new User();
    user.printName();
}

引用关系:

活动线程的栈帧 → 局部变量 user → 堆中的 User 对象

只要方法仍可能继续使用 user,这个 User 对象就可以从 GC Roots 到达,不能被回收。方法执行结束或 user 不再被使用后,这条引用链才可能消失。

3.2.2 类的静态字段引用

静态字段跟随已经加载的类存在,可以长期引用堆中的对象:

public class UserCache {
    // 静态字段一直引用 User 对象。
    private static final User ADMIN = new User();
}

引用关系:

已加载的 UserCache 类 → 静态字段 ADMIN → 堆中的 User 对象

只要这个类及其静态字段仍然有效,ADMIN 指向的对象通常就不能被回收。静态集合长期保存业务对象,也是常见的内存泄漏原因。

3.2.3 JVM 内部持有的引用

JVM 为了维持程序运行,会保留活动线程、类加载器等内部对象。例如:

Thread worker = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // 持续处理任务
    }
});
worker.start();

引用关系可以简化为:

JVM 线程管理 → 正在运行的 Thread → 线程关联的任务对象

线程还在运行时,即使业务代码不再保留 worker 变量,JVM 仍需调度这个线程,因此线程对象及它继续引用的对象可能无法回收。

3.2.4 JNI 本地代码持有的引用

JNI 全称是 Java Native Interface(Java 本地接口),可以把它理解成 Java 与 C、C++ 等本地代码之间的“翻译桥梁”:

Java 代码 → JNI → C/C++ 本地代码 → 操作系统或本地库

JNI 常用于调用操作系统底层能力、复用已有的 C/C++ 库,以及访问硬件驱动、音视频库或加密库。

Java 使用 native 声明由本地代码实现的方法:

public class NativeDemo {
    // native:这个方法只有 Java 声明,具体实现位于 C、C++ 本地库中。
    public native void process(User user);
 
    static {
        // loadLibrary:加载编译后的本地库,例如 Linux 的 .so 或 Windows 的 .dll。
        System.loadLibrary("native_demo");
    }
}

本地代码接收到 User 后,可以通过 JNI 引用继续持有这个 Java 对象:

引用关系可以简化为:

C/C++ 本地代码 → JNI 引用 → 堆中的 User 对象

JNI 局部引用通常在一次本地方法调用期间有效;JNI 全局引用可以长期存在,需要由本地代码主动释放。全局引用一直不释放时,相关 Java 对象就可能长期无法被回收。

一句话理解 JNI

JNI 是 Java 调用 C/C++ 等本地代码的桥梁;本地代码持有的 Java 对象引用,也会影响 GC。

快速区分

线程栈引用表示“Java 方法还可能使用”;静态字段引用表示“类还保存着”;JVM 内部引用表示“JVM 自己还要使用”;JNI 引用表示“C/C++ 本地代码还要使用”。

GC Roots → 对象 A → 对象 B
 
对象 C ↔ 对象 D

A、B 可以从 GC Roots 到达,不能回收。C、D 即使互相引用,只要与 GC Roots 断开,仍然可以被回收。

注意

“不可达”表示对象具备回收条件,不代表 GC 会立即执行,也不能依靠 System.gc() 保证某个对象立刻被回收。

3.3 引用强度也会影响回收

引用越弱,GC 越容易回收它指向的对象:

引用类型大白话对象什么时候可能被回收
强引用紧紧抓住对象强引用仍然可达时不回收
软引用内存够就先留着内存紧张时可能回收
弱引用只贴了一张临时标签GC 发现对象仅剩弱引用时就可能回收
虚引用不取对象,只接收通知对象进入可回收阶段时配合引用队列使用

3.3.1 强引用

平时直接创建对象,得到的就是强引用:

// user 是指向 User 对象的强引用。
User user = new User();
 
// 断开这条强引用后,如果对象也不能从其他 GC Roots 到达,
// 它才具备被回收的条件。
user = null;

只要对象仍能通过强引用从 GC Roots 到达,GC 就不会回收它。

3.3.2 软引用 SoftReference

软引用表达的是“内存充足时可以保留,内存紧张时允许回收”:

import java.lang.ref.SoftReference;
 
User user = new User();
 
// SoftReference:使用软引用保存 User 对象。
SoftReference<User> softReference = new SoftReference<>(user);
 
// 去掉原来的强引用,让对象只剩软引用。
user = null;
 
// get:对象尚未被回收时返回 User,已被回收时返回 null。
User result = softReference.get();

软引用曾常用于内存敏感缓存,但回收时间无法精确控制,不能把它当成具有稳定容量和淘汰规则的缓存。

3.3.3 弱引用 WeakReference

弱引用比软引用更弱。对象只剩弱引用时,GC 判断它为弱可达后,就可能清除弱引用并回收对象:

import java.lang.ref.WeakReference;
 
User user = new User();
 
// WeakReference:弱引用不会阻止 User 被 GC 回收。
WeakReference<User> weakReference = new WeakReference<>(user);
 
// 去掉原来的强引用。
user = null;
 
// 对象被回收后,get() 返回 null。
User result = weakReference.get();

ThreadLocalMap 的 key 使用弱引用,但 value 仍是强引用,所以 ThreadLocal 用完后仍要调用 remove()。可结合 10.ThreadLocal的原理和使用场景 理解。

3.3.4 虚引用 PhantomReference

虚引用不是用来取得对象的,调用 get() 始终返回 null。它通常和 ReferenceQueue 配合,用于获知对象已经进入可回收阶段:

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
 
ReferenceQueue<User> queue = new ReferenceQueue<>();
User user = new User();
 
// PhantomReference 必须和引用队列一起创建。
PhantomReference<User> phantomReference =
        new PhantomReference<>(user, queue);
 
// 去掉原来的强引用。
user = null;
 
// 虚引用不能用于取得目标对象,结果始终是 null。
System.out.println(phantomReference.get());

当 GC 处理目标对象时,虚引用会按规则进入关联的引用队列,程序可以检查队列并执行后续清理或监控逻辑。

记忆口诀

强引用不回收,软引用内存紧张时收,弱引用 GC 时可能收,虚引用只负责回收通知。

这些引用只能影响对象是否允许被回收,不能保证 GC 在某个确定时间执行。

四、🚀 实践:观察对象变为不可达

4.1 前置准备

准备一个可用的 JDK,无需额外依赖。

4.2 可以拿来干什么

理解可达性分析,可以帮助排查内存泄漏:如果业务对象一直无法回收,通常说明它仍被静态集合、线程、缓存或监听器等对象间接引用。

4.3 最小验证

新建 GcReachabilityDemo.java:

import java.lang.ref.WeakReference;
 
public class GcReachabilityDemo {
    public static void main(String[] args) throws InterruptedException {
        Object target = new Object();
 
        // WeakReference 不会像强引用一样阻止目标对象被回收。
        WeakReference<Object> weakReference = new WeakReference<>(target);
 
        // 断开局部变量到目标对象的强引用路径。
        target = null;
 
        // System.gc:只是向 JVM 提出执行 GC 的建议,不保证立即执行。
        System.gc();
        Thread.sleep(100);
 
        // 如果输出 null,说明目标对象已经被回收;
        // 如果不是 null,也不代表它永远不会回收,只是本次尚未回收。
        System.out.println(weakReference.get());
    }
}

运行:

javac GcReachabilityDemo.java
java GcReachabilityDemo

常见输出是 null,但具体回收时机由 JVM 决定,因此结果不保证每次相同。

五、🎤 面试时怎么流利地说

5.1 回答路线

可达性分析 → GC Roots → 引用链 → 不可达才可回收 → 回收时机不确定。

5.2 30 秒标准回答

Java GC 主要通过可达性分析判断对象是否可以回收。它会从 GC Roots 出发,沿着对象引用关系向下查找;能够到达的对象仍然存活,找不到引用路径的对象就具备被回收的条件。常见 GC Roots 包括线程栈中的引用、静态字段引用和 JVM 内部引用。即使两个对象循环引用,只要它们整体与 GC Roots 断开,也可以被回收。需要注意,可以回收不等于会被立即回收。

5.3 高频追问与短答

问:为什么循环引用不会造成 Java GC 无法回收?

因为 Java 不是只看对象的引用计数,而是检查对象能否从 GC Roots 到达。循环中的对象如果与 GC Roots 断开,整个环仍然是不可达的。

六、📌 总结

  • 核心算法:可达性分析。
  • 判断起点:GC Roots。
  • 没有从 GC Roots 到对象的引用路径,对象才具备回收条件。
  • 循环引用不等于不能回收。
  • 口诀:从根出发,有路则活,无路可收。

七、🔗 官方资料