java抢垃圾回收过程
Java的垃圾回收(Garbage Collection,简称 GC)是Java虚拟机(JVM)自动管理内存的核心机制。理解GC的过程,可以从“怎么判断是垃圾”、“在哪里回收”、“用什么算法回收”以及“具体回收流程(对象的一生)”这四个维度来剖析。
以下是完整的 Java 垃圾回收过程详解:
第一步:如何判定对象是“垃圾”?
JVM 在回收内存前,必须先确定哪些对象已经“死了”(即不再被任何途径引用的对象)。主要有两种算法:
- 引用计数法(Reference Counting):
- 原理:给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1;引用失效时,计数器减1。计数器为0的对象就是垃圾。
- 缺点:Java没有采用这种算法,因为无法解决循环引用的问题(例如:A引用B,B引用A,此外无其他引用,导致两者计数器都不为0,无法回收)。
- 可达性分析算法(Reachability Analysis):
- 原理:以一系列被称为 “GC Roots” 的根对象作为起点,从这些节点开始向下搜索。如果一个对象到 GC Roots 没有任何引用链相连(用图论的话说,就是从 GC Roots 到这个对象不可达),则证明此对象不可用。
- 哪些可以作为 GC Roots:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI(Native方法)引用的对象。
- JVM内部的引用(如系统类加载器、异常对象等)。
第二步:在哪里回收?(分代收集理论)
Java 堆内存(Heap)是 GC 的主战场。根据对象存活周期的不同,JVM 将堆内存分为新生代(Young Generation)和老年代(Old Generation):
- 新生代(Young Gen):
- 占堆内存的 1/3 左右。
- 特点:绝大多数对象都是“朝生夕死”的。
- 细分为:Eden 区、Survivor From区(S0)、Survivor To区(S1),默认比例为 8:1:1。
- 老年代(Old Gen):
- 占堆内存的 2/3 左右。
- 特点:存放生命周期长的、大对象。
第三步:怎么回收?(三大垃圾收集算法)
针对不同的内存区域,采用不同的回收算法:
- 标记-复制算法(Copying)(常用于新生代):
- 将内存分为两块,每次只使用一块。垃圾回收时,将存活的对象复制到另一块上,然后清除当前这块的所有空间。
- 优点:效率高,没有内存碎片。
- 缺点:浪费了一部分内存空间(在新生代中通过 8:1:1 的 Eden/Survivor 划分,将空间浪费降到了 10%)。
- 标记-清除算法(Mark-Sweep)(常用于老年代):
- 分为“标记”和“清除”两个阶段:先标记出所有需要回收的对象,然后统一回收。
- 缺点:会产生大量不连续的内存碎片,导致后续大对象无法找到连续空间而触发另一次 GC。
- 标记-整理算法(Mark-Compact)(常用于老年代):
- 标记过程与“标记-清除”一样,但后续步骤是让所有存活的对象都向内存一端移动,然后直接清理掉边界以外的内存。
- 优点:没有内存碎片,不需要浪费额外空间。
- 缺点:移动对象需要暂停用户线程(STW),耗时较长。
第四步:具体回收过程(对象的一生)
这是最核心的执行过程,通常被称为 Minor GC(YGC) 和 Full GC 的交替进行:
1. 对象的诞生
大多数新创建的对象会被分配在 Eden 区。
2. 第一次 Minor GC(新生代垃圾回收)
当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC:
- STW(Stop The World):暂停所有的用户线程(时间极短)。
- 进行可达性分析,找出 Eden 区中所有的存活对象。
- 将 Eden 区和 S0(From)区中存活的对象,一次性复制到 S1(To)区。
- 清空 Eden 和 S0 区。
- 存活的对象年龄加 1(此时在 S1 中,年龄为 1)。
- S0 和 S1 角色互换:此时 S1 变成下一次的 From 区,S0 变成 To 区(始终保持 To 区是空的)。
3. 重复 Minor GC(晋升过程)
随着业务运行,Eden 区再次满了,再次触发 Minor GC:
- 把 Eden 和当前的 From 区(此时是 S1)中存活的对象复制到 To 区(此时是 S0)。
- 存活对象年龄再加 1。
- 当对象的年龄达到阈值(默认是 15 岁,可以通过参数
-XX:MaxTenuringThreshold设置),这些“老兵”就会被晋升(Promote)到老年代。 - 特殊情况(动态对象年龄判定):如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到15岁。
- 特殊情况(大对象直接入老年代):非常大的对象(比如很长的字符串或数组)在分配时,如果 Eden 放不下,会直接进入老年代(避免在两个 Survivor 区之间来回复制)。
4. Full GC(整堆垃圾回收)
当老年代空间不足,或者系统检测到晋升到老年代的平均大小大于老年代的剩余空间时,会触发 Full GC(或者 Major GC):
- Full GC 会对整个堆内存(新生代、老年代)以及方法区(元空间)进行彻底的垃圾回收。
- STW 时间较长:因为老年代对象多,标记-整理/清除算法比较慢,会导致明显的系统卡顿。
- 如果 Full GC 之后,依然没有足够的内存分配给新对象,且堆也无法再扩容,就会抛出
OutOfMemoryError: Java heap space(内存溢出)。
第五步:垃圾收集器(执行 GC 的工具)
JVM 提供了不同的垃圾收集器,根据业务场景选择:
- Serial / Serial Old:单线程收集器,进行 GC 时必须暂停所有工作线程(STW),适合单核处理器、桌面应用。
- Parallel Scavenge / Parallel Old:多线程收集器,追求高吞吐量(让CPU更多地用于运行用户代码),是 JDK 8 的默认收集器。
- CMS(Concurrent Mark Sweep):以获取最短回收停顿时间(低延迟)为目标的收集器,第一次实现了垃圾回收线程与用户线程几乎同时工作。但会产生碎片(已被废弃)。
- G1(Garbage-First):JDK 9+ 的默认收集器。将堆内存拆分为多个独立的 Region(区域),不要求连续,能精确控制停顿时间,支持大内存。
- ZGC:JDK 11 引入的超低延迟垃圾收集器(停顿时间控制在 10ms 以内),能处理 TB 级别的堆内存。
总结口诀
新对象,生 Eden;
满则筛,剩入 S(Survivor);
S0/S1 倒手转,每转一次长一岁;
满十五,入老年;
老年满,Full GC(大扫除),扫不干净 OOM!