DAY01 - JVM - 垃圾回收机制详解 - 4. 垃圾回收


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 策略减少停顿时间,并利用 ArthasGCViewer 等工具分析 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() 试探是否有可回收对象(虽然不建议在生产用);

  • 使用 Arthasheapdump 导出内存快照,通过 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 的方法区不会因不再使用的类而无限膨胀。OSGiJSP 热部署等场景利用了类的动态卸载实现热更新。

🏢 企业实战理解

  • 阿里巴巴:在微内核架构中实现动态模块热更新时,基于类加载器的卸载机制有效避免了“内存无法回收”的问题。

  • 美团:JSP 热部署场景中通过观察 TraceClassUnloading 日志,确保新版本 JSP 文件生效且旧版本内存被卸载。

  • 字节跳动:多租户 SaaS 平台利用动态类加载机制,实现租户隔离下的独立类加载器,防止内存互相影响。

  • 华为云:在分布式应用中,为节省内存资源,采用动态类加载/卸载机制管理大量短生命周期的插件模块。

 

💬 大厂面试题 & 答案

Q1(阿里巴巴):什么条件下一个类会被卸载?

答:
必须满足以下三个条件:
1️⃣ 该类的所有实例对象都已被回收;
2️⃣ 加载该类的类加载器被回收;
3️⃣ 该类对应的 java.lang.Class 对象没有被引用。


Q2(字节跳动):为什么方法区的内存回收比堆内存更复杂?

答:
因为方法区存储的是类元数据、常量池等结构,而类可能涉及跨模块、跨线程引用,依赖关系复杂,回收时需要确保类不再被任何地方引用,且类加载器也要满足卸载条件。


Q3(美团):如何查看 JVM 是否卸载了某个类?

答:
启动 JVM 时添加 -XX:+TraceClassUnloading 参数,可以在 GC 时输出卸载日志,确认类是否已被卸载。

 

💬 场景题 & 答案

场景 1(美团):JSP 热部署后发现内存越来越大,定位发现是类无法卸载,怎么解决?

答:

  • 检查 JSP 类对应的类加载器是否正常释放;

  • 确认是否有静态变量/线程池等持有对类的引用,阻止卸载;

  • 重启容器或使用 Arthassc 命令查看类加载器状态,优化代码逻辑,避免内存泄漏。


场景 2(华为云):开发了一个插件化系统,频繁动态加载/卸载模块,发现方法区内存持续增长,怎么排查?

答:

  • 确认插件模块的类加载器是否独立,是否在卸载时被释放;

  • 检查是否有 ThreadLocal、单例/缓存等静态持有类的引用;

  • 通过 -XX:+TraceClassUnloading 参数观察类是否真正卸载,若未卸载则排查持有引用点。


场景 3(字节跳动):用 System.gc() 后发现类依然没有卸载,什么原因?

答:
说明不满足 JVM 卸载条件:
1️⃣ 该类的实例对象可能还在堆中;
2️⃣ 加载它的类加载器尚未被回收;
3️⃣ Class 对象仍被其他地方引用。
需要结合 jmap / Arthas sc 工具确认引用链,解决引用问题后再触发 GC。


类卸载的应用场景

在日常开发中,这种场景较少见。但在一些动态模块加载/热部署场景中经常出现,例如:

  • OSGi 动态模块化系统

  • JSP 热部署:每个 JSP 文件对应一个类加载器,当文件修改时,会卸载旧的类加载器,重新加载 JSP 文件,达到热更新效果。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏驰和徐策

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值