1. 从一次线上Full GC说起:垃圾回收为什么值得深入学
做Java开发这十几年,我见过太多次线上事故是因为垃圾回收(GC)出了问题。最典型的一次是服务在凌晨大促前突然CPU飙到100%,接口响应从几十毫秒变成几十秒,日志里全是“Allocation Failure”和Full GC告警。当时团队里几个新手一脸懵:明明堆内存还有空间,为什么一直在GC?后来dump堆出来一看,某个缓存类把业务对象全塞进了一个静态Map,几百万条数据全进了老年代,对象又被业务代码错误地持续引用,GC线程拼命回收却永远收不完,最终把CPU耗尽。
从那次之后我就深刻意识到,Java虚拟机(JVM)的垃圾回收机制不是面试题里的八股文,而是每一个Java开发者都需要真正吃透的底层能力。它决定了你的应用能支撑多大流量、能不能扛住突发峰值、内存是不是被白白浪费。这篇文章我会从JVM的内存划分讲起,把对象判定、回收算法、主流收集器、调优方法一条线串起来,结合我自己踩过的坑和实战经验来拆解,尽量让零基础的同学也能看懂,让有经验的开发者也能从原理层面补上这块短板。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先看战场:JVM内存区域与“垃圾”的本质
2.1 堆内存分代:为什么新生代、老年代要分开管
垃圾回收解决的核心问题只有一个:哪些内存可以回收,用什么方式回收。但在讨论回收之前,必须先搞清楚回收的战场在哪里。
JVM运行时数据区里,**堆(Heap)**是垃圾回收的主战场。几乎所有对象实例都在堆上分配,虚拟机规范里对堆没有做物理上的划分要求,但几乎所有商用JVM都会把堆分成新生代(Young Generation)和老年代(Old Generation)两个逻辑区域。新生代里又细分为Eden区、From Survivor区、To Survivor区,默认比例大概是8:1:1,这个比例可通过-XX:SurvivorRatio调整。
这种分代设计的出发点非常朴素:绝大多数对象“朝生夕灭”。我做过的多个高并发业务系统里,统计过对象存活情况,大概90%以上的对象在创建后很快就变成不可达状态,能被下一次Minor GC清理掉。如果所有对象都放在一个区域里统一管理,每次回收都要遍历整个堆,代价高到难以接受。分代之后,新生代只需复制少量存活对象,老年代则通过频率更低、方式不同的回收策略来处理长生命周期对象,效率能高几个量级。
还有一个特别容易忽略的点:大对象会直接进入老年代。用-XX:PretenureSizeThreshold可以设置大对象阈值,超过该值的对象直接在老年代分配,目的是避免在Eden区和两个Survivor区之间发生大量复制。但这里有个隐患:如果大对象越来越多,老年代很快就会被塞满,进而触发Full GC。我见过有团队为了“减少Minor GC次数”把这个阈值调得很低,结果老年代暴涨,Full GC从每天几次变成每小时几十次,性能直接崩掉。
2.2 方法区和运行时常量池:容易被忽视的回收目标
除了堆,方法区(Method Area)也是垃圾回收的覆盖范围。在JDK 8及以后,方法区的实现是元空间(Metaspace),它使用的是本地内存,默认情况下上限是物理内存大小,所以更容易被忽略。方法区回收的内容主要是废弃的常量和不再被使用的类,判定规则比堆里的对象严格得多。
对类卸载的条件有非常苛刻的要求:所有该类的实例都已被回收、加载该类的ClassLoader已被回收、该类对应的Class对象没有任何地方被引用,三个条件缺一不可。所以实际工作中,只有自定义ClassLoader配合框架使用得当,类卸载才会发生。在开发Web应用时,我们通常会限制元空间的大小,比如-XX:MaxMetaspaceSize=256m,避免因为动态生成类(CGLIB代理、热部署等场景常见)导致元空间无限制膨胀,甚至把物理内存打满。
这里给刚学JVM的同学一个建议:先别把精力放在如何“手动调优”方法区上,绝大多数业务应用只要保证元空间上限合理,就不会有大问题。真正需要花时间的是理解堆里的GC机制,这是面试和实战的重头戏。
3. 判定对象“生死”:谁的引用还有效,谁该被清扫
3.1 引用计数法的致命缺陷:循环引用
垃圾回收要做的第一步是判断哪些对象“已死”。最直观的思路是引用计数法(Reference Counting):每个对象维护一个计数器,记录有多少地方引用它,计数器为0时对象就可以被回收。这个算法实现简单、判定效率高,Python的早期版本和很多脚本语言都在用。
但引用计数法有一个绕不过去的硬伤:循环引用。比如对象A持有对象B的引用,对象B也持有对象A的引用,除此之外再也没有其他路径引用它们两个,那么A和B的引用计数永远为1,永远不会被回收,内存越积越多。这个缺陷在纯面向对象的业务系统里太容易触发了,比如两个业务实体互相关联、观察者模式中监听者持有被观察者的引用而对方也记着监听者列表。所以主流的Java虚拟机没有选择引用计数法,而是采用可达性分析(Reachability Analysis)。
3.2 可达性分析:从GC Roots出发的“地图探测”
可达性分析的核心思路
