首先,JVM 的垃圾回收,核心目标就一个:自动帮我们处理堆内存中不再使用的对象,释放内存空间,避免内存泄漏和 OOM(OutOfMemoryError)。Java 的 GC 从一开始就是个“后台默默干活”的存在,不过,真想明白它,咱得知道它干活的区域和干活的逻辑。
先说内存划分。JVM 把堆分成了两个主要区域:新生代(Young Generation)和老年代(Old Generation)。新生代里又细分成 Eden 区和两个 Survivor 区(From 和 To)。对象一出生先扔进 Eden 区,经过几次 Minor GC 后还活着的,就慢慢“熬”到老年代。
这时常有人问:GC 是怎么判断一个对象“死了”?这问题问得挺哲学,但 JVM 是靠根可达性(GC Roots)来判断。只要一个对象能从 GC Roots(比如线程栈、本地变量表、静态引用等)一路追踪到,它就还活着;追不到?那就准备被回收。
接下来是大家爱问的回收器。常见组合就是新生代用 ParNew 或 G1 的年轻代部分,老年代用 CMS 或 G1 的老年代部分。面试官一听你说出这些名字,眼睛会亮一下。
比如用 G1 的时候,它不是传统意义上的“代”区分,而是把堆划分成一堆大小相等的 region。新生代、老年代、甚至 humongous object 都是通过 region 组织起来的。G1 的好处是可以做预测性回收,比如你能通过 -XX:MaxGCPauseMillis=200
设置最大 GC 停顿时间,JVM 会尽量控制在这之内。
不过,别的回收器也不能就随便糊弄。比如 CMS(Concurrent Mark Sweep),它虽然是并发回收,但也不是没代价。它要先停顿(Stop-The-World)做一次标记,再在应用运行过程中并发标记和清除,最后还得来一次 Stop-The-World 的重新标记。这也就导致 CMS 容易出现浮动垃圾的问题,因为清除阶段对象还在不断变化。
再说点实战相关的。很多人不理解 Minor GC 和 Full GC 的区别。Minor GC 是回收新生代的,代价比较低,频率高;Full GC 是全场性的,包括老年代和元空间(以前叫永久代),而且会 Stop-The-World,代价大,一旦频繁,就得查内存泄漏或对象“养老”过快的问题了。
来看一段代码直观一点:
public class GCTest {
public static void
main(String[] args) {
// 模拟对象快速创建
for (int i = 0; i 10000; i++) {
byte[] b = new byte[1024 * 100];
// 每次创建 100KB 对象
}
}
}
这段代码会很快触发 Minor GC。你可以加上如下 JVM 参数观察:
-XX:+PrintGCDetails -Xms10m -Xmx10m -Xmn5m
其中 -Xmn5m
是设置新生代为 5MB,基本几轮就能看到 Eden 区满了,然后 Survivor 区搬迁,搬不动的对象晋升到老年代。老年代撑不住了?恭喜你,Full GC 登场!
还得说说对象晋升策略。JVM 不是所有对象都会等着满年龄晋升老年代。如果 Survivor 区装不下,或者大对象直接超过阈值(默认 PretenureSizeThreshold
),就可能直接被送进老年代,这也是导致老年代撑不住的常见元凶。
再来点“送命题”:你了解 GC 的三色标记吗?
这其实是 G1、CMS 等并发收集器的底层实现原理,它们用白、灰、黑三种标记来追踪对象引用。白色是“未处理的”,灰色是“待处理的”,黑色是“已处理且安全的”。灰色对象是个“中间态”,它的引用还要进一步扫描。这里就涉及
SATB(Snapshot-At-The-Beginning)算法,专门解决并发标记时对象图变动的问题。
讲这些面试官就知道你不是看了两篇博客就来混面试的,属于真写过代码、真出过 GC 问题、真调过参数的那种选手。
最后说个实话,GC 听起来高深,其实更多是靠你项目里的实际运行情况去调优,而不是死记 JVM 参数。你跑个大数据项目,参数配置跟跑一个电商秒杀系统完全不是一回事,盲目套模板只会出事。
建议大家平时多用 VisualVM、jstat、GCLogAnalyzer 这些工具,把 GC 日志跑一遍分析下,能发现不少平时看不到的细节。别到面试现场才开始“GC 从入门到放弃”,那就晚了。