【JVM】垃圾回收

你这段整体思路是对的,已经很像面试回答了。可以按 "什么是垃圾 → 怎么找垃圾 → 怎么回收垃圾 → JVM 为什么分代 → 常见垃圾回收器 → GC 调优" 这个顺序理解。

我帮你重新解释一遍,并顺便指出几个容易说错的点。


一、什么是垃圾?

在 Java 中,垃圾对象 指的是:

程序后续不可能再使用到的对象。

比如:

java 复制代码
User user = new User();
user = null;

原来的 new User() 对象已经没有变量引用它了,后续无法再访问,它就可能成为垃圾。


二、怎么判断一个对象是不是垃圾?

1. 引用计数法

早期思想是:每个对象维护一个引用计数器。

java 复制代码
User user = new User();

这个对象被 user 引用,引用计数 +1。

java 复制代码
user = null;

引用断开,引用计数 -1。

如果引用计数变成 0,就认为对象是垃圾。

但是它有一个大问题:循环引用

java 复制代码
class A {
    B b;
}

class B {
    A a;
}

假设:

java 复制代码
A a = new A();
B b = new B();

a.b = b;
b.a = a;

a = null;
b = null;

虽然外部已经无法访问 A 和 B 了,但 A 和 B 彼此引用,引用计数都不是 0。

所以引用计数法会误以为它们不是垃圾,从而无法回收,造成内存泄漏。


2. 根可达性分析算法

Java 主流 JVM 使用的是 GC Roots 可达性分析算法

它的核心思想是:

从一组特殊对象 GC Roots 出发,沿着引用链往下找,能找到的对象就是存活对象,找不到的对象就是垃圾对象。

可以理解成这样:

text 复制代码
GC Roots
   |
   v
 对象 A
   |
   v
 对象 B

A、B 能从 GC Roots 访问到,所以它们不是垃圾。

如果某个对象完全不在这条引用链上,就说明程序已经没办法访问它了,它就是垃圾。

常见的 GC Roots 有:

text 复制代码
1. 虚拟机栈中引用的对象
2. 方法区中静态变量引用的对象
3. 方法区中常量引用的对象
4. 本地方法栈 JNI 引用的对象
5. 正在运行的线程对象

比如:

java 复制代码
public static User staticUser = new User();

public void test() {
    User localUser = new User();
}

staticUser 引用的对象可以作为静态变量关联对象存活。

localUser 引用的对象在方法执行期间,也可以通过虚拟机栈中的局部变量表找到,所以暂时不是垃圾。


三、垃圾回收算法

垃圾找出来之后,JVM 要考虑怎么回收。常见算法主要有三类。


1. 标记-清除算法

流程:

text 复制代码
第一步:标记存活对象或垃圾对象
第二步:清除垃圾对象

示意:

text 复制代码
回收前:
[存活][垃圾][存活][垃圾][存活]

回收后:
[存活][空闲][存活][空闲][存活]

优点:

text 复制代码
实现相对简单
不用移动对象
回收速度相对较快

缺点:

text 复制代码
会产生内存碎片

所谓内存碎片就是:虽然总的空闲空间够,但不连续。

比如现在有三个空闲块:

text 复制代码
2MB + 3MB + 4MB = 9MB

但是如果你要分配一个 6MB 的大对象,可能找不到连续空间,还是会触发 GC,甚至 OOM。

所以你说的"会产生内存碎片,造成内存浪费和溢出"是对的。


2. 标记-整理算法

流程:

text 复制代码
第一步:标记存活对象
第二步:把存活对象向一端移动
第三步:清理边界外的垃圾

示意:

text 复制代码
回收前:
[存活][垃圾][存活][垃圾][存活]

整理后:
[存活][存活][存活][空闲][空闲]

优点:

text 复制代码
不会产生内存碎片
适合老年代

缺点:

text 复制代码
需要移动对象
效率比标记-清除低

因为对象移动之后,对象的引用地址也需要更新,成本比较高。


3. 复制算法

流程:

把内存分成两块,每次只使用其中一块。

text 复制代码
From 区:正在使用
To 区:空闲

GC 时,把存活对象复制到 To 区,然后清空 From 区。

示意:

text 复制代码
From 区:
[存活][垃圾][垃圾][存活]

复制到 To 区:
[存活][存活][空闲][空闲]

然后 From 区整体清空

优点:

text 复制代码
不会产生内存碎片
回收效率高

缺点:

text 复制代码
浪费一部分内存空间
如果存活对象很多,复制成本会很高

所以复制算法非常适合年轻代。

因为年轻代对象大多是"朝生夕死",垃圾多,存活对象少,需要复制的对象少,效率就很高。


四、为什么要分代垃圾回收?

Java 堆中的对象生命周期差异很大。

有些对象:

java 复制代码
new User();

可能方法执行完就没用了。

有些对象,比如缓存、单例、Spring Bean,可能会存活很久。

所以 JVM 使用了 分代思想

text 复制代码
年轻代:存放生命周期短的对象
老年代:存放生命周期长的对象

这样可以做到:

text 复制代码
年轻代频繁回收
老年代较少回收
不同区域使用不同回收算法

五、年轻代和老年代怎么回收?

年轻代

年轻代通常分为:

text 复制代码
Eden 区
Survivor From 区
Survivor To 区

大多数新对象会先分配到 Eden 区。

当 Eden 区满了,会触发 Minor GC。

Minor GC 时:

text 复制代码
Eden 和 From 中存活的对象复制到 To 区
然后清空 Eden 和 From
From 和 To 角色互换

对象每熬过一次 Minor GC,年龄通常会 +1。

当对象年龄达到一定阈值,比如默认最大年龄 15 左右,就可能晋升到老年代。

所以年轻代一般使用 复制算法


老年代

老年代对象存活时间长,回收频率低。

如果老年代也用复制算法,就会很亏,因为老年代存活对象多,复制成本高。

所以老年代通常使用:

text 复制代码
标记-清除
标记-整理
或者两者结合

六、CMS 垃圾回收器

CMS 全称是 Concurrent Mark Sweep

它主要是针对 老年代 的垃圾回收器。

它的目标是:

尽量减少 STW 停顿时间,追求低延迟。

STW 是 Stop The World,意思是垃圾回收时暂停所有用户线程。

CMS 的大致过程:

text 复制代码
1. 初始标记
2. 并发标记
3. 重新标记
4. 并发清除

其中:

text 复制代码
初始标记、重新标记需要 STW
并发标记、并发清除可以和用户线程一起执行

所以 CMS 停顿时间比较短。

但是 CMS 有几个问题:

text 复制代码
1. 使用标记-清除算法,会产生内存碎片
2. 并发执行时会占用 CPU 资源
3. 并发清理过程中用户线程还在运行,可能产生浮动垃圾

你原文中说:

CMS 采用标记清除算法,优点是速度快;并发标记和并发清理允许用户线程和 GC 线程同时工作。

这个说法是可以的。

不过要补充一句:

CMS 的缺点是容易产生内存碎片,所以后来逐渐被 G1 替代。


七、G1 垃圾回收器

G1 全称是 Garbage First

它是一个面向服务端应用的垃圾回收器,特点是:

可以设置期望的最大停顿时间,并尽量在这个时间内完成 GC。

比如:

bash 复制代码
-XX:MaxGCPauseMillis=200

意思是希望每次 GC 停顿尽量控制在 200ms 以内。

注意:这是 目标,不是绝对保证。


G1 的核心思想

传统分代是物理连续的:

text 复制代码
年轻代一整块
老年代一整块

G1 把整个堆切成很多大小相等的 Region:

text 复制代码
Region 1
Region 2
Region 3
Region 4
...

每个 Region 可以扮演不同角色:

text 复制代码
Eden Region
Survivor Region
Old Region
Humongous Region

G1 会统计每个 Region 的垃圾比例和回收收益。

回收时优先回收垃圾最多、收益最高的 Region。

这就是 Garbage First 的含义:

垃圾最多的区域优先回收。


G1 为什么能控制停顿时间?

因为它不是每次都回收整个老年代,而是挑一部分 Region 回收。

比如它估算:

text 复制代码
回收 20 个 Region 可能需要 180ms
回收 30 个 Region 可能需要 260ms

如果目标停顿时间是 200ms,它就可能只回收 20 个 Region。

所以 G1 可以做到相对可预测的停顿时间。


八、GC 调优怎么说?

GC 调优本质上不是盲目调参数,而是:

根据业务场景,在吞吐量、延迟、内存占用之间做权衡。

常见目标有两个:

1. 吞吐量优先

适合后台任务、批处理、计算任务。

比如:

text 复制代码
任务可以接受短暂停顿
但希望整体处理速度快

这种场景可以考虑 Parallel GC。

JDK 8 默认常见就是 Parallel GC。


2. 低延迟优先

适合接口服务、交易系统、实时系统。

这种场景希望:

text 复制代码
单次 GC 停顿尽量短
请求响应时间稳定

JDK 8 可以考虑 CMS 或 G1。

更现代的 JDK 里,可以考虑 G1、ZGC、Shenandoah。

但如果你面试说 JDK 8,重点说 CMS 和 G1 就够了。


九、你这段里有一个点需要修正

你原文说:

JDK7 到 JDK8 的变化是把方法区的实现从堆内存移到了本地内存。

这个大方向是对的,但表达可以更准确:

方法区是 JVM 规范中的概念。JDK 7 及以前,HotSpot 使用永久代 PermGen 来实现方法区;JDK 8 移除了永久代,改用元空间 Metaspace 实现方法区,而元空间使用的是本地内存。

也就是说:

text 复制代码
方法区:规范概念
永久代:JDK 8 之前 HotSpot 对方法区的实现
元空间:JDK 8 之后 HotSpot 对方法区的实现

为什么要移除永久代?

因为永久代大小受 JVM 参数限制,容易出现:

text 复制代码
java.lang.OutOfMemoryError: PermGen space

尤其是大量动态生成类、频繁部署应用、反射、代理、框架较多的场景。

JDK 8 之后,类元信息放到本地内存中的 Metaspace,默认情况下会根据系统内存动态扩展,所以更不容易因为类元信息太多导致永久代 OOM。

但注意:元空间也不是无限的,也可以通过参数限制:

bash 复制代码
-XX:MaxMetaspaceSize

十、逃逸分析和栈上分配为什么能减轻 GC 压力?

正常情况下,对象大多分配在堆上。

堆上的对象需要 GC 管理。

但是 JVM 可以通过逃逸分析判断:

这个对象会不会逃出当前方法或当前线程?

如果对象没有逃逸,JIT 编译器可能会进行优化,比如:

text 复制代码
栈上分配
标量替换
锁消除

比如:

java 复制代码
public void test() {
    User user = new User();
    user.name = "Tom";
    user.age = 18;
}

如果 user 没有返回,也没有赋值给外部变量,也没有被其他线程访问,那么 JVM 可能认为它没有逃逸。

这时 JVM 可能不真的在堆上创建完整对象,而是把它拆成几个局部变量:

text 复制代码
name = "Tom"
age = 18

这样对象都没进堆,自然就减少了 GC 压力。

不过面试里说的时候最好加一句:

逃逸分析是一种 JVM 优化手段,不代表所有未逃逸对象一定会栈上分配,具体要看 JIT 优化结果。


面试版回答可以这样说

你这段可以整理成下面这版:

Java 的垃圾回收主要解决两个问题:第一,判断哪些对象是垃圾;第二,如何回收这些垃圾对象。

判断垃圾对象时,早期有引用计数法,但是引用计数法解决不了循环引用问题,所以 Java 主流 JVM 使用的是 GC Roots 可达性分析算法。从 GC Roots 出发,沿着引用链能够访问到的对象就是存活对象,访问不到的对象就是垃圾对象。常见的 GC Roots 包括虚拟机栈中的引用、静态变量引用、常量引用、本地方法栈 JNI 引用等。

找到垃圾之后,常见回收算法有三种:标记-清除、标记-整理和复制算法。标记-清除效率较高,但会产生内存碎片;标记-整理不会产生碎片,但需要移动对象,成本较高;复制算法效率高、没有碎片,但会浪费一部分空间,适合存活对象少的区域。

因为 Java 对象生命周期不同,所以 JVM 采用分代回收思想。年轻代对象大多朝生夕死,适合使用复制算法;老年代对象存活时间长,适合使用标记-清除或标记-整理算法。

常见垃圾回收器中,CMS 是针对老年代的低停顿垃圾回收器,采用标记-清除算法,并且在并发标记、并发清除阶段可以和用户线程一起执行,因此停顿时间较短,但缺点是会产生内存碎片。G1 是面向整个堆的垃圾回收器,它把堆划分成多个 Region,优先回收垃圾收益高的 Region,并且可以设置期望的最大停顿时间,适合大堆内存、低延迟场景。

GC 调优方面,通常会根据业务场景选择合适的垃圾回收器。如果追求吞吐量,可以考虑 Parallel GC;如果追求低延迟,可以考虑 CMS、G1,现代 JDK 还可以考虑 ZGC。除此之外,还可以通过合理设置堆大小、年轻代比例、排查频繁 Full GC、减少对象创建、利用逃逸分析等方式降低 GC 压力。


你原来的内容已经比较完整了。最需要注意的是这三点:

text 复制代码
1. G1 的停顿时间是"尽量达到目标",不是绝对保证。
2. JDK8 不是"方法区没了",而是"方法区的实现从永久代变成元空间"。
3. 逃逸分析不等于一定栈上分配,更常见的优化还有标量替换。
相关推荐
KobeSacre2 小时前
JVM ZGC
java·开发语言·jvm
J-Tony1110 小时前
【JVM】垃圾回收器
jvm
思麟呀11 小时前
C++11并发编程:条件变量
java·linux·jvm·c++·windows
未若君雅裁11 小时前
JVM 是什么:组成、运行流程与整体架构
jvm·架构
light blue bird12 小时前
3C 数码电子BOM 协同工作台组件
java·开发语言·jvm·windows·.net·桌面端
wuminyu1 天前
Java锁机制之轻量级锁判断与尝试逻辑源码剖析
java·linux·c语言·jvm·c++
DO your like1 天前
CMS场景YGC失败导致FULL GC的总结
jvm
墨痕无声1 天前
JVM(六)
jvm
右耳朵猫AI1 天前
Java/JVM周刊2026W21 | Java 26发布、JDK 27抢先体验、Spring Boot 4.1预告、GlassFish 8.0.2发布
java·jvm·spring boot