Java 垃圾回收机制详解
Java 垃圾回收机制自动管理堆内存,通过识别无用对象释放空间。主要判断算法包括引用计数法(存在循环引用缺陷)和可达性分析算法(基于 GC Roots)。常见回收算法有标记 - 清除、复制、标记 - 整理及分代收集策略。主流垃圾回收器包含 Serial、Parallel、CMS 和 G1,各自适用于不同场景,如低延迟或高吞吐量需求。G1 回收器通过逻辑分区 Region 优化了堆内存管理。

Java 垃圾回收机制自动管理堆内存,通过识别无用对象释放空间。主要判断算法包括引用计数法(存在循环引用缺陷)和可达性分析算法(基于 GC Roots)。常见回收算法有标记 - 清除、复制、标记 - 整理及分代收集策略。主流垃圾回收器包含 Serial、Parallel、CMS 和 G1,各自适用于不同场景,如低延迟或高吞吐量需求。G1 回收器通过逻辑分区 Region 优化了堆内存管理。

垃圾回收(Garbage Collection,GC)是 Java 虚拟机自动管理堆内存的机制,负责识别不再使用的对象并释放其占用的内存。
垃圾回收的触发机制如下:
**基本原理:**为每个对象分配一个专有的引用计数器,当一个对象被引用后,计数器 +1,引用失效后,计数器 -1,计数器为 0 时,表示对象不再被任何变量引用,可以被回收。
这个算法非常简单易懂,可是它有一个致命的缺点,就是循环引用的问题,接下来我们来看个循环引用的例子。
// 一个简单的循环引用例子
class RefObject {
public Object instance = null;
}
public class Main {
public static void main(String[] args) {
RefObject objA = new RefObject(); // objA 引用计数 = 1
RefObject objB = new RefObject(); // objB 引用计数 = 1
objA.instance = objB; // objB 引用计数 = 2 (被 objA.instance 引用)
objB.instance = objA; // objA 引用计数 = 2 (被 objB.instance 引用)
objA = null; // objA 引用计数减为 1 (仅被 objB.instance 引用)
objB = null; // objB 引用计数减为 1 (仅被 objA.instance 引用)
// 此时,两个对象已经无法被外界访问(objA 和 objB 变量都指向了 null),
// 但它们彼此引用,引用计数均为 1,无法被引用计数算法回收。
}
}
可以看到,当 objA 和 objB 设置为 null 时,正常来说应该被垃圾回收,可是由于 objA 被 objB 引用,objB 被 objA 引用,导致计数器不为 0 无法被回收,这就是循环引用的问题所在。所以大部分情况我们都是使用下面这种算法。
**基本原理:**通过一系列称为'GC Roots'的根对象作为起始节点,从这些根节点开始,根据引用关系向下搜索,如果某个对象到 GC Roots 间没有任何引用链相连,则证明此对象是不可用的,可以被回收。
它与引用计数法最核心的区别在于:它不关心对象被引用了多少次,只关心能否从根节点触达。

该算法从根本上解决了循环引用的问题。在上面的例子中,虽然 A 和 B 互相引用,但只要它们无法从 GC Roots 到达,就会被判定为垃圾。
然而,可达性分析算法也有缺点。从原理那我们也不难看出,可达性分析算法的实现相比于引用计数法更加复杂了,同时,为了保证分析结果的准确性,在进行可达性分析时,必须在一个一致性快照中进行。这意味着在分析期间,整个执行系统必须被冻结,不允许对象的引用关系发生变化。这个过程就是著名的 Stop The World(STW),是垃圾收集器产生停顿的主要原因之一。
**基本原理:**先标记所有存活对象,再清除未标记对象。
一个简单的实现如下:
// 算法伪代码示意
public class MarkSweep {
void mark(Object obj) {
if (obj != null && !obj.isMarked()) {
obj.setMarked(true);
mark(obj.references); // 递归标记所有引用对象
}
}
void sweep(Heap heap) {
for (Object obj : heap.objects) {
if (!obj.isMarked()) {
heap.free(obj); // 释放未标记对象
} else {
obj.setMarked(false); // 重置标记位
}
}
}
}
该算法简单直接,不过效率不高而且会产生内存碎片。
**基本原理:**内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。

该算法解决了不会产生内存碎片的问题,可又带来了新的问题:每次申请内存时只能申请一半的内存空间,内存利用率严重不足。
**基本原理:**标记 - 整理算法的'标记'过程与'标记 - 清除算法'的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。

该算法既解决了内存碎片问题的产生,也提高内存利用率,不过整理阶段需要移动对象,开销较大。
**基本原理:**分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的垃圾回收次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值 (默认是 15)后,如果对象还存活,那么该对象会进入老年代。
不同分代使用的回收机制也不同,我们来介绍下这两种内存划分。
新生代:
老年代:
JVM 配置示例:
# JVM 参数 -XX:+UseSerialGC
实际上是 Serial 收集器的多线程版本
JVM 配置示例:
# JVM 参数 -XX:+UseParallelGC
# 新生代 Parallel Scavenge,老年代 Serial Old
-XX:+UseParallelOldGC
# 新生代 Parallel Scavenge,老年代 Parallel Old
-XX:+UseParNewGC
# 新生代 ParNew,需配合 CMS 使用
-XX:ParallelGCThreads=n
# 设置并行 GC 线程数
适合对延迟不敏感,高吞吐量需求的场景。
老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短 GC 回收停顿时间。

收集过程:
JVM 配置示例:
# JVM 参数 -XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=68 # 老年代使用率触发阈值
-XX:+UseCMSCompactAtFullCollection # 开启碎片整理
相应的,CMS 回收器也有部分缺点,如对 CPU 资源敏感,无法处理浮动垃圾(并发时新产生的垃圾),内存碎片问题(标记 - 清除算法)。
这是目前使用率最高的垃圾回收器,它做了一个革命性的设计:不再物理分代,而是逻辑分区的 Region,此外,G1 收集器不同于之前的收集器的一个重要特点是:G1 回收的范围是整个 Java 堆 (包括新生代,老年代),而前几种收集器回收的范围仅限于新生代或老年代。
下面是 G1 垃圾回收器的基本流程:

JVM 配置示例:
# JVM 参数 -XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标最大停顿时间
-XX:G1HeapRegionSize=n # Region 大小(1M~32M,2 的幂)

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online