java垃圾回收
垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。
引用类型
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java 提供了四种强度不同的引用类型。
1. 强引用
被强引用关联的对象不会被回收。
使用 new 一个新对象的方式来创建强引用。
Object obj = new Object();
2. 软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。
使用 SoftReference 类来创建软引用。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
3. 弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
使用 WeakReference 类来创建弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
4. 虚引用
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。
使用 PhantomReference 来创建虚引用。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
垃圾回收详解
常见面试题:
- 如何判断对象是否死亡(两种方法)。引用计数法,已废弃。可达性分析法。
- 简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。
- 如何判断一个常量是废弃常量
- 如何判断一个类是无用的类
- 垃圾收集有哪些算法,各自的特点?
- HotSpot 为什么要分为新生代和老年代?
- 常见的垃圾回收器有哪些?
- 介绍一下 CMS,G1 收集器。
- Minor Gc 和 Full GC 有什么不同呢?
堆内存被通常分为下面三部分:
- 新生代内存(Young Generation). Eden和两个Survive
- 老生代(Old Generation)
- Metaspace(元空间) 取代,元空间使用的是直接内存 。
Young Generation ├── Eden (占 80%) ├── Survivor0 (占 10%) └── Survivor1 (占 10%)
垃圾回收步骤
新创建的对象默认分配在 Eden 区。
Eden 是新生代对象的第一个落脚点,如果没有 Eden,新建对象就必须直接进入 Survivor 区,但 Survivor 区容量小,GC 频率高,效率低;
Survivor 区之间是互换关系(复制算法),一个为 From,一个为 To,不能直接承担所有新对象分配压力。
Survivor 区的作用:复制与筛选机制
- Eden → Survivor To(S1)
- Survivor From(S0)中存活对象 → Survivor To(S1)。
- 之后 S1 和 S0 互换身份(From/To)
Eden 放入新对象
Minor GC:复制 Eden 中的活对象 → Survivor0
下次 GC:Survivor0 的活对象 → Survivor1,年龄+1
再次 GC:Survivor1 的活对象 → Survivor0,年龄继续+1
达到阈值(默认 15 岁):晋升到老年代
两个 Survivor 每次 GC 互换角色,叫“From / To” 区
如果只有一个 Survivor 区,GC 时 无法保证复制的“目标区域”是干净的,容易出现对象交错、碎片、崩溃等问题。
晋升到老年代(Tenured)的条件
- 每个对象在 Survivor 区中每次 GC 存活一次就会 年龄 +1
- 默认阈值是 15 次(可配置,见参数)
- 达到阈值后,对象就会进入 老年代
对象内存分配
优先Eden分配
- 大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
大对象直接老年代
- 大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。大对象直接进入老年代的行为是由虚拟机动态决定的,它与具体使用的垃圾回收器和相关参数有关。大对象直接进入老年代是一种优化策略,旨在避免将大对象放入新生代,从而减少新生代的垃圾回收频率和成本。
长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
大部分情况,对象都会首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间(s0 或者 s1)中,并将对象年龄设为 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。
对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。 这是因为每次minor GC可能会有对象晋升到老年代。
如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
不同区域GC
针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:
部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个 Java 堆和方法区。
Minor GC 和 Full GC
- Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
- Full GC:收集整个 Java 堆和方法区。老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多
Full GC 的触发条件
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
1. 调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
2. 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
3. 空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。
4. JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
5. Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
可达性分析算法
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:
- 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中 JNI 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
垃圾收集算法
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
JDK 默认垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version 命令查看):
- JDK 8: Parallel Scavenge(新生代)+ Parallel Old(老年代)
- 前者是标记复制,后者是标记整理
- JDK 9 ~ JDK22: G1
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代。
- 新生代使用:复制算法
- 老年代使用:标记 - 清除 或者 标记 - 整理 算法
JVM 参数 GC可调, 具有四种类型的 GC 实现:
- 串行垃圾收集器
- 并行垃圾收集器
- CMS 垃圾收集器
- G1 垃圾收集器
垃圾收集器

以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
细节参考 link
| 收集器 | 新生代 / 老年代 | 是否并发 | 是否压缩整理 | 暂停时间 | 使用场景 |
|---|---|---|---|---|---|
| Serial | 是 / 是 | 否 | 是 | 高 | 小内存、单核场景(如客户端) |
| Parallel | 是 / 是 | 否 | 是 | 中 | 吞吐优先场景,如批处理 |
| CMS | 是 / 是 | 部分并发 | 否 | 低 | 低延迟要求,但 JDK9 起废弃 |
| G1 | 是 / 是(Region) | 是 | 是(并发整理) | 中低 | 推荐,默认 GC,适合大多数服务端应用 |
| ZGC | 是 / 是(Region) | 全并发 | 是 | 极低 | 超低延迟,大内存应用,JDK11+ |
| Shenandoah | 是 / 是(Region) | 全并发 | 是 | 极低 | RedHat 主推,类似 ZGC,适合低延迟 |
- Serial 简单、低资源;
- Parallel 高吞吐;
- CMS 停顿短但容易碎片;CMS 不建议使用,G1 是推荐替代方案。
- G1 平衡型、适用于大多数服务端;
- ZGC/Shenandoah 面向超低延迟场景。
CMS 和 G1 有什么区别?
| 对比项 | CMS | G1 |
|---|---|---|
| 内存结构 | 新生代 + 老年代 | Region 动态分配 |
| 并发性 | 并发标记 | 并发标记 + 压缩 |
| 碎片问题 | 有 | 无 |
| 吞吐量 | 中 | 中高 |
| 已废弃 | 是 | 否(默认) |
STW 是 JVM 在某些 GC 阶段需要 暂停所有用户线程。为了减少影响:
- 使用并发收集器(如 G1、ZGC)
- 设置合理参数(如
MaxGCPauseMillis) - 减少大对象和频繁分配
- 使用 GC 日志分析 GC 次数与耗时
GC卡顿排查
- 开启 GC 日志:
-Xlog:gc*或-XX:+PrintGCDetails - 分析 GC 时间、频率、Full GC 是否频繁
- 观察堆使用情况、晋升失败等
- 使用工具(如 VisualVM、jstat、JFR、Arthas)诊断
G1
G1 的思路是 —— 以 Region 为单位整理活对象。 G1 通过在 GC 过程中,按 Region 单位将活对象搬迁并压缩,避免碎片化,同时又分阶段进行,减轻暂停负担。
具备如下特点:
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
✅ 具体流程如下:
- 标记活对象(Concurrent Marking)
- 通过并发标记阶段,识别出所有“存活对象”。 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 并计算每个 Region 的“回收价值”(垃圾比例)
- 筛选 Region
- G1 选择回收价值最高的部分 Region 进行回收(称为 收集集(Collection Set, CSet))
- 对象转移(Evacuation)
- 将存活对象从 CSet 中迁移到新的空 Region(可 Eden、Survivor、Old)
- 迁移时是压缩式移动:连续地复制活对象 → 减少碎片
- 更新引用
- 并发地通过记忆集(Remembered Set)和 SATB(Snapshot At The Beginning)机制,更新被引用地址
- 旧 Region 回收
- 所有对象被转移后,旧 Region 被标记为空,可重用
注意
- G1 的压缩不是在 Full GC 阶段才执行,而是在 Young GC 和 Mixed GC 中增量地进行
- 每次 GC 只处理部分 Region,减少一次性 Stop-The-World 的开销 。 首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
- 所以 G1 被称为支持“增量压缩”的收集器
QA
如何判断一个常量是废弃常量?
运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?
假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,”abc” 就会被系统清理出常量池了
如何判断一个类是无用的类?
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类”:
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的
ClassLoader已经被回收。 - 该类对应的
java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
即便满足了也不一定回收。