JVM 垃圾收集
# 参考
- 深入理解Java虚拟机(第2版) (opens new window)
- 这次,真正学懂 Java 垃圾回收机制 (opens new window)
- 不用找了,深入理解G1垃圾收集器和GC日志,都整理好了 (opens new window)
- 46张PPT讲述JVM、GC算法和性能调优 (opens new window)
- 垃圾优先型垃圾回收器调优 (opens new window)
- 从实际案例聊聊Java应用的GC优化 (opens new window)
# 垃圾回收区域
内存垃圾回收的目的是未必避免程序在运行过程中出现内存溢出和内存泄漏问题。
JVM内存划分中,程序计数器、本地方法栈、虚拟机栈所需要的内存大小在代码结构确定后就已知的,并且会随着线程的生命周期创建和销毁,所以这部分内存分配具有确定性,不需要过多关注。
而对于堆和方法区,需要多大内存是要在运行时才能知道的,因为方法的参数不同,调用次数不同,这些都是在运行期动态生成的,所以垃圾回收的职责就是对这部分垃圾对象进行回收,释放内存。
回收方法区:java虚拟机规范中,不要求方法区实现垃圾收集器,主要是对方法区进行垃圾收集的性价比不高,但是方法区也是可以在运行时产生新的字面量和类的。例如new String("abc")的时候,如果常量池中不存在“abc”这个对象,则会在常量池中创建。JSP的实现过程是将新的servlet类加载到jvm中。
所以方法区(HotSpot用永久代实现方法区)的垃圾回收主要包括:废弃常量和无用类。 是否对类进行回收HotSpott提供了-Xnoclassgc来控制
方法区内存的回收查看《深入理解Java虚拟机(第2版)》3.2.5章节
# 判断哪些对象可以回收
引用计数法:为每个对象创建一个引用计数器,统计所有指向该对象的引用个数,当一个对象引用个数为0时,则说明对象没有引用可以回收
缺点:引用计数法无法处理循环引用的对象
可达性分析法:通过“GC Roots”对象集往下搜索引用链,当一个引用链无法到达“GC Roots”时,则说明该引用链的所有对象可以回收(obj5、obj6、obj7无法到达GC Roots,所以可以被回收)。
引用计数法是目前主流的垃圾标记算法,GC Roots包括
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法(JNI)栈中引用的对象
# 垃圾回收算法
标记-清除算法(Mark-Sweep):最基本的垃圾收集算法,逻辑简单,缺点:效率偏低,容易产生内存碎片
复制算法(Copying):逻辑简单、效率高、没有内存碎片,但是内存使用率不高,适合存活对象少(需要复制的对象少)的情况
标记-整理算法(Mark-Compact):内存利用率高,没有内存碎片,适合存活对象多,垃圾少的情况(需要移动位置的对象少)
# 分代垃圾回收
以上垃圾回收算法各自都有优缺点,为了针对不同场景,jvm开发者对内存做了划分,针对不同区域使用不同的回收算法。
- 新生代
- 大部分对象在gc时被回收,存活对象少,使用复制算法。为了解决内存占用过高问题,将新生代划分成了Eden, Survivors(s0, s1或者from, to),内存分配比例是Eden:from:to = 8:1:1
- 老年代
- 经过多次gc后依然存活的对象进入老年代,所以gc时存活对象多,使用“标记-清除”或者“标记-整理”算法
详细查看《深入理解Java虚拟机(第2版)》3.3章节
# 垃圾对象转移和收集过程
- 新对象在Eden区创建,当Eden区快满的时候,进行Minor GC(YGC),将Eden清空,把存活对象放到From区,然后新对象继续在Eden区创建。
- 当Eden区又满的时候,进行Minor GC(YGC),清理Eden区和From区垃圾对象,把存活对象放到To区,依次类推,From区和To区交替使用。
- 当对象经过多次Minor GC(YGC)后依然存活(超过回收年龄,不同垃圾收集器的阈值不一样,可以通过参数修改),进入老年代(Old区)。
- 如果存活对象太多,From区或者To区放不下,则直接进入Old区。
- Old区快满时(当内存使用比例达到一定阈值,不同收集器阈值不一样,cms收集器默认是92%),触发Major GC(FGC),同时对新生代和老年代进行垃圾回收。
# 垃圾收集器
各垃圾收集器类型和适用分代范围
# Serial
单线程串行收集器,用于新生代垃圾收集。使用“复制算法”,多用于客户端程序。
# Serial Old
Serial的老年代版本,同样是单线程收集器,使用“标记-整理”算法。多用于客户端程序,同时作为CMS担保失败后的备预案收集器。
# ParNew
Serial的多线程版本,同时是使用“复制算法”,目的是为了充分利用cpu资源,缩短停顿时间。适用于多线程的服务器程序,配合CMS收集器一起使用
# Parallel Scavenge
与ParNew一样,是使用复制算法的并发新生代收集器。不同之处在于Parallel Scavenge是吞吐量优先的,可以设置期望停顿时间(MaxGCPauseMillis)和吞吐量(GCTimeRatio),但是无法与CMS仪器配合使用。
# Parallel Old
Parallel Scavenge的老年代版本,使用”标记-整理“算法。在JDK1.6之后提供,为了配合Parallel Scavenge实现高吞吐量的收集器组合。
# CMS
低停顿的并发收集器,部分阶段能够与用户线程同时进行。采用“标记-清除”算法,多用于web等对延迟要求较高的系统。
随着垃圾收集器的发展,在JDK1.9中CMS已经被标记为废弃
垃圾收集过程
- 初始标记(initial mark): stop the world,仅标记GC Roots能直接关联到的对象,速度很快;
- 并发标记(concurrent mark): 进行GC Roots Tracing的过程;
- 重新标记(remark): stop the world,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但比并发标记时间短;
- 并发清除(concurrent sweep): 整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优缺点
- 优点:并发收集、低停顿
- 缺点
- 对CPU资源非常敏感,会与用户线程抢占cpu资源,在cpu资源不足的情况下同样会影响程序执行
- 无法处理浮动垃圾、产生大量空间碎片
- 因为cms在垃圾收集时,用户线程同时运行,在这期间产生的垃圾超过可提供的内存空间时,cms将临时采用后预案,使用Serial Old对老年代重新进行垃圾收集。
-XX:CMSinitiatingoccupancyFraction可以设置老年代达到内存占用比例时触发cms,即预留多少空间给垃圾收集期间同时产生的对象。
JDK1.5 默认是68%,JDK1.6默认值调整到了92%
如果老年代内存增长较快时可适当调低,反之可以适当调高
# G1
同样是以低延迟为目标的并发收集器,在高并发和大内存下表现很好,在JDK1.9中成为了默认垃圾收集器。物理上已经没有严格的年代划分,将堆内存划分成多个region,针对每个region进行收集,没有垃圾碎片,可以设置期望停顿时间。如果最求高吞吐量,G1并不是一个好的选择。
优点:
- 大内存友好,相对于 cms 如果内存设置较大,在重新标记阶段可能会造成比较长时间停顿
- 没有内存碎片
- 可预测暂停时间,可以解决突发流量导致的内存突然增加情况
劣势:牺牲一定吞吐量
参考:
- 详细可查看《深入理解java虚拟机(第2版)》3.5.7章节,
- 不用找了,深入理解G1垃圾收集器和GC日志,都整理好了 (opens new window)
- Java Hotspot G1 GC的一些关键技术 (opens new window)
# 常用GC收集器组合
垃圾收集器 | 回收算法 | 参数 | 说明 |
---|---|---|---|
Serial Serial Old | 复制算法 标记-整理算法 | -XX:+UseSerialGC (Serial + Serial Old) | 单线程,不能充分利用CPU,多用于client模式 |
Parallel Scavenge Parallel Old | 复制算法 标记-整理算法 | -XX:+UseParallelOldGC (Parallel Scavenge + Parallel Old) | 多线程,并行收集,高吞吐量 |
ParNew CMS | 复制算法 标记-清除算法 | -XX:+UseConcMarkSweepGC (ParNew + CMS) | 多线程,追求极致低延迟 |
G1 | 复制算法 + 标记整理 | -XX:+UseG1GC -XX:MaxGCPauseMillis=100 (可选,期望停顿时间ms) | 多线程,低延迟,JDK1.9中默认的收集器 |
提示
如果 JDK 版本是 1.8 以上,推荐使用 G1 收集器
- G1 在大内存下表现比 CMS 好,现在硬件成本越来越低,很多 GC 问题,只要多分配一些内存就能很好解决
- G1 可以设置最长停顿时间,一次 GC 可以只回收部分内存,保证一次 GC 在预期时长之内,能更好应对对突发高并发场景