G1 Getting Started

铁锚翻译的Oracle介绍G1垃圾回收器的文章。

说明

concurrent: 并发, 多个线程协同做同一件事情(有状态)

parallel: 并行, 多个线程各做各的事情(互相间无共享状态)

参考: What’s the difference between concurrency and parallelism

概述

目的

本文介绍如何使用G1,及在 Hotspot JVM 中怎么使用G1垃圾收集器。 您将了解 G1 收集器的内部原理, 切换为 G1 收集器的命令行参数, 以及让其记录GC日志的选项。

需要的时间

大约 1 个小时

简介

本文涵盖了Java虚拟机(JVM, Java Virtual Machine)中 G1 的基础知识。

  1. 第一部分, 简单概述JVM的同时介绍了垃圾收集和性能.
  2. 接下来讲述了 Hotspot JVM 中 CMS 收集器是如何工作的.
  3. 接着再一步一步地指导在 Hotspot JVM 中使用G1进行垃圾回收的工作方式.
  4. 之后的一个小节介绍 G1 垃圾收集器可用的命令行参数.
  5. 最后,您将了解如何配置使G1收集器记录日志.

硬件与软件环境需求

下面是 硬件与软件环境需求 清单:

  • 一台PC机, 运行 Windows XP 以上操作系统, Mac OS X 或者 Linux 都可以. 注意,因为作者在Windows 7上进行开发和测试, 尚未在所有平台上完成测试。 但在 OS X和Linux 上应该也是正常的。最好配置了多核CPU.
  • Java 7 Update 9 或更高版本
  • 最新的 Java 7 Demos and Samples Zip 文件

准备条件

在开始学习本教程之前, 你需要:

  • 下载并安装最新的 Java JDK (JDK 7 u9 或 以后的版本): Java 7 JDK 下载页面

  • 下载并安装 Demos and Samples (示例与样例) zip 文件, 下载页面和JDK相同. 然后解压到合适的位置. 如: C:\javademos

Java 技术 和 JVM

Java 概述

Java 是 Sun Microsystems 公司在1995年发布的一门编程语言. 同时也是一个运行Java程序的底层平台. 提供工具、游戏和企业应用程序支持。Java 运行在全世界超过8.5亿的PC,以及数十亿的智能设备上,包括 mobile 和 TV. Java 是由许多关键部件组成的一个整体, 统称为Java平台。

JRE(Java Runtime Edition)

一般来说下载了Java以后, 你就得到了一个Java运行时: Java Runtime Environment (JRE). JRE 由Java虚拟机 Java Virtual Machine (JVM), Java 平台核心类(core classes), 以及 Java平台支持库组成. 必须有这三大组件的支持才能在你的电脑上运行 Java 程序. 例如 Java 7, 可以在操作系统上作为桌面应用程序运行, 还可以通过 Java Web Start 从Web上安装, 或者是作为嵌入式Web程序在浏览器中运行 (通过 JavaFX).

Java 编程语言

Java 是一门面向对象编程语言(object-oriented programming language), 包涵以下特性.

  • Platform Independence - Java 应用程序被编译为字节码(bytecode)存放到 class 文件中, 由JVM加载. 因为程序在 JVM 中运行, 所以可以跨平台运行在各种操作系统/设备上.
  • Object-Oriented - Java 是一门面向对象的语言, 继承了 C 和 C++ 的很多特性,并在此基础上进行扩充和优化.
  • Automatic Garbage Collection - Java对内存进行 自动分配(allocates) 和自动释放(deallocates). 所以程序不再执行这一繁琐的任务(其实自动内存回收,更多的好处是减少了编程需要重复处理的这种细节,另一个例子是对JDBC的封装).
  • Rich Standard Library - Java包含大量的标准对象,可以执行诸如输入输出(input/output), 网络操作以及日期处理等任务.

JDK(Java Development Kit)

JDK 是用来开发Java程序的一系列工具集. 通过JDK, 你可以编译用Java语言书写的程序, 并在 JVM 中运行. 另外, JDK 还提供了打包(packaging)和分发(distributing)程序的工具.

JDK 和 JRE 使用同样的 Java Application Programming Interfaces (Java API).Java API 是预先打包好以供程序员用来开发程序的类库集合. 通过 Java API 使得很多常规任务可以很轻松的就完成,如 字符串操作(string manipulation), 时间日期处理(date/time processing), 网络编程(networking), 以及实现各种数据结构(data structures, 如 lists, maps, stacks, and queues).

JVM(Java Virtual Machine)

Java Virtual Machine (JVM) 是一台抽象的计算机(abstract computing machine). JVM 本质是一个程序, 但在运行于JVM上的程序看来, 他就像一台真实机器一样. 这样, Java程序就能使用相同的接口和库. 每种特定操作系统上的 JVM 实现, 都将 Java 程序指令转换为本地机器的指令(instructions)和命令(commands). 由此,实现了Java程序的平台独立性.

Java虚拟机的第一个原型实现,由 Sun Microsystems, Inc. 完成, 在一台手持设备上用软件模拟了 Java虚拟机指令集, 类似于今天的 PDA(Personal Digital Assistant). Oracle 当前在移动设备,桌面系统和服务器上都提供了Java虚拟机实现, 但Java虚拟机不限制使用任何特定的技术,硬件,或操作系统。JVM也不一定都是基于软件的,你可以直接在硬件CPU上实现JVM指令, 还可以芯片上实现,或者采用 microcode 的方式来实现.

Java 虚拟机完全不关心Java语言的细节, 只识别 class 文件这种特定的二进制格式. 一个 class 文件包含 Java虚拟机指令(或称之为字节码 bytecode) 及符号变量表(symbol table), 还有一些辅助信息.

基于安全性考虑, Java虚拟机对 class 文件中的代码执行 强语法检查和组成结构规范限制. 既然虚拟机有这种特征, 那么任何一门编程语言,只要能编译为合法的 class 文件,都可以加载到 Java虚拟机 里面执行。由于具有通用性,跨平台特性, 其他语言的实现者可以把Java虚拟机作为该语言的加载执行工具。(1) The Java Virtual Machine

探索 JVM 体系架构

Hotspot 架构

HotSpot JVM 有一个稳定强悍的架构, 支持强大的功能与特性, 具备实现高性能和大规模可伸缩性的能力。例如,HotSpot JVM JIT编译器能动态进行优化生成。换句话说,他们运行Java程序时,会针对底层系统架构动态生成高性能的本地机器指令。此外,通过成熟的演进和运行时环境的持续工程,加上多线程垃圾收集器,HotSpot JVM即使实在大型计算机系统上也能获得很高的伸缩性.

刚刚被提升上来的对象用深绿色显示. Survivor 区用绿色表示.

总结起来,G1的年轻代收集归纳如下:

  • 堆一整块内存空间,被分为多个heap区(regions).
  • 年轻代内存由一组不连续的heap区组成. 这使得在需要时很容易进行容量调整.
  • 年轻代的垃圾收集,或者叫 young GCs, 会有 stop the world 事件. 在操作时所有的应用程序线程都会被暂停(stopped).
  • 年轻代 GC 通过多线程并行进行.
  • 存活的对象被拷贝到新的 survivor 区或者老年代.

Old Generation Collection with G1

和 CMS 收集器相似, G1 收集器也被设计为用来对老年代的对象进行低延迟(low pause)的垃圾收集. 下表描述了G1收集器在老年代进行垃圾回收的各个阶段.

G1 收集阶段 - 并发标记周期阶段(Concurrent Marking Cycle Phases)

G1 收集器在老年代堆内存中执行下面的这些阶段. 注意有些阶段也是年轻代垃圾收集的一部分.

阶段 说明
(1) 初始标记(Initial Mark) (Stop the World Event,所有应用线程暂停) 此时会有一次 stop the world(STW)暂停事件. 在G1中, 这附加在(piggybacked on)一次正常的年轻代GC. 标记可能有引用指向老年代对象的survivor区(根regions).
(2) 扫描根区域(Root Region Scanning) 扫描 survivor 区中引用到老年代的引用. 这个阶段应用程序的线程会继续运行. 在年轻代GC可能发生之前此阶段必须完成.
(3) 并发标记(Concurrent Marking) 在整个堆中查找活着的对象. 此阶段应用程序的线程正在运行. 此阶段可以被年轻代GC打断(interrupted).
(4) 再次标记(Remark) (Stop the World Event,所有应用线程暂停) 完成堆内存中存活对象的标记. 使用一个叫做 snapshot-at-the-beginning(SATB, 起始快照)的算法, 该算法比CMS所使用的算法要快速的多.
(5) 清理(Cleanup) (Stop the World Event,所有应用线程暂停,并发执行)
在存活对象和完全空闲的区域上执行统计(accounting). (Stop the world)
擦写 Remembered Sets. (Stop the world)
重置空heap区并将他们返还给空闲列表(free list). (Concurrent, 并发)
(*) 拷贝(Copying) (Stop the World Event,所有应用线程暂停) 产生STW事件来转移或拷贝存活的对象到新的未使用的heap区(new unused regions). 只在年轻代发生时日志会记录为 `[GC pause (young)]`. 如果在年轻代和老年代一起执行则会被日志记录为 `[GC Pause (mixed)]`.

G1老年代收集步骤

顺着定义的阶段,让我们看看G1收集器如何处理老年代(old generation).

6. 初始标记阶段(Initial Marking Phase)

存活对象的初始标记被固定在年轻代垃圾收集里面. 在日志中被记为 GC pause (young)(inital-mark)

7. 并发标记阶段(Concurrent Marking Phase)

如果找到空的区域(如用红叉“X”标示的区域), 则会在 Remark 阶段立即移除. 当然,"清单(accounting)"信息决定了活跃度(liveness)的计算.

8. 再次标记阶段(Remark Phase)

空的区域被移除并回收。现在计算所有区域的活跃度(Region liveness).

9. 拷贝/清理阶段(Copying/Cleanup)

G1选择“活跃度(liveness)”最低的区域, 这些区域可以最快的完成回收. 然后这些区域和年轻代GC在同时被垃圾收集 . 在日志被标识为 [GC pause (mixed)]. 所以年轻代和老年代都在同一时间被垃圾收集.

10.拷贝/清理之后(After Copying/Cleanup)

所选择的区域被收集和压缩到下图所示的深蓝色区域和深绿色区域.

老年代GC(Old Generation GC)总结

总结下来,G1对老年代的GC有如下几个关键点:

  • 并发标记清理阶段(Concurrent Marking Phase)
    • 活跃度信息在程序运行的时候被并行计算出来
    • 活跃度(liveness)信息标识出哪些区域在转移暂停期间最适合回收.
    • 不像CMS一样有清理阶段(sweeping phase).
  • 再次标记阶段(Remark Phase)
    • 使用的 Snapshot-at-the-Beginning (SATB, 开始快照) 算法比起 CMS所用的算法要快得多.
    • 完全空的区域直接被回收.
  • 拷贝/清理阶段(Copying/Cleanup Phase)
    • 年轻代与老年代同时进行回收.
    • 老年代的选择基于其活跃度(liveness).

命令行参数与最佳实践

命令行参数与最佳实践

在本节中,让我们看看G1的各种命令行选项.

命令行基本参数

要启用 G1 收集器请使用: -XX:+UseG1GC

下面是启动 Java2Demo示例程序的命令行示例. Java2Demo位于下载 JDK demos and samples 后解压的文件夹中:

java -Xmx50m -Xms50m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar

关键命令行开关

-XX:+UseG1GC - 让 JVM 使用 G1 垃圾收集器.

-XX:MaxGCPauseMillis=200 - 设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽力去达成这个目标. 所以有时候这个目标并不能达成. 默认值为 200 毫秒.

-XX:InitiatingHeapOccupancyPercent=45 - 启动并发GC时的堆内存占用百分比. G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。值为 0 则表示“一直执行GC循环)'. 默认值为 45 (例如, 全部的 45% 或者使用了45%).

最佳实践

在使用 G1 作为垃圾收集器时,你应该遵循下面这些最佳实践的指导.

不要设置年轻代的大小(Young Generation Size)

假若通过 -Xmn 显式地指定了年轻代的大小, 则会干扰到 G1收集器的默认行为.

  • G1在垃圾收集时将不再关心暂停时间指标. 所以从本质上说,设置年轻代的大小将禁用暂停时间目标.
  • G1在必要时也不能够增加或者缩小年轻代的空间. 因为大小是固定的,所以对更改大小无能为力.
响应时间指标(Response Time Metrics)

设置 XX:MaxGCPauseMillis=<N> 时不应该使用平均响应时间(ART, average response time) 作为指标,而应该考虑使用目标时间的90%或者更大作为响应时间指标. 也就是说90%的用户(客户端/?)请求响应时间不会超过预设的目标值. 记住,暂停时间只是一个目标,并不能保证总是得到满足.

什么是转移失败(Evacuation Failure)?

对 survivors 或 promoted objects 进行GC时如果JVM的heap区不足就会发生提升失败(promotion failure). 堆内存不能继续扩充,因为已经达到最大值了. 当使用 -XX:+PrintGCDetails 时将会在GC日志中显示 to-space overflow (to-空间溢出)。

这是很昂贵的操作!

  • GC仍继续所以空间必须被释放.
  • 拷贝失败的对象必须被放到正确的位置(tenured in place).
  • CSet指向区域中的任何 RSets 更新都必须重新生成(regenerated).
  • 所有这些步骤都是代价高昂的.
如何避免转移失败(Evacuation Failure)

要避免避免转移失败, 考虑采纳下列选项.

  • 增加堆内存大小
    • 增加 -XX:G1ReservePercent=n, 其默认值是 10.
    • G1创建了一个假天花板(false ceiling),在需要更大 'to-space' 内存的情况下会尝试从保留内存获取(leave the reserve memory free).
  • 更早启动标记周期(marking cycle)
  • 通过采用 -XX:ConcGCThreads=n 选项增加标记线程(marking threads)的数量.
G1 的 GC 参数完全列表

下面是完整的 G1 的 GC 开关参数列表. 在使用时请记住上面所述的最佳实践.

选项/默认值 说明
-XX:+UseG1GC 使用 G1 (Garbage First) 垃圾收集器
-XX:MaxGCPauseMillis=n 设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽量去达成这个目标.
-XX:InitiatingHeapOccupancyPercent=n 启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示"一直执行GC循环". 默认值为 45.
-XX:NewRatio=n 年轻代与老年代(new/old generation)的大小比例(Ratio). 默认值为 2.
-XX:SurvivorRatio=n eden/survivor 空间大小的比例(Ratio). 默认值为 8.
-XX:MaxTenuringThreshold=n 提升年老代的最大临界值(tenuring threshold). 默认值为 15.
-XX:ParallelGCThreads=n 设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同.
-XX:ConcGCThreads=n 并发垃圾收集器使用的线程数量. 默认值随JVM运行的平台不同而不同.
-XX:G1ReservePercent=n 设置堆内存保留为假天花板的总量,以降低提升失败的可能性. 默认值是 10.
-XX:G1HeapRegionSize=n 使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb.

记录G1的GC日志

记录G1的GC日志

我们要介绍的最后一个主题是使用日志信息来分享G1收集器的性能. 本节简要介绍垃圾收集的相关参数,以及日志中打印的相关信息.

设置日志细节(Log Detail)

可以设置3种不同的日志级别.

(1) -verbosegc (等价于 -XX:+PrintGC) 设置日志级别为 fine.

日志输出示例

[GC pause (G1 Humongous Allocation) (young) (initial-mark) 24M- >21M(64M), 0.2349730 secs]
[GC pause (G1 Evacuation Pause) (mixed) 66M->21M(236M), 0.1625268 secs]    

(2) -XX:+PrintGCDetails 设置日志级别为 更好 finer. 使用此选项会显示以下信息:

  • 每个阶段的 Average, Min, 以及 Max 时间.
  • 根扫描(Root Scan), RSet 更新(同时处理缓冲区信息), RSet扫描(Scan), 对象拷贝(Object Copy), 终止(Termination, 包括尝试次数).
  • 还显示 “other” 执行时间, 比如选择 CSet, 引用处理(reference processing), 引用排队(reference enqueuing) 以及释放(freeing) CSet等.
  • 显示 Eden, Survivors 以及总的 Heap 占用信息(occupancies).

日志输出示例

[Ext Root Scanning (ms): Avg: 1.7 Min: 0.0 Max: 3.7 Diff: 3.7]
[Eden: 818M(818M)->0B(714M) Survivors: 0B->104M Heap: 836M(4096M)->409M(4096M)]

(3) -XX:+UnlockExperimentalVMOptions -XX:G1LogLevel=finest 设置日志级别为 最好 finest. 和 finer 级别类似, 包含每个 worker 线程信息.

[Ext Root Scanning (ms): 2.1 2.4 2.0 0.0
           Avg: 1.6 Min: 0.0 Max: 2.4 Diff: 2.3]
       [Update RS (ms):  0.4  0.2  0.4  0.0
           Avg: 0.2 Min: 0.0 Max: 0.4 Diff: 0.4]
           [Processed Buffers : 5 1 10 0
           Sum: 16, Avg: 4, Min: 0, Max: 10, Diff: 10]

Determining Time

有两个参数决定了GC日志中打印的时间显示形式.

(1) -XX:+PrintGCTimeStamps - 显示从JVM启动时算起的运行时间.

日志输出示例

1.729: [GC pause (young) 46M->35M(1332M), 0.0310029 secs]

(2) -XX:+PrintGCDateStamps - 在每条记录前加上日期时间.

日志输出示例

2012-05-02T11:16:32.057+0200: [GC pause (young) 46M->35M(1332M), 0.0317225 secs]

理解 G1 日志

为了使你更好地理解GC日志, 本节通过实际的日志输出,定义了许多专业术语. 下面的例子显示了GC日志的内容,并加上日志中出现的术语和值的解释说明.

Note: 更多信息请参考 Poonam Bajaj的博客: G1垃圾回收日志.

G1 日志相关术语

  • Clear CT
  • CSet
  • External Root Scanning
  • Free CSet
  • GC Worker End
  • GC Worker Other
  • Object Copy
  • Other
  • Parallel Time
  • Ref Eng
  • Ref Proc
  • Scanning Remembered Sets
  • Termination Time
  • Update Remembered Set
  • Worker Start
Parallel Time(并行阶段耗时)
414.557: [GC pause (young), 0.03039600 secs] [Parallel Time: 22.9 ms]
[GC Worker Start (ms): 7096.0 7096.0 7096.1 7096.1 706.1 7096.1 7096.1 7096.1 7096.2 7096.2 7096.2 7096.2
       Avg: 7096.1, Min: 7096.0, Max: 7096.2, Diff: 0.2]

Parallel Time – 主要并行部分运行停顿的整体时间

Worker Start – 各个工作线程(workers)启动时的时间戳(Timestamp)

Note: 日志是根据 thread id 排序,并且每条记录都是一致的.

External Root Scanning(外部根扫描)
[Ext Root Scanning (ms): 3.1 3.4 3.4 3.0 4.2 2.0 3.6 3.2 3.4 7.7 3.7 4.4
     Avg: 3.8, Min: 2.0, Max: 7.7, Diff: 5.7]

External root scanning - 扫描外部根花费的时间(如指向堆内存的系统词典(system dictionary)等部分)

Update Remembered Set(更新 RSet)
[Update RS (ms): 0.1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Avg: 0.0, Min: 0.0, Max: 0.1, Diff: 0.1]
   [Processed Buffers : 26 0 0 0 0 0 0 0 0 0 0 0
    Sum: 26, Avg: 2, Min: 0, Max: 26, Diff: 26]

Update Remembered Set - 必须更新在pause之前已经完成但尚未处理的缓冲. 花费的时间取决于cards的密度。cards越多,耗费的时间就越长。

Scanning Remembered Sets(扫描 RSets)
[Scan RS (ms): 0.4 0.2 0.1 0.3 0.0 0.0 0.1 0.2 0.0 0.1 0.0 0.0 Avg: 0.1, Min: 0.0, Max: 0.4, Diff: 0.3]F

Scanning Remembered Sets - 查找指向 Collection Set 的指针(pointers)

Object Copy(对象拷贝)
[Object Copy (ms): 16.7 16.7 16.7 16.9 16.0 18.1 16.5 16.8 16.7 12.3 16.4 15.7 Avg: 16.3, Min: 12.3, Max:  18.1, Diff: 5.8]

Object copy – 每个独立的线程在拷贝和转移对象时所消耗的时间.

Termination Time(结束时间)
[Termination (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
0.0 Avg: 0.0, Min: 0.0, Max: 0.0, Diff: 0.0] [Termination Attempts : 1 1 1 1 1 1 1 1 1 1 1 1 Sum: 12, Avg: 1, Min: 1, Max: 1, Diff: 0]

Termination time - 当worker线程完成了自己那部分对象的复制和扫描,就进入终止协议(termination protocol)。它查找未完成的工作(looks for work to steal), 一旦它完成就会再进入终止协议。 终止尝试记录(Termination attempt counts)所有查找工作的尝试次数(attempts to steal work).

GC Worker End
[GC Worker End (ms): 7116.4 7116.3 7116.4 7116.3 7116.4 7116.3 7116.4 7116.4 7116.4 7116.4 7116.3 7116.3
    Avg: 7116.4, Min: 7116.3, Max: 7116.4, Diff:   0.1]
[GC Worker (ms): 20.4 20.3 20.3 20.2 20.3 20.2 20.2 20.2 20.3 20.2 20.1 20.1
     Avg: 20.2, Min: 20.1, Max: 20.4, Diff: 0.3]

GC worker end time – 独立的 GC worker 停止时的时间戳.

GC worker time – 每个独立的 GC worker 线程消耗的时间.

GC Worker Other
[GC Worker Other (ms): 2.6 2.6 2.7 2.7 2.7 2.7 2.7 2.8 2.8 2.8 2.8 2.8
    Avg: 2.7, Min: 2.6, Max: 2.8, Diff: 0.2]

GC worker other – 每个GC线程中不能归属到之前列出的worker阶段的其他时间. 这个值应该很低. 过去我们见过很高的值,是由于JVM的其他部分的瓶颈引起的(例如在分层[Tiered]代码缓存[Code Cache]占有率的增加)。

Clear CT
[Clear CT: 0.6 ms]

清除 RSet 扫描元数据(scanning meta-data)的 card table 消耗的时间.

Other
[Other: 6.8 ms]

其他各种GC暂停的连续阶段花费的时间.

CSet
[Choose CSet: 0.1 ms]

敲定要进行垃圾回收的region集合时消耗的时间. 通常很小,在必须选择 old 区时会稍微长一点点.

Ref Proc
[Ref Proc: 4.4 ms]

处理 soft, weak, 等引用所花费的时间,不同于前面的GC阶段

Ref Enq
[Ref Enq: 0.1 ms]

将 soft, weak, 等引用放置到待处理列表(pending list)花费的时间.

Free CSet
[Free CSet: 2.0 ms]

释放刚被垃圾收集的 heap区所消耗的时间,包括对应的remembered sets。

总结

在此OBE中, 您对Java JVM 中的G1垃圾收集器有了个大致的了解。首先你学到了为何堆和垃圾收集器是所有Java JVM的关键部分。接下来讲述了使用CMS和G1收集器进行垃圾回收的工作方式. 接下来,您了解了G1的命令行参数/开关以及和使用它们的最佳实践。最后,您了解了日志对象以及GC日志中的数据。

在本教程中,你学到了这些知识:

  • Java JVM 的组成部分
  • 对 G1 的概述
  • 概述 CMS 垃圾收集器
  • 概述 G1 垃圾收集器
  • 命令行参数与最佳实践
  • G1 的日志信息

相关资源

更多相关信息请参考以下网站链接.

作者信息

  • 课程开发人员: Michael J Williams
  • 质量保证: Krishnanjani Chitta