1. JVM垃圾回收机制的本质理解
第一次接触JVM垃圾回收(GC)时,很多人会被各种算法名词搞得晕头转向。实际上GC的本质非常简单:自动管理Java堆内存中不再使用的对象,释放内存空间供新对象使用。这种自动化内存管理机制,正是Java区别于C/C++等语言的重要特性。
在HotSpot虚拟机中,GC主要针对堆内存和方法区(元空间)进行回收。我经常用"房间打扫"的比喻来解释GC:Java堆就像你的卧室,新对象就像不断买进的新物品。如果不定期清理,房间很快就会被无用物品占满。GC就是那个自动帮你识别并扔掉无用物品的智能管家。
2. GC核心算法深度剖析
2.1 标记-清除算法
标记-清除(Mark-Sweep)是最基础的GC算法,分为两个阶段:
- 标记阶段:从GC Roots出发,遍历所有可达对象并标记
- 清除阶段:回收未被标记的对象占用的内存
这个算法有两个明显缺陷:
- 内存碎片化:清除后会产生不连续的内存空间
- 停顿时间较长:特别是堆内存较大时
// 示例:GC Roots包括 // 1. 虚拟机栈中引用的对象 // 2. 方法区中类静态属性引用的对象 // 3. 方法区中常量引用的对象 // 4. 本地方法栈中JNI引用的对象2.2 复制算法
复制(Copying)算法将内存分为大小相同的两块,每次只使用其中一块。当这块内存用完时,将存活对象复制到另一块,然后一次性清理已使用的内存空间。
优点:
- 解决内存碎片问题
- 分配内存时无需考虑内存碎片(只需要移动堆顶指针)
缺点:
- 内存利用率只有50%
- 对象存活率高时复制开销大
提示:新生代的Eden区和Survivor区就是基于复制算法设计的
2.3 标记-整理算法
标记-整理(Mark-Compact)是标记-清除的改进版,在标记完成后,将所有存活对象向一端移动,然后直接清理掉边界以外的内存。
优势:
- 解决了内存碎片问题
- 适合老年代这种对象存活率高的区域
劣势:
- 移动对象需要更新引用,增加了GC停顿时间
3. HotSpot虚拟机的GC实现
3.1 分代收集理论
HotSpot采用分代收集策略,将堆内存划分为:
- 新生代(Young Generation)
- Eden区
- Survivor区(From/To)
- 老年代(Old Generation)
- 永久代/元空间(JDK8+)
# 查看JVM内存分布的常用命令 jmap -heap <pid>3.2 新生代GC(Minor GC)
新生代采用复制算法,具体流程:
- 新对象首先分配在Eden区
- Eden区满时触发Minor GC
- 将Eden和From Survivor区的存活对象复制到To Survivor区
- 交换From和To Survivor区的角色
- 对象年龄增加到阈值(默认15)时晋升到老年代
3.3 老年代GC(Major GC/Full GC)
老年代通常使用标记-清除或标记-整理算法。当出现以下情况时会触发Full GC:
- 老年代空间不足
- 方法区空间不足
- System.gc()被调用(不建议主动调用)
- Minor GC后存活对象超过老年代剩余空间
4. 主流GC收集器对比
4.1 串行收集器(Serial)
单线程工作的收集器,GC时需要暂停所有工作线程(Stop The World)。
适用场景:
- 客户端模式
- 内存较小的嵌入式系统
启动参数:
-XX:+UseSerialGC4.2 并行收集器(Parallel)
多线程并行执行GC,但仍需Stop The World。
特点:
- 吞吐量优先
- JDK8默认收集器
启动参数:
-XX:+UseParallelGC -XX:+UseParallelOldGC4.3 CMS收集器
以获取最短回收停顿时间为目标的收集器,采用标记-清除算法。
工作流程:
- 初始标记(STW)
- 并发标记
- 重新标记(STW)
- 并发清除
缺点:
- 对CPU资源敏感
- 无法处理浮动垃圾
- 会产生内存碎片
启动参数:
-XX:+UseConcMarkSweepGC4.4 G1收集器
面向服务端应用的收集器,将堆划分为多个Region,优先回收价值最大的Region。
特点:
- 可预测的停顿时间模型
- 整体基于标记-整理,局部基于复制算法
- JDK9+默认收集器
启动参数:
-XX:+UseG1GC5. GC性能调优实战
5.1 关键参数配置
| 参数 | 说明 | 建议值 |
|---|---|---|
| -Xms | 初始堆大小 | 与-Xmx相同 |
| -Xmx | 最大堆大小 | 物理内存的1/4~1/2 |
| -Xmn | 新生代大小 | 整个堆的1/3~1/2 |
| -XX:SurvivorRatio | Eden/Survivor比例 | 8(即Eden:From:To=8:1:1) |
| -XX:MaxTenuringThreshold | 晋升老年代年龄阈值 | 15 |
| -XX:+PrintGCDetails | 打印GC详细信息 | 生产环境建议开启 |
5.2 常见问题排查
问题1:频繁Full GC
可能原因:
- 老年代空间不足
- 大对象直接进入老年代
- 长期存活对象过多
解决方案:
- 增加-XX:NewRatio值
- 调整-XX:PretenureSizeThreshold
- 检查内存泄漏
问题2:GC停顿时间过长
可能原因:
- 堆内存过大
- 收集器选择不当
- 对象引用树过于复杂
解决方案:
- 考虑使用G1或ZGC
- 减小堆大小
- 优化对象结构
5.3 监控工具推荐
- jstat:监控GC统计信息
jstat -gcutil <pid> 1000VisualVM:图形化监控工具
GC日志分析:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log6. 新一代GC技术展望
ZGC和Shenandoah作为新一代低延迟GC,具有以下特点:
- 停顿时间不超过10ms
- 支持TB级堆内存
- 并发执行大部分GC操作
启动参数示例:
-XX:+UseZGC -XX:+UseShenandoahGC在实际项目中,我发现很多性能问题其实源于对GC机制理解不足。比如曾经遇到一个案例:系统每隔几小时就会卡顿几十秒,最终发现是因为有人误将缓存对象设置为静态变量,导致大量本应短期存活的对象晋升到老年代。通过调整缓存策略并适当增加新生代大小,问题得到完美解决。