DAY01 - JVM - 垃圾回收机制详解
4. 垃圾回收
在 C/C++ 这类语言中,没有内置的垃圾回收机制。程序员需要手动释放内存,否则就会造成内存泄漏。内存泄漏是指对象已经不再使用,但它占用的内存没有被释放,如果长时间积累,会导致内存溢出,甚至程序崩溃。
示例:未手动释放导致内存泄漏
在这段代码中,程序通过死循环不停创建 Test
类的对象,每次循环结束后对象就已经不再使用了,但没有主动删除它们,最终造成内存泄漏。
示例:手动释放内存
在这段代码中,程序显式调用 delete
删除对象,避免了内存泄漏。
我们称这种回收过程为垃圾回收(GC, Garbage Collection)。在 C/C++ 中,这属于手动回收:
✅ 优点 | ❌ 缺点 |
---|---|
内存释放及时,程序员完全掌控 | 编写繁琐,容易忘记释放、重复释放,导致悬空指针/内存泄漏 |
Java 的自动垃圾回收
Java 引入了自动垃圾回收(GC)机制,通过垃圾回收器在后台自动识别并释放不再使用的对象,从而极大降低了程序员的负担。垃圾回收器主要回收堆内存中的无用对象。
-
自动垃圾回收:根据对象的使用情况,由 JVM 自动判定并回收。
-
✅ 优点:简化开发、减少内存泄漏风险。
-
❌ 缺点:程序员无法控制回收的“及时性”,GC 发生的时机由 JVM 决定。
-
-
手动垃圾回收(如 C/C++):
-
✅ 优点:内存回收时机可控。
-
❌ 缺点:编写不当易出错(悬空指针、重复释放、泄漏)。
-
哪些内存需要垃圾回收器管理?
JVM 内存区域中,线程私有的部分(如虚拟机栈、本地方法栈、程序计数器)都是随线程创建/销毁的,生命周期由线程本身决定,所以不需要垃圾回收器处理。
🧠 理论理解
垃圾回收(GC)是 JVM 的核心机制之一,负责在程序运行期间自动释放不再使用的对象内存,避免程序员手动释放带来的复杂性和隐患。GC 主要回收堆内存中的对象,而线程私有内存(如栈帧)是随线程结束自动释放的,不归 GC 管辖。GC 的出现让 Java 具备“内存安全”特性,但它的不可控性(回收时机不可预测)也是开发者需要面对的现实。
🏢 企业实战理解
-
阿里巴巴:线上系统中,GC 是性能调优的重中之重,尤其是大促/秒杀场景,必须通过 G1、ZGC 等高性能 GC 策略减少停顿时间,并利用
Arthas
、GCViewer
等工具分析 GC 日志。 -
字节跳动:自研字节码插桩平台时会关注垃圾回收的触发机制,防止动态生成的对象持续占用堆空间。
-
美团:对 JVM 内存模型有严格监控,特别是广告平台通过 CMS → G1 → ZGC 迭代优化,提升 GC 性能应对高并发。
-
Google (Android):虽然 Android 使用 ART 虚拟机,其垃圾回收机制依然借鉴了 JVM 的设计理念,保证移动端内存回收不影响前台响应。
-
OpenAI:在 Java 微服务组件中,曾针对高内存占用场景评估不同 GC 策略与 JVM 参数的适配性,尤其是在大模型推理过程中优化内存释放。
💬 大厂面试题 & 答案
Q1(阿里巴巴):Java 中垃圾回收的原理是什么?
答:
Java 的垃圾回收基于可达性分析(GC Roots Tracing),判断一个对象是否可达。如果对象无法从 GC Roots(如线程栈、方法区静态变量)链路访问到,就被视为“不可达”,会被标记为垃圾,等待回收。
Q2(字节跳动):Java 中垃圾回收主要回收哪块内存?为什么?
答:
主要回收的是堆内存,因为堆是存放对象实例的区域,生命周期不确定且存在大量动态创建的对象。而线程栈、程序计数器等是随线程生命周期自动销毁的,不归 GC 管理。
Q3(美团):你了解过哪些主流的垃圾回收器?
答:
-
Serial GC(单线程,适合单核环境)
-
Parallel GC(吞吐量优先,多线程)
-
CMS GC(低停顿)
-
G1 GC(低延迟、分区管理)
-
ZGC / Shenandoah(超低停顿,支持大内存)
Q4(华为云):System.gc() 一定会立即触发垃圾回收吗?为什么?
答:
不一定。System.gc()
只是请求 JVM 进行一次垃圾回收,具体是否立即执行取决于 JVM 的实现。比如 HotSpot JVM 中,默认只是“建议”,不会强制触发。
💬 场景题 & 答案
场景 1(阿里巴巴):线上高峰期突然出现 Full GC 频繁,导致接口响应变慢,你怎么排查?
答:
-
第一,
jstat -gc
/Arthas
查看内存使用与 GC 频率; -
第二,分析堆内存是否“老年代”占满(可能是对象晋升过快);
-
第三,结合 GC 日志确认是否为大对象/内存泄漏问题;
-
第四,定位问题代码:如存在缓存未释放、循环引用等问题,及时优化。
-
最后,考虑调整 GC 策略(如从 CMS → G1)降低停顿。
场景 2(字节跳动):程序内存持续上涨但 GC 一直未触发,应该怎么做?
答:
-
检查 JVM 参数是否配置了较大的
-Xms/-Xmx
,导致堆空间充足、触发 GC 频率低; -
手动执行
System.gc()
试探是否有可回收对象(虽然不建议在生产用); -
使用
Arthas
的heapdump
导出内存快照,通过MAT
工具分析对象占用情况,确认是否内存泄漏或对象滞留问题。
4.1 方法区的回收
方法区中可以被回收的内容主要是不再使用的类信息。
JVM 卸载一个类,必须同时满足以下三个条件:
1️⃣ 该类的所有实例都已被回收(堆中无任何该类或其子类的实例)。
2️⃣ 加载该类的类加载器已被回收。
3️⃣ 该类对应的 java.lang.Class
对象没有在任何地方被引用。
代码示例:类的卸载机制
package chapter04.gc;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
public class ClassUnload {
public static void main(String[] args) throws InterruptedException {
try {
ArrayList<Class<?>> classes = new ArrayList<>();
ArrayList<URLClassLoader> loaders = new ArrayList<>();
ArrayList<Object> objs = new ArrayList<>();
while (true) {
URLClassLoader loader = new URLClassLoader(
new URL[]{new URL("file:D:\\lib\\")});
Class<?> clazz = loader.loadClass("com.itheima.my.A");
Object o = clazz.newInstance();
// objs.add(o);
// classes.add(clazz);
// loaders.add(loader);
System.gc();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
添加 JVM 启动参数:
-XX:+TraceClassLoading -XX:+TraceClassUnloading
✅ 注释掉 add()
代码后,能够满足上述三个条件,并在 System.gc()
调用时卸载对应类。
注意:
System.gc()
只是发出垃圾回收请求,不保证立即执行,是否进行回收由 JVM 判断。
执行效果截图:
🧠 理论理解
方法区(元空间)中除了存放类的结构信息、常量池等内容,还包含类的元数据。当满足**“无实例 + 类加载器被回收 + Class
对象无引用”**三个条件时,该类及其元数据会被卸载。这保证了 JVM 的方法区不会因不再使用的类而无限膨胀。OSGi、JSP 热部署等场景利用了类的动态卸载实现热更新。
🏢 企业实战理解
-
阿里巴巴:在微内核架构中实现动态模块热更新时,基于类加载器的卸载机制有效避免了“内存无法回收”的问题。
-
美团:JSP 热部署场景中通过观察
TraceClassUnloading
日志,确保新版本 JSP 文件生效且旧版本内存被卸载。 -
字节跳动:多租户 SaaS 平台利用动态类加载机制,实现租户隔离下的独立类加载器,防止内存互相影响。
-
华为云:在分布式应用中,为节省内存资源,采用动态类加载/卸载机制管理大量短生命周期的插件模块。
💬 大厂面试题 & 答案
Q1(阿里巴巴):什么条件下一个类会被卸载?
答:
必须满足以下三个条件:
1️⃣ 该类的所有实例对象都已被回收;
2️⃣ 加载该类的类加载器被回收;
3️⃣ 该类对应的 java.lang.Class
对象没有被引用。
Q2(字节跳动):为什么方法区的内存回收比堆内存更复杂?
答:
因为方法区存储的是类元数据、常量池等结构,而类可能涉及跨模块、跨线程引用,依赖关系复杂,回收时需要确保类不再被任何地方引用,且类加载器也要满足卸载条件。
Q3(美团):如何查看 JVM 是否卸载了某个类?
答:
启动 JVM 时添加 -XX:+TraceClassUnloading
参数,可以在 GC 时输出卸载日志,确认类是否已被卸载。
💬 场景题 & 答案
场景 1(美团):JSP 热部署后发现内存越来越大,定位发现是类无法卸载,怎么解决?
答:
-
检查 JSP 类对应的类加载器是否正常释放;
-
确认是否有静态变量/线程池等持有对类的引用,阻止卸载;
-
重启容器或使用
Arthas
的sc
命令查看类加载器状态,优化代码逻辑,避免内存泄漏。
场景 2(华为云):开发了一个插件化系统,频繁动态加载/卸载模块,发现方法区内存持续增长,怎么排查?
答:
-
确认插件模块的类加载器是否独立,是否在卸载时被释放;
-
检查是否有
ThreadLocal
、单例/缓存等静态持有类的引用; -
通过
-XX:+TraceClassUnloading
参数观察类是否真正卸载,若未卸载则排查持有引用点。
场景 3(字节跳动):用 System.gc()
后发现类依然没有卸载,什么原因?
答:
说明不满足 JVM 卸载条件:
1️⃣ 该类的实例对象可能还在堆中;
2️⃣ 加载它的类加载器尚未被回收;
3️⃣ Class
对象仍被其他地方引用。
需要结合 jmap
/ Arthas sc
工具确认引用链,解决引用问题后再触发 GC。
类卸载的应用场景
在日常开发中,这种场景较少见。但在一些动态模块加载/热部署场景中经常出现,例如:
-
OSGi 动态模块化系统
-
JSP 热部署:每个 JSP 文件对应一个类加载器,当文件修改时,会卸载旧的类加载器,重新加载 JSP 文件,达到热更新效果。