一、垃圾收集器
常用垃圾收集器图示:

图上半部分表示 Young generation 垃圾收集器,下半部分表示 Tenured generation 垃圾收集器。连线表示可以同时使用
1. Serial 收集器
- 特点:
- 新生代收集器
- 单线程
- 它进行垃圾收集时,必须暂停其他所有工作线程(Stop The World)
- 对于单 CPU 来说和其它收集器来比 --> 简单而高效
- 在 Client 模式下的虚拟机来说是一个很好的选择
- 新生代使用复制算法
- Serial/Serial Old 收集器运行示意图
2. ParNew 收集器
- 特点:
- 新生代收集器
- ParNew 收集器其实就是 Serial 收集器的多线程版本
- 运行在 Server 模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重新的原因是,除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。
- 新生代采用复制算法
- ParNew/Serial Old 收集器运行示意图
3. Parallel Scavenge 收集器(吞吐量优先收集器)
- 特点:
- 新生代收集器
- 采用复制算法
- 并行的多线程收集器
- 目标是达到一个可控制的吞吐量(Throughput)。
所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。
- 重要参数:
- -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,允许的值为一个大于 0 的毫秒数,收集器尽可能地保证内存回收花费的时间不超过设定值。但是停顿时间小,意味着停顿次为的增加,吞吐量也会下降。
- -XX:GCTimeRatio:控制吞吐量大小,允许的值为一个大于 0 且小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为 19,那允许的最大 GC 时间就占总时间的 5% (即 1/(1+19)),默认值为 99,就是允许最大 1%(即 1/(1+99))的垃圾收集时间。
- -XX:+UseAdaptiveSizePolicy,这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmm)、Eden 与 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿暗或者最大的吞吐量,这种调节方式称为 GC 自适应的调节策略(GC Ergonomics)
- Parallel Scavenge/Parallel Old 收集器运行示意图
4. Serial Old 收集器
- 特点:
- Serial Old 是 Serial 收集器的老年代版本
- 单线程收集器
- 使用“标记——整理”算法。
- 主要用于 Client 模式下的虚拟机使用。
- 在 Server 模式下,主要有两大用途:
- 一种是在 JDK 1.5以及之前的版本中与 Parallel Scavenge 收集器搭配使用
- 另一种用途就是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
- Serial Old 收集器的工作过程运行示意图
5. Parallel Old 收集器
- 特点:
- 是 Parallel Scavenge 收集器的老年代版本
- 多线程
- 使用“标记——整理”算法
- 在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
- Parallel Old 收集器的工作过程:
6. CMS(Concurrent Mark Sweep) 收集器(最短回收停顿时间)
- 特点:
- 最短回收停顿时间为目标
- 基于“标记——清除”算法
- 多线程
- 整体过程分为 4 个步骤:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent swap)
其中,初始标记、重新标记仍然需要“Stop The World”。初始标记仅仅只是标记一下 GC Roots 能直接关联的对象,速度很快,并发标记阶段就是进行 GC Roots Tracing 的过程,而重新标记阶段则是为修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的稍长一些,但远比并发标记的时间短。
- CMS 收集器运行示意图:
- 优点:
- 并发收集
- 低停顿
- 缺点:
- CMS 收集器对 CPU 资源非常敏感
- CMS 收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次 Full GC 的产生。
- 参数:-XX:CMSInitiatingOccupancyFraction,该值来控制当老年代使用了百分之多少的空间时触发一次,以便降低内存回收次数从而获取理好的性能,在 JDK 1.6 中,CMS 收集器的启动阈值已经提升至 92% 。要是 CMS 运行期间预留的内存无法满足程序需要,就会出现一次 “Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数 -XX:CMSInitiatingOccupancyFraction 设置得太高很容易导致大量“Concurrent Mode Failure” 失败,性能反而降低。
- 由于基于“标记——清除”算法实现的收集器,就可能意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次 Full GC。为了解决这个问题,虚拟机提供了以下参数:
- -XX:UseCMSCompactAtFullCollection 开关参数(默认就是开启的),用于 CMS 收集器顶不住要进行FullGC 时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
- -XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的 Full GC 后,跟着来一次带压缩的(默认值为 0,表示每次进入 Full GC 时都进行碎片整理)。
7. G1(Garbage-First)收集器
-
特点:
- 并行与并发
- G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿的时间,部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
- 分代收集
- 与其他收集器一样,分代概念依然保留。G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,它可以采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。
- 空间整合
- 与 CMS 的“标记——清理”算法不同,G1 从整体来看是基于“标记——整理”算法实现的收集器,从局部(两个Region 之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着 G1 在运行期间不会产生内存碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。
- 可预测的停顿
- 这是 G1 相对于 CMS 的另一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 m 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒,这几乎已经是实时 Java (RTSJ)的垃圾收集器的特征了。
在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老年代,而 G1 不再是这样。使用 G1 收集器时,Java 堆的内存布局就与其他收集器有很大差别,它将整个 Java 堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离了,它们都是一部分 Region(不需要连续)的集合。
- 并行与并发
-
运作大致步骤:
在 G1 收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用 Remembered Set 来避免全堆扫描的。G1 中每个 Region 都有一个与之对应的 Remembered Set,虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 之中。当进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏。如果不计算维护 Remembered Set 的操作,G1 收集器的运行大致可划分为以下几个步骤:
- 初始化标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
-
运行示意图:
8. 垃圾收集器汇总
垃圾收集器 | 区域 | 算法 | 特点 |
---|---|---|---|
Serial | Young | 复制 | 单线程,简单高效,收集时停顿大,Client模式下的默认收集器 |
ParNew | Young | 复制 | Serial的多线程版本,可以和CMS配合使用 |
ParallelScavenge | Young | 复制 | 多线程,吞吐量优先收集器。Server模式下默认收集器 |
SerialOld | Old | 标记—整理 | 单线程,Client模式下默认收集器。 |
ParallelOld | Old | 标记—整理 | 多线程,和Parallel Scavenge搭配使用。Server模式默认收集器。 |
CMS | Old | 标记—清除 | 多线程,最短回收停顿收集器,对CPU资源敏感,无法收集标记过程中产生的垃圾,会产生空间碎片。需要参数配置是否压缩 |
G1 | Young/Old | 标记—整理—复制 | 多线程,分代收集,空间整理,可预测停顿。 |
9. 垃圾收集器参数
参数 | 描述 |
---|---|
UseSerialGC | 虚拟机运行在 Client 模式下的默认值,打开此开关后,使用 Serial + Serial Old 的收集器组合进行内存回收 |
UseParNewGC | 打开此开关后,使用 ParNew + Serial Old 收集器组合进行内存回收 |
UseConcMarkSweepGC | 打开此开关后,使用 ParNew + CMS + Serial Old 收集器组合进行内存回收。 Serial Old 收集器将作为 CMS 收集器出现 Concurrent Mode Failure 失败后的后备收集器使用 |
UseParallelGC | 虚拟机运行在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old(PS MarkSwee)的收集器组合进行内存回收 |
UseParallelOldGC | 打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收 |
SurvivorRatio | 新生代中 Eden 区域与 Survivor 区域的容量比值,默认为 8,代表 Eden : Survivor=8 : 1 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象直接在老年代分配 |
MaxTenuringThreshold | 晋升到老年代的对象年龄。每个对象在坚持过一次 Minor GC 之后,年龄就增加 1,当超过这个参数值时就进入老年代 |
UseAdaptiveSizePolicy | 动态调整 Java 堆中各个区域的大小以及进入老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 区的所有对象都存活的极端情况 |
ParallelGCThreads | 设置并行 GC 时进行内存回收的线程数 |
GCTimeRatio | GC 时间占总时间的比率,默认值为 99,即允许 1% 的 GC 时间。仅在使用 Parallel Scavenge 收集器时生效 |
MaxGCPauseMillis | 设置 GC 的最大停顿时间。仅在使用 Parallel Scavenge 收集器生效 |
CMSInitiatingOccupancyFraction | 设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集。默认值为 68%,仅在使用 CMS 收集器时生效 |
UseCMSCompactAtFullCollection | 设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在使用 CMS 收集器时生效 |
CMSFullGCsBeforeCommpaction | 设置 CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理。仅在使用 CMS 收集器时生效 |
二、 内存分配与回收策略
对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过 JIT 编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲,将按线程优先在 TLAB 上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。
1. 对象优先在 Eden 分配
大多数情况下,对象在新生代的 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
虚拟机提供了 -XX:+PrintGCDetails 参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。
2. 大对象直接进入老年代
虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制(新生代采用复制算法收集内存)
3. 长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为 1。对象在 Survivor 区中每“熬过”一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 设置。
4. 动态对象年龄判定
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。
5. 空间分配担保
在发生Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。
【注】JDK 1.6 Update 24之后 HandlePromotionFailure 参数将不在起作用,规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC,否则将进行 Full GC。
6. 参数汇总
参数 | 说明 |
---|---|
-XX:+PrintGCDetails | 发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况 |
-XX:PretenureSizeThreshold | 大于这个设置值的对象直接在老年代分配 |
-XX:MaxTenuringThreshold | 对象晋升老年代的年龄阈值,默认15 |
-XX:-HandlePromotionFailure | 是否允许担保失败(JDK 1.6 Update 24之后不在起作用) |
新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕死的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
老年代 GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就直接进行 Major GC 的策略选择过程)。Major GC 的速度一般会比 Minor GC 慢 10 倍以上。