深入理解Java虚拟机-垃圾收集器与内存分配策略

对象存活判定

引用计数法(Reference Counting)

  • 每个对象维护一个引用计数器,记录有多少引用指向该对象。
  • 当引用计数器为0时,表示对象不再被使用,可以被回收。
  • 无法解决循环引用问题(如上述示例),需要额外的空间存储引用计数

可达性分析算法(Reachability Analysis)

  • 通过一系列称为GC Roots的根对象作为起点,从这些根对象开始向下搜索,形成引用链
  • 如果一个对象不在任何引用链上(即不可达),则判定为可回收对象
  • GC Roots的类型:
    • 虚拟机栈中的局部变量:当前正在执行的方法中的局部变量引用的对象
    • 方法区中的静态变量:类的静态字段引用的对象
    • 方法区中的常量:字符串常量池中的引用
    • 本地方法栈中的JNI引用:Native方法引用的对象
    • Java虚拟机内部的引用:如基本数据类型对应的Class对象、系统类加载器
    • 同步锁持有的对象:被synchronized锁持有的对象
  • 解决了循环引用问题,适用于复杂的对象引用关系,但需要遍历所有对象,性能开销较大

引用类型与对象回收

  • 强引用(Strong Reference):最常见的引用类型,只要强引用存在,对象就不会被回收
    • Object obj = new Object();
  • 软引用(Soft Reference):在内存不足时,软引用对象会被回收(适合缓存场景)
    • SoftReference<Object> softRef = new SoftReference<>(new Object());
  • 弱引用(Weak Reference):无论内存是否充足,弱引用对象都会被回收(适合临时缓存)
    • WeakReference<Object> weakRef = new WeakReference<>(new Object());
  • 虚引用(Phantom Reference):虚引用对象无法通过get()获取,主要用于跟踪对象被回收的状态
    • PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>());

对象存活的特殊情况

  • finalize()方法:对象在被回收前,可以重写finalize()方法进行资源释放
  • finalize()执行时机不确定,可能导致对象复活(重新被引用),性能开销大,不建议使用

垃圾收集算法

标记-清除算法(Mark-Sweep)

  • 标记阶段:从GC Roots开始遍历,标记所有可达对象
  • 清除阶段:遍历整个堆,回收未被标记的对象
  • 优点:实现简单,适用于老年代(对象存活率高)
  • 缺点:内存碎片化严重,可能导致大对象无法分配
  • 适用场景:CMS(Concurrent Mark-Sweep)收集器的老年代回收

复制算法(Copying)

  • 将堆内存(Survivor)分为两块(From空间和To空间)
  • 复制阶段:将From空间中的存活对象复制到To空间
  • 清理阶段:清空From空间,交换From和To空间的角色
  • 优点:无内存碎片化问题,效率高(只需复制存活对象)
  • 缺点:内存利用率低,不适合对象存活率高的场景(如老年代)
  • 适用场景:新生代的垃圾收集(如Serial、ParNew收集器)

标记-整理算法(Mark-Compact)

  • 标记阶段:与标记-清除算法相同,标记所有可达对象
  • 整理阶段:将存活对象向一端移动,清理边界外的内存
  • 优点:无内存碎片化问题,适合对象存活率高的场景(如老年代)
  • 缺点:效率较低(需要移动对象)
  • 适用场景:Serial Old、Parallel Old收集器的老年代回收

分代收集算法(Generational Collection)

  • 根据对象生命周期将堆内存划分为新生代老年代
  • 新生代:对象生命周期短,采用复制算法
  • 老年代:对象生命周期长,采用标记-清除或标记-整理算法
  • Eden区:新对象首先分配在Eden区
  • Survivor区:分为From和To空间,用于存放经过一次GC后存活的对象
  • 晋升机制:对象在Survivor区经历多次GC后,晋升到老年代
  • 优点:针对不同代采用不同算法,效率高,减少Full GC的频率
  • 适用场景:现代JVM的主流垃圾收集器(如G1、CMS、ZGC)

增量收集算法(Incremental Collecting)

  • 将GC过程分为多个小步骤,与用户线程交替执行

分区收集算法(Region-Based Collecting)

  • 将堆内存划分为多个独立区域(Region),每个区域独立回收
  • 适用场景:G1收集器
算法 优点 缺点 适用场景
标记-清除 实现简单 内存碎片化,效率低 老年代(CMS)
复制 无碎片化,效率高 内存利用率低 新生代(Serial、ParNew)
标记-整理 无碎片化,适合高存活率 效率较低 老年代(Serial Old)
分代收集 针对不同代优化,效率高 实现复杂 现代JVM(G1、CMS)

经典垃圾收集器

垃圾收集器的核心目标

  • 低延迟(Low Pause Time):减少用户线程的停顿时间(STW)
  • 高吞吐量(High Throughput):最大化应用运行时间占比
  • 内存效率(Memory Efficiency):减少内存碎片,提升内存利用率

经典垃圾收集器分类

收集器 工作模式 适用区域 算法 特点
Serial 串行 新生代/老年代 复制/标记-整理 单线程、简单高效、STW长
ParNew 并行 新生代 复制 多线程版Serial,配合CMS使用
Parallel Scavenge 并行 新生代 复制 吞吐量优先
Serial Old 串行 老年代 标记-整理 Serial的老年代搭档
Parallel Old 并行 老年代 标记-整理 Parallel Scavenge的老年代搭档
CMS 并发 老年代 标记-清除 低延迟、内存碎片化
G1 并发+分区 全堆 标记-整理+复制 平衡吞吐量与延迟
ZGC 并发 全堆 染色指针+读屏障 超低延迟(<10ms)
Shenandoah 并发 全堆 Brooks指针+读屏障 低延迟、与堆大小无关

STW(Stop-The-World)

  • 暂停所有线程:在 STW 期间,除了垃圾回收线程,所有应用线程都会被暂停
  • 全局一致性:确保垃圾回收器在回收内存时,应用线程不会修改对象引用或创建新对象
  • 持续时间:STW 的时间长短取决于垃圾回收器的类型和堆内存的大小
  • 触发场景:Yong GC有概率触发STW,Full GC一定会触发STW

Serial收集器

  • 新生代:采用复制算法,单线程处理垃圾回收,触发Minor GC时暂停所有用户线程(STW)
  • 老年代:Serial Old收集器使用标记-整理算法
  • 适用场景:单核CPU环境(如客户端应用、嵌入式系统),内存较小(几百MB)且对停顿时间不敏感的场景
  • 参数配置:-XX:+UseSerialGC # 显式启用Serial收集器

ParNew收集器

  • 新生代并行版Serial,多线程执行复制算法,需配合CMS使用
  • 默认线程数与CPU核数相同,可通过-XX:ParallelGCThreads调整
  • 适用场景:多核CPU且需低延迟的老年代回收(与CMS搭配)
  • 参数配置:-XX:+UseParNewGC # 启用ParNew收集器(需与CMS配合)

Parallel Scavenge/Old收集器

  • 新生代(Parallel Scavenge):多线程复制算法,吞吐量优先(-XX:GCTimeRatio控制GC时间占比)

  • 老年代(Parallel Old):多线程标记-整理算法。

  • 适用场景:后台计算任务(如批处理),允许较长STW以换取高吞吐量

  • 参数配置

    bash 复制代码
    -XX:+UseParallelGC      # 启用Parallel Scavenge
    -XX:+UseParallelOldGC   # 启用Parallel Old
    -XX:MaxGCPauseMillis=200  # 目标最大停顿时间(不保证)

CMS(Concurrent Mark-Sweep)收集器

  • 初始标记(Initial Mark):标记GC Roots直接关联对象(STW)

  • 并发标记(Concurrent Mark):遍历对象图,标记可达对象(与用户线程并发)

  • 重新标记(Remark):修正并发标记期间的变化(STW)

  • 并发清除(Concurrent Sweep):回收垃圾对象(与用户线程并发)

  • 内存碎片问题:标记-清除算法可能导致碎片,需定期Full GC(或开启-XX:+UseCMSCompactAtFullCollection压缩内存)

  • 适用场景:对延迟敏感的应用(如Web服务),老年代回收时允许部分并发

  • 参数配置

    bash 复制代码
    -XX:+UseConcMarkSweepGC       # 启用CMS
    -XX:CMSInitiatingOccupancyFraction=70  # 老年代使用率70%时触发CMS
    -XX:+CMSParallelRemarkEnabled # 并行重新标记以缩短STW

G1(Garbage-First)收集器

  • 内存分区:将堆划分为多个等大的Region(1MB~32MB),每个Region可属于Eden、Survivor、Old或Humongous(大对象区)

  • Young GC:回收Eden和Survivor区

  • Mixed GC:回收部分Old Region(基于回收价值预测)

  • SATB(Snapshot-At-The-Beginning):通过快照保证并发标记的正确性

  • 适用场景:大内存(6GB以上)、多核CPU,需平衡吞吐量与延迟(如大数据平台)

  • 参数配置

    bash 复制代码
    -XX:+UseG1GC                 # 启用G1
    -XX:MaxGCPauseMillis=200     # 目标最大停顿时间
    -XX:G1HeapRegionSize=16m     # 设置Region大小

ZGC(Z Garbage Collector)

  • 染色指针(Colored Pointers):在指针中嵌入元数据(如标记、重映射状态),减少内存开销

  • 读屏障(Load Barrier):在访问对象时触发屏障,处理并发标记和转移

  • 并发转移:将存活对象移动到新Region,无需长时间STW

  • 适用场景:超大堆(TB级)、要求亚毫秒级停顿(如实时交易系统)

  • 参数配置

    bash 复制代码
    -XX:+UseZGC                  # 启用ZGC(JDK 15+默认支持)
    -XX:+ZGenerational           # 启用分代ZGC(JDK 21+)

Shenandoah收集器

  • Brooks指针:每个对象额外存储一个转发指针(Forwarding Pointer),用于并发转移

  • 并发回收:标记、转移、重映射阶段均与用户线程并发

  • 适用场景:与ZGC类似,但实现更简单,适用于OpenJDK社区版本

  • 参数配置

    bash 复制代码
    -XX:+UseShenandoahGC         # 启用Shenandoah
    -XX:ShenandoahGCMode=iu      # 设置并发模式(iu:增量更新)

ZGC 减少 STW 停顿的关键技术

  1. 并发标记与清理:ZGC 的标记和清理过程大部分是并发执行的,不会阻塞应用程序的执行。
  2. 染色指针:使得垃圾回收线程与应用程序线程可以在不进行同步的情况下访问对象,避免了锁的争用。
  3. 区域化堆:堆被分为多个区域,能够在多个区域上并行回收,避免了全局回收带来的长时间停顿。
  4. 分阶段回收:将 GC 过程拆分为多个小阶段,并将大部分阶段并发执行,只有少数几个阶段会触发短暂的 STW。
  5. 延迟回收:将某些清理工作推迟到后台执行,进一步降低了应用程序停顿的时间。

垃圾收集器的选择策略

应用场景 推荐收集器 核心优势
单核/小内存客户端应用 Serial 简单、无多线程开销
高吞吐量后台计算 Parallel Scavenge 最大化CPU利用率
Web服务(低延迟老年代回收) CMS 并发标记清除,减少STW
大内存多核应用(平衡型) G1 分区回收,可控停顿时间
超大内存、超低延迟 ZGC/Shenandoah 亚毫秒级停顿,TB级堆支持

调优实战建议

  • 使用jstat -gcutil <pid>观察各代内存使用和GC时间
  • 通过-Xlog:gc*输出详细GC日志,用工具(如GCViewer)分析
  • 新生代大小:避免过小导致频繁Minor GC,或过大延长单次GC时间
  • 晋升阈值:调整-XX:MaxTenuringThreshold控制对象晋升老年代的速度
  • 使用jmap -histo:live <pid>查看对象分布,定位未释放的大对象

内存分配策略

内存分配的核心策略

  • JVM的内存分配策略基于分代收集理论,将堆内存划分为新生代和老年代,不同区域采用不同的分配规则
  • 对象优先在Eden区分配:大多数新对象在新生代的Eden区分配,Eden区内存不足时,触发Minor GC,回收新生代垃圾
  • 大对象直接在老年代分配:大对象(如长字符串、大数组)直接在老年代分配,避免在Eden区和Survivor区之间反复复制
  • 长期存活的对象进入老年代
    • 对象在新生代经历多次GC后仍存活,会被晋升到老年代
    • 年龄计数器:对象每存活一次Minor GC,年龄加1
    • 年龄阈值:通过-XX:MaxTenuringThreshold设置晋升阈值(默认值15)
  • 动态对象年龄判断:若Survivor区中相同年龄对象的总大小超过Survivor区容量的一半,则年龄≥该值的对象直接晋升老年代
  • 空间分配担保
    • 在Minor GC前,JVM检查老年代剩余空间是否大于新生代对象总大小
    • 若不足,则检查是否允许担保失败(-XX:-HandlePromotionFailure),若允许则继续Minor GC,否则触发Full GC
  • 调优参数
    • -XX:NewRatio=2:老年代与新生代的内存比例为2:1。
    • -XX:SurvivorRatio=8:Eden区与Survivor区的内存比例为8:1:1(Eden:From:To )
    • -XX:PretenureSizeThreshold=4M :对象超过4MB直接进入老年代

内存分配策略的调优实战

问题 原因 解决方案
频繁Full GC 老年代空间不足,对象晋升过快 增大老年代(-Xmx),调整晋升阈值
Eden区GC频繁 短生命周期对象过多,Eden区过小 增大新生代(-XX:NewRatio
大对象分配失败 老年代碎片化,无法容纳大对象 使用G1收集器整理内存,或增大堆空间
直接内存溢出 NIO操作未释放堆外内存 显式调用System.gc()或限制直接内存大小
bash 复制代码
# 堆内存与新生代配置
-Xms4g -Xmx4g                # 初始堆和最大堆设为4GB
-XX:NewRatio=3               # 老年代:新生代=3:1(新生代1GB)
-XX:SurvivorRatio=8          # Eden:Survivor=8:1:1(Eden=800MB)

# 晋升与分配优化
-XX:MaxTenuringThreshold=10  # 对象年龄阈值设为10
-XX:PretenureSizeThreshold=4M # 大对象阈值4MB
-XX:+UseTLAB                 # 启用TLAB(默认开启)
相关推荐
快手技术几秒前
快手Klear-Reasoner登顶8B模型榜首,GPPO算法双效强化稳定性与探索能力!
后端
二闹10 分钟前
三个注解,到底该用哪一个?别再傻傻分不清了!
后端
用户490558160812522 分钟前
当控制面更新一条 ACL 规则时,如何更新给数据面
后端
林太白23 分钟前
Nuxt.js搭建一个官网如何简单
前端·javascript·后端
码事漫谈25 分钟前
VS Code 终端完全指南
后端
该用户已不存在1 小时前
OpenJDK、Temurin、GraalVM...到底该装哪个?
java·后端
怀刃1 小时前
内存监控对应解决方案
后端
码事漫谈1 小时前
VS Code Copilot 内联聊天与提示词技巧指南
后端
Moonbit2 小时前
MoonBit Perals Vol.06: MoonBit 与 LLVM 共舞 (上):编译前端实现
后端·算法·编程语言
Moonbit2 小时前
MoonBit Perals Vol.06: MoonBit 与 LLVM 共舞(下):llvm IR 代码生成
后端·程序员·代码规范