本文继《垃圾收集(一)》总结垃圾收集器与内存分配与回收策略

一、垃圾收集器

以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。

注:G1 垃圾收集器既可以回收年轻代内存,也可以回收老年代内存。而其他垃圾收集器只能针对特定代的内存进行回收。

1.1 串行收集器

串行收集器(Serial)是最基本、发展历史最悠久的收集器,新生代单线程收集器,标记和清理都是单线程。

串行收集器是 client 模式下的默认收集器配置。因为在客户端模式下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的年轻代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。

串行收集器采用单线程 stop-the-world 的方式进行收集。当内存不足时,串行 GC 设置停顿标识,待所有线程都进入安全点(Safepoint)时,应用线程暂停,串行 GC 开始工作,采用单线程方式回收空间并整理内存。

特点:
1.单线程意味着复杂度更低、占用内存更少,垃圾回收效率高;
2.同时也意味着不能有效利用多核优势。
3.事实上,串行收集器特别适合堆内存不高、单核甚至双核 CPU 的场合。

Serial 收集器

开启选项:-XX:+UseSerialGC

打开此开关后,使用 Serial + Serial Old 收集器组合来进行内存回收。

Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:

  • 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
  • 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。

1.2 并行收集器

开启选项:-XX:+UseParallelGC
打开此开关后,使用 Parallel Scavenge + Serial Old 收集器组合来进行内存回收。
开启选项:-XX:+UseParallelOldGC
打开此开关后,使用 Parallel Scavenge + Parallel Old 收集器组合来进行内存回收。

其他收集器都是以关注停顿时间为目标,而并行收集器是以关注吞吐量(Throughput)为目标的垃圾收集器。

并行收集器是 server 模式下的默认收集器。

并行收集器与串行收集器工作模式相似,都是 stop-the-world 方式,只是暂停时并行地进行垃圾收集。

并行收集器年轻代采用复制算法,老年代采用标记-整理,在回收的同时还会对内存进行压缩。

特点:
并行收集器适合对吞吐量要求远远高于延迟要求的场景。
并且在满足最差延时的情况下,并行收集器将提供最佳的吞吐量。

Parallel Scavenge 收集器

Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是:

  • -XX:MaxGCPauseMillis - 控制最大垃圾收集停顿时间,收集器将尽可能保证内存回收时间不超过设定值。
  • -XX:GCTimeRatio - 直接设置吞吐量大小的(值为大于 0 且小于 100 的整数)。

缩短停顿时间是以牺牲吞吐量和年轻代空间来换取的:年轻代空间变小,垃圾回收变得频繁,导致吞吐量下降。

Parallel Scavenge 收集器还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定年轻代的大小(-Xmn)、EdenSurvivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。

Parallel Old 收集器

是 Parallel Scavenge 收集器的老年代版本,使用多线程和 “标记-整理” 算法

1.3 并发标记-清除收集器(CMS)

开启选项:-XX:+UseConcMarkSweepGC 打开此开关后,使用 CMS + ParNew + Serial Old 收集器组合来进行内存回收。

并发标记-清除收集器是以获取最短停顿时间为目标。

开启后,年轻代使用 ParNew 收集器;老年代使用 CMS 收集器,如果 CMS 产生的碎片过多,导致无法存放浮动垃圾,JVM 会出现 Concurrent Mode Failure ,此时使用 Serial Old 收集器来替代 CMS 收集器清理碎片。

CMS 收集器

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。

CMS 收集器运行步骤如下:

  1. 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
  2. 并发标记: 进行 GC Roots Tracing 即遍历整个对象图的过程,它在整个回收过程中耗时最长,不需要停顿。
  3. 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
  4. 并发清除: 回收在标记阶段被鉴定为不可达的对象。不需要停顿。

缺点:
并发收集 - 并发指的是用户线程和 GC 线程同时运行。 吞吐量低 - 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。 无法处理浮动垃圾 - 可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。

ParNew 收集器

开启选项:-XX:+UseParNewGC

ParNew 收集器其实是 Serial 收集器的多线程版本。多个垃圾收集器并行(parallel)运行

是 Server 模式下的虚拟机首选年轻代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。

ParNew 收集器也是使用 -XX:+UseConcMarkSweepGC 后的默认年轻代收集器。

ParNew 收集器默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。

1.4 G1 收集器

开启选项:-XX:+UseG1GC

前面提到的垃圾收集器一般策略是关注吞吐量或停顿时间。而 G1 是一种兼顾吞吐量和停顿时间的 GC 收集器。

G1其实是Garbage First的意思,垃圾优先? 不是,是优先处理那些垃圾多的内存块的意思。

G1 最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至 CMS 的众多缺陷。

分代和分区
旧的垃圾收集器一般采取分代收集,Java 堆被分为年轻代、老年代和永久代。收集的范围都是整个年轻代或者整个老年代。
G1 取消了永久代,并把年轻代和老年代划分成多个大小相等的独立区域(Region),年轻代和老年代不再物理隔离。G1 可以直接对年轻代和老年代一起回收。即面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

G1收集器的 运作过程大致可划分为以下四个步骤:

  • 初始标记(Initial Marking): 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
  • 并发标记(Concurrent Marking): 从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理 SATB 记录下的在并发时有引用变动的对象。
  • 最终标记(Final Marking): 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
  • 筛选回收(Live Data Counting and Evacuation): 负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region的全部空间。涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
    特点:
  1. 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“标记 - 复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
  2. 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
  3. 垃圾回收过程是 stop the world 事件,所有应用线程都会因此操作暂停;并且使用多线程并行回收。

1.5 总结

收集器 串行/并发 年轻代/老年代 收集算法 目标 使用场景
Serial 串行 年轻代 标记-复制 响应速度优先 单CPU下的 Client 模式
Serial Old 串行 老年代 标记-整理 响应速度优先 单 CPU 环境下的 Client 模式、CMS 的后备预案
ParNew 串行+并行 年轻代 标记-复制 响应速度优先 多 CPU 环境时在 Server 模式下与 CMS 配合
Parallel Scavenge 串行+并行 年轻代 标记-复制 吞吐量优先 在后台运算而不需要太多交互的任务
Parallel Old 串行+并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务
CMS 串行+并发 老年代 标记-清除 响应速度优先 集中在互联网站或 B/S 系统服务端上的 Java 应用
G1 串行+并发 年轻代+老年代 标记整理+标记复制 响应速度优先 面向服务端应用,将来替换 CMS

二、内存分配与回收策略

对象的内存分配,也就是在堆上分配。主要分配在年轻代的 Eden 区上,少数情况下也可能直接分配在老年代中。

2.1 Minor GC

Eden 区空间不足时,触发 Minor GC。

Minor GC 发生在年轻代上,因为年轻代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。

Minor GC 工作流程:

  1. Java 应用不断创建对象,通常都是分配在 Eden 区域,当其空间不足时(达到设定的阈值),触发 minor GC。仍然被引用的对象(绿色方块)存活下来,被复制到 JVM 选择的 Survivor 区域,而没有被引用的对象(黄色方块)则被回收。
  2. 经过一次 Minor GC,Eden 就会空闲下来,直到再次达到 Minor GC 触发条件。这时候,另外一个 Survivor 区域则会成为 To 区域,Eden 区域的存活对象和 From 区域对象,都会被复制到 To 区域,并且存活的年龄计数会被加 1。
  3. 类似第二步的过程会发生很多次,直到有对象年龄计数达到阈值,这时候就会发生所谓的晋升(Promotion)过程,如下图所示,超过阈值的对象会被晋升到老年代。这个阈值是可以通过 -XX:MaxTenuringThreshold 参数指定。

2.2 Full GC

Full GC 发生在老年代上,老年代对象和年轻代的相反,其存活时间长,因此 Full GC 很少执行,而且执行速度会比 Minor GC 慢很多。

Full GC 的触发条件:

  1. 调用 System.gc()
  2. 老年代空间不足 如果清除后仍然不足则会有OutOfMemoryError: Java heap space异常
  3. 方法区空间不足 如果清除后仍然不足则会有OutOfMemoryError: PermGen space异常
  4. Minor GC 的平均晋升空间大小大于老年代可用空间
  5. 对象大小大于 To 区和老年代的可用内存

2.3 内存分配策略

(一) 对象优先在 Eden分配

大多数情况下,对象在年轻代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。

(二) 大对象直接进入老年代

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。

(三) 长期存活的对象进入老年代

为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。

-XX:MaxTenuringThreshold 用来定义年龄的阈值。

(四) 动态对象年龄判定

虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

(五) 空间分配担保

在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于年轻代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的;如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC