深度分析Java内存回收机制
内存回收机制是Java区别于C/C++等语言的核心特性之一,也是Java开发者理解程序性能、解决内存相关问题(如内存泄漏、OOM)的关键。
核心目标: 自动回收程序中不再使用的对象所占用的内存,防止内存耗尽,同时尽量减少对程序执行的影响(特别是停顿时间)。
一、 基础概念与内存模型
- 自动内存管理: Java开发者无需(也无法)像C/C++那样显式调用
delete
或free
来释放对象内存。JVM负责跟踪所有对象,并在它们“不再被需要”时自动回收其占用的内存。 - 内存区域划分 (JVM Runtime Data Areas):
- 堆 (Heap): GC工作的主战场。存放所有对象实例和数组。这是GC管理的主要区域。堆是线程共享的。
- 方法区 (Method Area / Metaspace): 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在Java 8及以后,永久代(PermGen)被移除,取而代之的是元空间(Metaspace),它主要使用本地内存(Native Memory)来存储这些数据,由JVM自行管理其内存回收(主要回收不再使用的类加载器和类信息)。
- 虚拟机栈 (VM Stack): 线程私有。存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法调用创建一个栈帧。栈帧随方法结束而销毁,其中基本类型变量和对象引用所占内存自动释放,不涉及GC。栈上分配的对象(逃逸分析优化)理论上也不涉及GC,但主流HotSpot JVM主要采用标量替换优化,对象本身并不在栈上连续分配。
- 本地方法栈 (Native Method Stack): 为Native方法服务,类似虚拟机栈。
- 程序计数器 (Program Counter Register): 线程私有。指示当前线程执行的字节码指令地址。不涉及GC。
- 直接内存 (Direct Memory): 不是JVM运行时数据区的一部分,但可以通过
ByteBuffer.allocateDirect()
申请。这部分内存由操作系统管理,但Java的GC机制可以间接管理其关联的DirectByteBuffer
对象。当DirectByteBuffer
对象被回收时,其关联的清理器(Cleaner
)会被触发(通过ReferenceQueue
),尝试释放对应的直接内存。但这依赖于GC和Cleaner
线程的执行。
二、 关键概念:对象存活性判断 - “垃圾”的定义
GC要回收的是“垃圾”,即不再被任何地方引用的对象。判断对象是否存活的核心算法:
-
引用计数法 (Reference Counting):
- 原理: 每个对象维护一个计数器,记录有多少引用指向它。当引用被创建(如赋值)时计数器+1;当引用失效(如变量离开作用域、被置为null)时计数器-1。计数器为0的对象即视为“垃圾”。
- 优点: 实现简单,判定效率高。
- 缺点: 无法解决循环引用问题 (A引用B,B引用A,但A和B都不再被外部引用,它们的计数器都不为0,却永远无法被回收)。因此,主流的Java虚拟机都不采用引用计数法作为主要的垃圾判定算法。
-
可达性分析算法 (Reachability Analysis):
- 原理: 定义一系列称为**“GC Roots”** 的对象作为起始点集。从这些根节点开始,根据引用关系向下搜索(图遍历)。搜索走过的路径称为**“引用链”**。如果一个对象到GC Roots没有任何引用链相连(即从GC Roots开始不可达),则证明此对象不再被使用,可以被回收。
- GC Roots 通常包括:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象。
- 本地方法栈中JNI(即Native方法)引用的对象。
- 方法区中类静态属性引用的对象(static变量)。
- 方法区中常量引用的对象(如字符串常量池里的引用)。
- Java虚拟机内部的引用(如基本数据类型对应的Class对象,常驻的异常对象
NullPointerException
、OutOfMemoryError
等,系统类加载器)。 - 所有被同步锁(
synchronized
关键字)持有的对象。 - 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
- 优点: 解决了循环引用问题。
- 缺点: 分析过程相对耗时(需要暂停用户线程 - Stop-The-World, STW)。
三、 引用类型与回收强度
Java将引用分为四类,影响GC行为:
- 强引用 (Strong Reference): 最常见的引用类型(
Object obj = new Object();
)。只要强引用存在,对象就永远不会被回收。 - 软引用 (Soft Reference): (
java.lang.ref.SoftReference
) 在内存不足即将发生OOM之前,这些对象会被回收。 常用于实现内存敏感的缓存(如图片缓存)。回收发生在OutOfMemoryError
抛出之前。 - 弱引用 (Weak Reference): (
java.lang.ref.WeakReference
) 下一次GC发生时,无论当前内存是否充足,都会被回收。 常用于实现规范化映射(如WeakHashMap
)或监听器列表,防止因监听器未注销导致的内存泄漏。 - 虚引用 (Phantom Reference): (
java.lang.ref.PhantomReference
) 最弱的引用。无法通过虚引用获取对象实例。对象被回收时,虚引用会被放入关联的ReferenceQueue
。 主要用于在对象被回收时收到一个系统通知(通过检查ReferenceQueue
),用于执行一些资源清理工作,例如DirectByteBuffer
的清理器。
四、 垃圾收集算法 - 方法论
-
标记-清除算法 (Mark-Sweep):
- 过程:
- 标记 (Mark): 从GC Roots开始,遍历所有可达对象,进行标记(通常是在对象头中置位)。
- 清除 (Sweep): 遍历整个堆,回收所有未被标记的对象所占用的空间。
- 优点: 简单直接。
- 缺点:
- 效率问题: 标记和清除两个过程的效率都不高(遍历整个堆)。
- 空间问题: 回收后会产生大量不连续的内存碎片。碎片过多可能导致后续分配较大对象时找不到足够的连续内存,从而触发另一次GC。
- 过程:
-
复制算法 (Copying):
- 过程: 将可用内存按容量划分为大小相等的两块(A区和B区)。每次只使用其中一块(如A区)。当A区满了,触发GC:将A区中所有存活的对象复制到B区,然后一次性清理掉整个A区。下次使用B区,如此往复。
- 优点:
- 效率高: 只需移动存活对象(通常存活对象少),按顺序分配内存,没有碎片。
- 实现简单: 清除就是清空整个半区。
- 缺点:
- 内存利用率低: 总有一半内存空闲,浪费空间。
- 存活对象多时效率下降: 复制大量对象开销大。
- 应用: 非常适合对象朝生夕死(存活率低)的新生代。HotSpot JVM的新生代(Eden + S0/S1)就是基于复制算法优化(Appel式回收)。
-
标记-整理算法 (Mark-Compact):
- 过程:
- 标记 (Mark): 同标记-清除。
- 整理 (Compact): 将所有存活的对象向内存空间的一端移动(滑动),然后直接清理掉边界以外的所有内存。
- 优点:
- 没有内存碎片: 移动后空间连续。
- 内存利用率高: 不像复制算法浪费一半空间。
- 缺点:
- 效率相对较低: 移动存活对象需要更新引用地址(需要STW),且移动本身耗时。
- 应用: 适合存活对象较多的老年代(Tenured/Old Generation)。
- 过程:
-
分代收集算法 (Generational Collection): 现代商用JVM的主流算法!
- 核心思想: 基于对象生命周期的不同,将堆内存划分为不同的代(Generation),对不同代采用最适合的收集算法。
- 堆内存划分:
- 新生代 (Young Generation): 存放新创建的对象。特点:绝大多数对象在这里快速死去(生命周期短)。划分为:
- Eden区: 新对象诞生地。
- Survivor区 (S0/S1, From/To): 两个大小相等的区域,用于保存经过一次Minor GC后存活的对象。
- 老年代 (Old/Tenured Generation): 存放在新生代中经历多次GC后仍然存活的对象(生命周期长)。也存放大对象(超过
-XX:PretenureSizeThreshold
设置值,直接进入老年代)。 - 永久代/元空间 (PermGen/Metaspace): (如前所述,Java 8+为Metaspace) 存放类元数据、常量池等。其GC行为独立于堆。
- 新生代 (Young Generation): 存放新创建的对象。特点:绝大多数对象在这里快速死去(生命周期短)。划分为:
- GC类型:
- Minor GC / Young GC: 只回收新生代的垃圾。触发频繁,速度快(因为新生代小且对象存活率低,通常采用优化的复制算法)。
- Major GC / Full GC: 回收整个堆(包括新生代、老年代)以及(通常)方法区(Metaspace)的垃圾。触发频率低,速度慢(因为老年代对象存活率高,可能采用标记-清除或标记-整理算法),STW时间长,对应用性能影响大。应尽量避免频繁Full GC。
- 对象分配与晋升:
- 新对象优先在Eden区分配。
- 当Eden区满,触发Minor GC:
- 将Eden区和当前使用的Survivor区(如S0)中存活的对象,复制到另一个空闲的Survivor区(如S1)。
- 同时,给每个存活对象年龄+1(记录在对象头中)。
- 清空Eden区和刚使用过的Survivor区(S0)。
- S0和S1角色互换(原来的To变成新的From)。
- 当一个对象在Survivor区中“熬过”一定次数的Minor GC(年龄达到阈值
-XX:MaxTenuringThreshold
,默认15),它会被晋升 (Promotion) 到老年代。 - 如果Survivor区空间不足(无法容纳Eden和另一个Survivor的存活对象),或者存活对象过大,会直接进入老年代(提前晋升或分配担保失败)。
- 当老年代空间不足时,会尝试触发Major GC/Full GC。
五、 垃圾收集器 - 算法实现者
JVM提供了多种垃圾收集器,适用于不同场景(吞吐量优先、低延迟优先、大堆内存等)。不同收集器可能用于不同分代。
-
Serial 收集器:
- 单线程工作。
- 进行GC时,必须暂停所有用户线程(STW)。
- 简单高效,没有线程交互开销。
- 应用场景: 客户端模式或资源受限的嵌入式系统。
-XX:+UseSerialGC
(新生代Serial + 老年代Serial Old)。
-
ParNew 收集器:
- Serial收集器的多线程并行版本(仅作用于新生代)。
- 多线程并行进行标记和复制。
- GC时仍需STW。
- 应用场景: Server模式下与CMS收集器配合使用的主流新生代收集器。
-XX:+UseParNewGC
(需搭配老年代CMS)。
-
Parallel Scavenge / Parallel Old 收集器:
- 吞吐量优先收集器。
- Parallel Scavenge: 新生代收集器,多线程并行复制算法。
- Parallel Old: 老年代收集器,多线程并行标记-整理算法。
- 关注点:可控制的吞吐量 (用户代码运行时间 / (用户代码运行时间 + GC时间))。可通过
-XX:MaxGCPauseMillis
(最大GC停顿时间目标)和-XX:GCTimeRatio
(吞吐量目标)参数调节。 - 应用场景: 后台运算、批处理任务等对吞吐量敏感的应用。
-XX:+UseParallelGC
/-XX:+UseParallelOldGC
。
-
CMS 收集器 (Concurrent Mark-Sweep):
- 低延迟优先收集器,目标是减少STW时间(尤其是老年代回收的停顿)。
- 老年代收集器,基于标记-清除算法。
- 过程 (四个主要阶段):
- 初始标记 (Initial Mark): (STW) 标记GC Roots直接关联的老年代对象。速度很快。
- 并发标记 (Concurrent Mark): GC线程与用户线程并发执行,遍历老年代对象图进行可达性分析。
- 重新标记 (Remark): (STW) 修正并发标记期间因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录。比初始标记时间长,但远短于并发标记。
- 并发清除 (Concurrent Sweep): GC线程与用户线程并发执行,清除不可达对象。
- 优点: 并发阶段(标记和清除)大大减少了STW时间。
- 缺点:
- CPU资源敏感: 并发阶段占用线程,会与应用线程争抢CPU。
- 浮动垃圾 (Floating Garbage): 并发清理阶段用户线程还在运行,会产生新的垃圾,只能留到下一次GC清理。
- 内存碎片: 标记-清除算法导致。可能触发Full GC(Serial Old)进行碎片整理。
- Concurrent Mode Failure: 如果在并发清理完成前老年代空间被填满(通常是晋升太快或浮动垃圾过多),会触发Serial Old进行Full GC(导致长时间STW)。需预留足够空间(
-XX:CMSInitiatingOccupancyFraction
)。
- 应用场景: 对响应时间敏感的B/S系统、Web服务等。
-XX:+UseConcMarkSweepGC
(新生代通常配合ParNew)。
-
G1 收集器 (Garbage-First):
- JDK 9+ 的默认收集器。目标是在可控的停顿时间内获得尽可能高的吞吐量,并支持超大堆(数十GB甚至更大)。
- 核心思想: 将堆划分为多个大小相等的独立区域(Region)。G1跟踪各个Region里垃圾堆积的“价值”(回收所得空间大小以及回收所需时间),在后台维护一个优先列表。每次根据用户设定的允许停顿时间(
-XX:MaxGCPauseMillis
,默认200ms),优先回收价值最大的Region(Garbage-First)。 - 特点:
- Region分区: 物理上不再连续分代,但逻辑上仍保留新生代、老年代概念(由一组Region组成)。有特殊的Humongous Region用于存放大对象。
- Mixed GC: G1除了常规的Young GC(只收集Eden/Survivor Region),还有一种Mixed GC模式。Mixed GC不仅收集新生代Region,也会根据预测模型选择部分价值高的老年代Region进行收集。这是G1实现老年代回收的主要方式。
- 算法: 整体基于标记-整理算法,局部(Region之间)基于复制算法。避免了CMS的内存碎片问题。
- 可预测停顿模型: G1能建立停顿预测模型,有计划地选择部分Region进行回收,确保在指定的停顿时间内完成垃圾收集。
- Remembered Sets (RSet): 每个Region都有一个RSet,记录其他Region中指向本Region内对象的引用。避免全堆扫描,使Region的回收相对独立。
- Collection Sets (CSets): 每次GC时被选中回收的Region集合。
- 过程 (简化):
- 初始标记 (Initial Mark): (STW) 标记GC Roots直接关联的对象,并修改TAMS指针(为并发标记做准备)。通常与一次Young GC一起完成。
- 并发标记 (Concurrent Mark): 并发遍历堆,进行可达性分析。
- 最终标记 (Final Marking): (STW) 处理SATB(Snapshot-At-The-Beginning)记录,修正并发标记期间的变化。
- 筛选回收 (Evacuation): (STW) 根据停顿预测模型,选择价值高的Region组成CSet,将CSet中存活的对象复制到空闲Region(复制算法),同时清空原Region(标记-整理效果)。这个阶段是多线程并行进行的。
- 优点: 兼具高吞吐和低延迟潜力、无碎片、可管理超大堆。
- 应用场景: 需要低延迟、大内存的现代应用。
-XX:+UseG1GC
。
-
ZGC (Z Garbage Collector) 和 Shenandoah:
- 目标: 将STW时间控制在10ms以内,且与堆大小无关(亚毫秒级到10ms级),适用于超大堆(TB级)。
- 核心技术:
- 着色指针 (Colored Pointers): (ZGC) 在指针中嵌入元数据(标记位、重映射位等),避免传统GC需要对象头存储标记信息的开销。
- 读屏障 (Read Barrier): 在应用程序线程读取对象引用时,插入一小段代码(屏障),配合着色指针实现并发转移(对象移动时,应用线程通过屏障能感知到并访问到对象的新地址)。
- 并发转移 (Concurrent Relocation/Marking): GC线程与用户线程并发地完成对象的标记和转移(压缩)。
- 特点: 几乎全程并发,STW时间极短且固定(ZGC的STW主要发生在根扫描阶段,与GC Roots数量有关,与堆大小无关)。
- 应用场景: 对延迟极其敏感的超大规模应用(金融交易、实时分析等)。
-XX:+UseZGC
(JDK 15+ Production Ready) /-XX:+UseShenandoahGC
(需要额外支持)。
六、 GC调优要点与常见问题
- 理解目标: 明确调优目标(吞吐量?低延迟?最小化内存占用?)。
- 选择合适的收集器:
- 小应用/Client:Serial
- 吞吐量优先:Parallel Scavenge / Parallel Old
- 低延迟/响应优先:CMS (JDK 8及之前), G1 (JDK 9+ 默认且推荐)
- 极致低延迟/超大堆:ZGC / Shenandoah (JDK 11+)
- 调整堆大小:
-Xms
/-Xmx
:设置堆的初始大小和最大大小。通常设置成一样大,避免堆自动扩展收缩带来的开销。- 过小:频繁GC,影响吞吐量。
- 过大:单次GC停顿时间长,增加OS管理开销。
- 调整新生代/老年代比例:
-XX:NewRatio
(老年代/新生代比例,默认2,即老年代是新生代的2倍)-XX:SurvivorRatio
(Eden/Survivor比例,默认8,即 Eden:Survivor=8:1)- 根据对象生命周期特点调整。短命对象多,增大新生代比例。
- 避免过早晋升:
- 增大
-XX:MaxTenuringThreshold
。 - 增大Survivor区大小(通过
-XX:SurvivorRatio
或直接设置-XX:SurvivorSize
)。 - 确保Survivor区能容纳每次Minor GC后的存活对象。
- 增大
- 处理大对象:
- 避免创建过大的对象数组。
- 调整
-XX:PretenureSizeThreshold
,让大对象直接进入老年代(减少在新生代的复制开销)。
- 监控与分析:
- JVM参数:
-XX:+PrintGCDetails
,-XX:+PrintGCDateStamps
,-Xloggc:<file>
记录GC日志。 - 工具:
jstat
:命令行查看JVM统计信息(GC次数、时间、各代容量使用率等)。- VisualVM:图形化监控(堆、线程、GC等)。
- Java Mission Control (JMC):更强大的监控诊断工具(Flight Recorder)。
- GC日志分析工具: GCViewer, GCEasy, HPjmeter 等,可视化分析GC日志。
- JVM参数:
- 识别与解决内存泄漏:
- 现象: Full GC越来越频繁,每次回收后老年代可用内存越来越少,最终OOM。
- 原因: 对象已不再使用,但由于意外的强引用(如静态集合类长期持有、监听器未注销、未关闭的资源如Connection/Statement/Stream)导致无法被回收。
- 诊断工具:
jmap -histo:live <pid>
:查看堆直方图(强制触发Full GC)。jmap -dump:live,format=b,file=heapdump.hprof <pid>
:生成堆转储文件。- MAT (Memory Analyzer Tool), VisualVM Heap Dump Analyzer: 分析堆转储文件,查找支配树、可疑引用链、大对象等。
七、 总结
Java的垃圾回收机制是其自动内存管理的核心,通过可达性分析判定对象生死,并主要采用分代收集思想结合多种算法(复制、标记-清除、标记-整理)来高效回收内存。不同的垃圾收集器(Serial, Parallel, CMS, G1, ZGC, Shenandoah)针对不同的性能目标(吞吐量、延迟、堆大小)进行了优化。理解GC的工作原理、内存模型、不同收集器的特点以及调优方法,对于开发高性能、高可靠的Java应用至关重要。持续监控GC行为,结合日志分析和堆转储工具定位问题,是优化应用内存使用和性能的有效手段。随着硬件发展(大内存、多核)和应用需求(低延迟)的演进,GC技术(如ZGC, Shenandoah)也在不断创新,追求更短的停顿时间和更大的堆管理能力。