这个教程覆盖了如何使用G1垃圾收集器和它是怎样被Hotspot JVM
使用的,你会学到G1收集器内部是如何工作的,使用G1时的一些关键命令行开关和记录它的操作的一些选项。
大约1小时
这个OBE(Oracle By Example)
覆盖了Java里的Java虚拟机G1垃圾回收的基本概念,在OBE
的第一部分,
在介绍垃圾收集器和性能时会附带提供JVM
的概览。下一部分回顾一下Hotspot JVM
的CMS
收集器如何工作。然后,一步一步来指导使用Hotspot JVM
的G1
垃圾回收,跟着,会用一段来讲G1
垃圾回收器的可用的命令行选项。最后,你会学到G1
回收器的日志选项。
下面是一个硬件软件需求列表:
开启教程之前,你应该:
C:/javademos
Java是Sun微系统公司在1995年首次发布的一个编程语言和计算平台。它是支撑Java程序包括工具、游戏、商业应用的底层技术。在全球Java运行在超过8亿5千万台个人电脑上,数以十亿计的设备上,包括移动、电视设备。Java由一些关键组件组成,作为一个整体,创建Java平台。
当你下载Java,你获得Java运行环境(JRE,Java Runtime Environment)。 JRE由Java虚拟机(JVM)、Java平台核心类和支撑Java平台的库组成。要在你电脑上运行Java应用,所有的这些都是必须的。有了Java 7, Java应用从操作系统方面来说是作为桌面应用运行的,作为一个桌面应用但是从网络上安装需要使用Java Web Start
,或者在浏览器里作为一个Web嵌入式的应用(使用JavaFX
)
Java是一个面向对象的编程语言,包含以下特性:
字节码
,在JVM里被加载。一旦应用在JVM里运行,它们可以运行在许多不同的操作系统和设备上。Java开发工具箱(JDK,Java Development Kit)是一个开发Java应用的工具集。有了JDK,你可以编译用Java语言编写的应用程序,在JVM里面运行它们。另外,JDK提供工具打包和发布你的应用。
JDK和JRE分享同样的Java应用编程接口(Java API,Java Application Programming Interfaces)。Java API是已经打包的库的集合,开发者用来创建Java应用。Java API通过提供工具完成很多通常的编程任务包括字符串操作、时间日期处理、网络和实现数据结构(比如:列表、映射、栈和队列)使开发更加容易。
Java虚拟机(JVM,Java Virtual Machine)是一个抽象的计算机器,Java虚拟机是一个程序,对在它里面运行的编写的程序来说, 看起来像一个机器。这样,Java程序就会用相同的接口和库来编写。每一个针对特定操作系统的JVM实现,把Java程序指令翻译成运行在本地操作系统的指令和命令。这样,Java程序实同了平台独立。
Sun微系统公司完成了第一个Java虚拟机的原型实现,仿真Java虚拟机指令集设置进一个被类似当时的个人数码助手(PDA,Personal Digital Assistant)手持设备的托管的软件里。Oracle现在实现仿真Java虚拟机在移动、桌面和服务器设备上,但是Java虚拟机没有承担任何具体的技术实现,管理硬件或者管理操作系统。It is not inherently interpreted, but can just as well be implemented by compiling its instruction set to that of a silicon CPU. It may also be implemented in microcode or directly in silicon(斜体这里翻译不清楚,请大神指点。好像涉及解释执行、指令集、微码等)
Java虚拟机对Java编程语言一概不知,只知道一个特定的二进制格式,就是类文件格式,一个类文件包含Java虚拟机指令(或者叫字节码)和一个符号表,和一些辅助信息。
为了达到安全的目的,Java虚拟机在类文件代码上利用强大的语法和结构化的约束条件。然而任何函数性语言可以依照一个可用的被Java虚拟机托管类文件来表达。被通用的、机器平台独立性吸引,其它语言的实现者可以把Java虚拟机视为其它语言的递送载体,(1)The Java Virtual Machine
Hotspot
虚拟机拥有一个架构,它支持强大特性和能力的基础平台,支持实现高性能和强大的可伸缩性的能力。举个例子,Hotspot虚拟机JIT编译器生成动态的优化,换句话说,它们在Java应用执行期做出优化,为底层系统架构生成高性能的本地机器指令。另外,经过它的运行时环境和多线程垃圾回收成熟的进化和连续的设计, Hotspot
虚拟机在高可用计算系统上产出了高伸缩性。
Java虚拟机的主要组件,包括类加载器、运行时数据区和执行引擎。
Java虚拟机有关性能的关键组件已经在下面的图片上高亮显示了。
Java虚拟机有三个组件关注着什么时候进行性能优化,堆空间是你的对象所存储的地方,这个区域
被启动时选择的垃圾回收器管理,大部分调优选项与调整堆大小和根据你的情况选择最适当的垃圾收集器相关。即时编译器对性能也有很大的影响,但是使用新版本的Java虚拟机时很少需要调整。
典型的,当调优一个Java应用时,把焦点放在两个主要的目标上:响应能力或者吞吐量。随着教程的进行我们会再重新提及这些概念。
响应能力指的是一个应用回应一个请求数据的速度有多快。示例包括:
吞吐量关注在特定的时间周期内一个应用的工作量的最大值。举例如何衡量吞吐量,包括:
Garbage-First(G1,垃圾优先)收集器是服务类型的收集器,目标是多处理器机器、大内存机器。它高度符合垃圾收集暂停时间的目标,同时实现高吞吐量。Oracle JDK 7 update 4 以及更新发布版完全支持G1垃圾收集器。G1垃圾回集器为以下应用设计:
G1垃圾收集器计划长期替换并发标记清除收集器(CMS,Concurrent Mark-Sweep Collector)。G1和CMS比较,有一些不同点让G1成为一个更好的解决方案。一个不同点是G1是一个压缩收集器。G1收集器充分地压缩空间以完全避免为分配空间使用细粒度的空闲列表,而不是依赖于区块。这相当简化了收集器的部件,和尽量消除可能的碎片问题。同时,G1收集器相比CMS收集器而方言,提供更可预言的垃圾收集暂停时间,允许用户指定想要暂停时间指标。
旧的垃圾收集器(串行的:serial,并行的:parallel,并发标记清除:CMS)都把堆结构化为三个部分:年轻代、年老代和固定大小的永久代。
所以内存对象最终都在这三个区域里。
G1收集器应用了一个不同的方法。
堆空间被分割成一些相同大小的堆区域,每一个都是连续范围的虚拟内存。特定的区域集合像旧的收集器一样被指派为相同的角色(伊甸:eden、幸存:survivor、年老:old),但是它们没有一个固定大小。这在内存使用上提供了更强大的灵活性。
当执行垃圾收集时,G1收集器以与CMS收集器类似的方式操作。G1收集器执行一个全局的并发标记阶段来决定堆中的对象的活跃度。之后标记阶段就完成了。G1收集器知道哪个区域基本上是空的。它首先会收集那些产出大量空闲空间的区域。这就是为什么这个垃圾收集的方法叫做垃圾优先的原因。就像名称显示的那样,G1收集器集中它的收集和压缩活动在堆里的那些可完全被回收的区域,那就是垃圾。G1收集器使用一个暂停预言的模式去达到一个用户定义的暂停时间指标,基于用户指定的暂停时间指标去选择收集区域的数量。
被G1收集器鉴定为可以回收的区域就是垃圾,使用抽空的方式收集。G1收集器从堆空间的一个或多个区域里复制对象到堆空间的一个单独的区域内,这个过程中同时压缩和释放内存。这个抽空过程在多处理上以并行的方式运行,以减小暂停时间和增加吞吐量。因此,每一次垃圾收集G1收集器连续不断地去减少碎片,在用户指定的暂停时间内工作。这超越了以往方法的能力。并发标记-清除(CMS,Concurrent Mark Sweep)垃圾收集器不做压缩操作。并行年老代(ParallelOld)垃圾收集只进行整个堆的压缩,会导致相当大的暂停时间。
注意: G1收集器不是实时的收集器非常重要。它在很大程度上符合用户设定的暂停时间指标但是并不绝对符合。基于前面垃圾收集的数据来看,G1收集器会估算在用户指定的时间指标能收集多少区域。因此,收集器有一个合理的精确的收集这些区域的代价模型,它使用这个模型决定在用户指定的暂停时间内收集哪些、多少个区域。
注意: G1收集器同时有并发(和应用线程一起运行,比如,提炼、标记、清理)和并行(多线程,比如,stop the world)两个阶段。全量垃圾回收仍然是单线程的,但是如果调优的适当你的应用应该会避免全量垃圾回收。
如果你从ParallelOldGc或者CMS收集器迁移到G1收集器,你很有可能会看到一个大的Java虚拟机进程大小,这和审计”数据结构比如已记忆集合(Remembered Sets)和收集集合(Collection Sets)有很大关系“。
Remembered Sets或者RSets把对象引用推进一个给定的区域。在堆空间中每一个区有一个RSet。RSet允许一个区域并行的、独立的收集。RSet总体的足迹影响小于5%。
Collection Sets或者CSets,是在垃圾回收过程中会被回收的区域集合。在RSet中的所有活跃对象在垃圾回收过程中会被抽空(复制/移动)。集合包含的区域可以是eden、survivor或者年老代。CSets在Java虚拟机大小的影响小于1%。
G1收集器首要关注的是为用户运行着需要大堆空间、限制的垃圾回收延迟的应用提供一个解决方案。这意味着堆大小为6GB左右或者更大,稳定的、可预言的暂停时间小于0.5秒。
如果应用有以下一个或多个特点,当下运行着CMS或ParallelOldGC垃圾收集器的应用把收集器切换到G1收集器的话,会从中受益的:
注意:如果你在使用CMS或者ParallenOldGC收集器,你的应用不曾经历过长时间的垃圾收集暂停,保持使用你当前的收集器比较好。在使用最新的JDK的情况下,改变到G1收集器不是一个必要的事情。
并发标记清除(CMS)收集器(也叫并发低延迟收集器)回收年老代垃圾。它通过和应用线程并发的执行大部分垃圾收集工作的方式来尝试最小化垃圾回收引起的暂停。正常情况下并发低延迟收集器不会复制或者压缩活跃对象。一次垃圾收集的完成不必移动活跃对象。如果内存碎片成为一个问题,分配更大的堆空间。
注意: CMS收集器在年轻代上使用和并行收集器相同的算法。
CMS收集器在堆的年老代空间上执行以下阶段:
阶段 | 描述 |
---|---|
(1)初始标记(Stop the World事件) | 年老代里的对象被标记为可达的包括那些可能从年轻代可达的对象。此期间暂停时间相对minor gc的暂停时间是比较 短的 |
(2)并发标记 | 当Java应用线程运行时,并发的遍历年老代对象图可达的对象。从标记的对象和根上可达到标记对象开始扫描。设值方法在并发的2、3、5阶段期间执行,在这些阶段(包括晋升的对象)被分配进CMS代所有对象都会立刻被标记为活跃对象。 |
(3)重新标记(Stop the World事件) | 寻找那些在并发标记阶段丢失的,在并发收集器完成之后跟踪对象之后由Java应用线程的更新的对象。 |
(4)(并发清除) | 收集在标记阶段被鉴定为不可达的对象。收集死亡对象会增加空闲列表的空间,方便之后的对象分配。聚合死亡对象可以会在此点发生。注意活跃对象是不会被移动。 |
(5)(重新设置) | 清理数据结构为下一次并发收集做准备 |
接下来,让我们一步一步的回顾下CMS收集器的操作步骤
堆空间被分割为三块空间。
年轻代分割成一个Eden区和两个Survivor区。年老代一个连续的空间。就地完成对象收集。除非有FullGC否则不会压缩。
年轻代被标为浅绿色,年老代被标记为蓝色。如果你的应用已经运行了一段时间,CMS的堆看起来应该是这个样子。对象分散在年老代区域里。
使用CMS,年老代对象就地释放。它们不会被来回移动。这个空间不会被压缩除非发生FullGC。
从Eden和Survivor区复制活跃对象到另一个Survivor区。所有达到他们的年龄阈值的对象会晋升到年老代。
一次年轻代垃圾收集之后,Eden区和其中一个Survivor区被清空。
最近晋升的对象以深蓝色显示在上图中,绿色的对象是年轻代幸免的还没有晋升到老年代对象。
发生两次stop the world事件:初始标记和重新标记。当年老代达到特定的占用比例时,CMS开始执行。
(1)初始标记是一个短暂暂停的、可达对象被标记的阶段。(2)并发标记寻找活跃对象在应用连续执行时。最后,在(3)重新标记阶段,寻找在之前并发标记阶段中丢失的对象。
在之前阶段没有被标记的对象会被就地释放。不进行压缩操作。
注意:未被标记的对象等于死亡对象
(4)清除阶段之后,你可以看到大量内存被释放。你还可以注意到没有进行压缩操作。
最后,CMS收集器会走过(5)重新设置阶段,等待下一次垃圾收集时机的到来。
G1收集器在分配堆空间的方法上有些不同。下面的图片一步一步系统的回顾G1收集器。
堆空间是一个被分成许多固定大小区域的内存块。
Java虚拟机启动时选定区域大小。Java虚拟机通常会指定2000个左右的大小相等、每个大小范围在1到32M的区域。
实际上,这些区域被映射成Eden、Survivor、年老代空间的逻辑表述形式。
图片中的颜色表明了哪个区域被关联上什么角色。活跃对象从一个区域疏散(复制、移动)到另一个区域。区域被设计为并行的方式收集,可以暂停或者不暂停所有的其它用户线程。
明显的区域可以被分配成Eden、Survivor、Old区域。另外,有第四种类型的区域叫做极大区域(Humongous regions)。这些区域被设计成保持标准区域大小的50%或者更大的对象。它们被保存在一个连续的区域集合里。最后,最后一个类型的区域就是堆空间里没有使用的区域。
注意:写作此文章时,收集极大对象时还没有被优化。因此,你应该避免创建这个大小的对象。
堆空间被分割成大约2000个区域。最小1M,最大32M,蓝色区域保持年老代对象,绿色区域保持年轻代对象。
注意:区域没有必要像旧的收集器一样是保持连续的。
活跃对象会被疏散(复制、移动)到一个或多个survivor区域。如果达到晋升总阈值,对象会晋升到年老代区域。
这是一个stop the world暂停。为下一次年轻代垃圾回收计算Eden和Survivor的大小。保留审计信息有助于计算大小。类似目标暂停时间的事情会被考虑在内。
这个方法使重调区域大小变得很容易,按需把它们调大或调小。
活跃对象被疏散到Survivor或者年老代区域。
最近晋升的对象显示为深蓝色。Survivor区域显示为绿色。
关于G1的年轻代回收做以下总结:
类似CMS收集器,G1收集器为年老代对象被设计成一个低暂停收集器。下面的表描述了在年老代上的G1收集阶段。
G1垃圾收集器在堆上的年老代执行以下阶段。注意一些阶段是年轻代回收的一部分。
阶段 | 描述 |
---|---|
(1)初始标记(stop the world事件) | 这是一个stop the world事件,使用G1回收器,背负着一个常规的年轻代收集。标记那些有引用到年老代的对象的survivor区(根区) |
(2)根区扫描 | 为到年老代的引用扫描survivor区,这个发生在应用继续运行时。这个阶段在年轻代收集前必须完成 |
(3)并发标记 | 遍历整个堆寻找活跃对象,这个发生在应用运行时,这个阶段可以被年轻代垃圾回收打断。 |
(4)重新标记(stop the world事件) | 完全标记堆中的活跃对象,使用一个叫作snapshot-at-the-beginning(SATB)的比CMS收集器的更快的算法 |
(5)清理(stop the world事件和并发) | 在活跃对象上执行审计操作和释放区域空间(stop the world);净化已记忆集合(stop the world);重置空间区域和返回它们到空闲列表(并发) |
(*)复制(stop the world事件) | 这些是stop the world暂停为了疏散或者复制活跃对象到新的未使用的区域。这个可以由被记录为[GC Pause (young)]的年轻代区域或者被记录为[GC Pause (mixed)]年轻代和年老代区域完成 |
记住已被定义的阶段,让我们来看一下G1收集器是如何作用于年老代的。
年轻代垃圾收集肩负着活跃对象初始标记的任务。在日志文件中被标为GC pause (young)(inital-mark)
如果发现空区域(“X”标示的),在重新标记阶段它们会被马上清除掉。当然,决定活性的审计信息也在此时被计算。
G1选择活性最低的区域,这些区域能够以最快的速度回收。然后这些区域会在年轻代垃圾回收过程中被回收。在日志中被指示为[GC pause (mixed)]。所以年轻代和年老代在同一时间被回收。
被选择的区域已经被回收和压缩到图中显示的深蓝色区和深绿色区中。
总结下,我们可以列出一些关于G1收集器在年老代的上关键点。
并发标记阶段
- 当应用运行时,并发的计算活性信
- 在疏散暂停期间,活性信息鉴定哪些区被最好的回收
- 没有像CMS一样的清除操作
重新标记阶段
- 使用比在CMS中使用的算法更快的Snapshot-at-the-Beginning(SATB)算法
- 完全空的区域会被回收掉
复制/清理阶段
- 年轻代和年老代被同时回收
- 年老代区域基于它们的活性被选择
在这部分我们看一下G1收集器的多样的命令行选项。
为了启用G1收集器,使用:-XX:+UseG1GC
这个是启动在已下载的JDK演示和示例里的Java2Demo程序的示例命令行:
java -Xmx50m -Xms50m -XX:UserG1GC -XX:MaxGCPauseMillis=200 -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar
-XX:+UseG1GC - 告诉Java虚拟机使用G1垃圾收集器
-XX:MaxGCPauseMillis=200 - 为最大GC暂停时间设置一个指标。这是一个软目标,Java虚拟机将尽最大努力实现它。因此,暂停时间目标有时候可能不会达到。默认值是200毫秒。
-XX:InitiatingHeapOccupancyPercent=45 - 触发并发垃圾收集周期的整个堆的百分比时机。
使用G1收集器时你应该遵守的一些最佳实践
不要设置年轻代大小
通过-Xmn明确地设置年轻代大小来插手G1收集器的默认行为。
代替使用平均响应时间(ART)做为指标,来设置XX:MaxGCPauseMillis=,考虑设置值将会符合这个时间的90%或者更高比例。这意味着90%的用户发出一个请求将不会经历高于这个目标的时间。记住,暂停时间只是一个目标,不保证总是能够达到。
当Java虚拟机在Survivor和晋升的对象垃圾回收期间,堆空间用光了就会发生晋升失败。堆空间不能再扩展了因为已经在最大值了,使用-XX:+PrintGCDetails参数时,这种情况会在GC日志中通过to-space-overflow指示出来。这个代价非常大。
为了避免疏散失败,考虑以下选项。
增大堆大小
- 增大-XX:G1ReservePercent=n参数值,默认是10
- G1收集器创建一个假的上限通过尝试保留储备内存的自由假如’to-space’被渴望得到。
提前启动标记周期
使用-XX:ConcGCThreads=n选项增大标记线程的数量
这是一个G1垃圾收集器开关的完整列表,记着去使用上述的最佳实践。
选项和默认值 | 描述 |
---|---|
-XX:+UseG1GC | 使用垃圾优先(G1,Garbage First)收集器 |
-XX:MaxGCPauseMillis=n | 设置垃圾收集暂停时间最大值指标。这是一个软目标,Java虚拟机将尽最大努力实现它 |
-XX:InitiatingHeapOccupancyPercent=n | 触发并发垃圾收集周期的整个堆空间的占用比例。它被垃圾收集使用,用来触发并发垃圾收集周期,基于整个堆的占用情况,不只是一个代上(比如:G1)。0值 表示’do constant GC cycles’。默认是45 |
-XX:NewRatio=n | 年轻代与年老代的大小比例,默认值是2 |
-XX:SurvivorRatio=n | eden与survivor空间的大小比例,默认值8 |
-XX:MaxTenuringThreshold=n | 最大晋升阈值,默认值15 |
-XX:ParallerGCThreads=n | 设置垃圾收集器并行阶段的线程数量。默认值根据Java虚拟机运行的平台有所变化 |
-XX:ConcGCThreads=n | 并发垃圾收集器使用的线程数量,默认值根据Java虚拟机运行的平台有所变化 |
-XX:G1ReservePercent=n | 为了降低晋升失败机率设置一个假的堆的储备空间的上限大小,默认值是10 |
-XX:G1HeapRegionSize=n | 使用G1收集器,Java堆被细分成一致大小的区域。这设置个体的细分的大小。这个参数的默认值由工学意义上的基于堆的大小决定 |
我们需要涵盖的最后的主题是使用G1垃圾回收器的日志记录信息来分析性能。这部分提供你可以用来收集打印在日志里的数据和信息的开关的快速的概览。
你可以设置三种不同级别的详情。
(1)-verbosegc (和-XX:+PrintGC等效)参数设置fine日志详情级别
sample Output
[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详情级别。使用这个选项会显示以下信息:
* 显示每个阶段的平均、最小、最大时间
* 根扫描、RSet更新(附带处理的缓冲信息)、RSet扫描、对象复制、终止(附带尝试次数)。
* 也显示’other’时间,比如花费在选择CSet上的时间、引用处理、引用排队和释放CSet。
* 显示Eden、Survivor和总堆空间占用。
Sample Output
[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但是包括每个工作者线程的信息。
Sample Output
[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]
有两个开关可以决写在垃圾收集日志中如何显示时间。
(1)-XX:+PrintGCTimeStamps - 显示自从Java虚拟机启动之后流逝的时间。
Sample Output
1.729: [GC pause (young) 46M->35M(1332M), 0.0310029 secs]
(2)-XX:+PrintGCDateStamps - 为每一项添加日期时间的前缀。
Sample Output
2012-05-02T11:16:32.057+0200: [GC pause (young) 46M->35M(1332M), 0.0317225 secs]
为了理解这个日志,这部分使用实际的垃圾收集输出日志来明确一些术语。下面的示例列出了输出日志中的术语和值,你会在日志中找到它们。
注意:更多信息请查看Poonam Bajaj’s Blog post on G1 GC logs
Worker Start
Parallel Time
External Root Scanning
Update Remembered Set
Scanning Remembered Sets
Object Copy
Termination Time
GC Worker End
GC Worker Other
Clear CT
Other
CSet
Ref Proc
Ref Eng
Free CSet
此入门教程源地址上有一些介绍,但是和Poonam Bajaj’s Blog post on G1 GC logs内容几乎相同,所以详细信息请看我翻译的Poonam Bajaj’s Blog post on G1 GC logs
在这个OBE
里,你已经对包含在Java虚拟机里的G1垃圾收集器有了大概的认识。首先你学习了为什么堆和垃圾收集器是任何Java虚拟机的关键部件。然后你回顾了使用CMS收集器和G1收集器的垃圾收集是如何工作的。然后你学习了关于G1收集器的命令行开关和使用它们的最佳实践。最后,你学习了如何把对象和数据记录到垃圾收集日志里。
在这个教程里,你已经学到了:
更多相关信息请查看下面这些站点和链接:
教程源地址:Getting Started with the G1 Garbage Collector
有翻译不对或者不清楚的地方,敬请各位大神指正,文中就有一处不会翻译
注意:尊重劳动成果,转载请注明出处,谢谢! G1 垃圾收集器入门