技术记录栈

记录点滴:java、datebase、linux、spring、javascript、nginx

2018/08/05

Java G1垃圾收集器工作原理和特性

本教程介绍如何在Hotspot JVM中使用G1垃圾收集器,了解G1收集器内部如何运行,如何切换到G1收集器,以及打开操作日志的选项。

文章原文来源于Oracle官网,提供的一篇关于G1垃圾收集器的文章Getting Started with the G1 Garbage Collector

介绍

本OBE涵盖了Java中Java虚拟机(JVM)G1垃圾收集(GC)的基础知识。 

在OBE的第一部分中,提供了JVM的概述以及垃圾收集和性能的介绍。 

接着将了解到CMS收集器如何与Hotspot JVM配合使用。

然后介绍Hotspot JVM垃圾收集的工作原理。 

紧接着将了解到影响G1垃圾收集工作的命令行选项。 

最后,将了解用于记录G1收集器日志的选项。

硬件和软件要求

以下是硬件和软件要求列表:

运行Windows XP或更高版本,Mac OS X或Linux的PC。 

Java 7 Update 9或更高版本。

最新的Java 7演示和示例Zip文件。

请注意,此操作已在Windows 7中完成,尚未在所有平台上进行测试。 但是,一切都应该在OS X或Linux上正常工作。 首选多核处理器的计算机。

先决条件

在开始本教程之前,下载并安装最新版本的Java JDK(JDK 7 u9或更高版本)。下载并安装Demos and Samples zip文件,解压并将内容放在目录中。

Java Technology and the JVM

 

Java概述

Java是一种编程语言和计算平台,由Sun Microsystems于1995年首次发布。它是支持Java程序(包括实用程序,游戏和业务应用程序)的底层技术。

Java在全球超过8.5亿台个人计算机上运行,并在全球数十亿台设备上运行,包括移动和电视设备, Java由许多关键组件组成,这些组件作为一个整体构成了Java平台。

Java 运行时版本

下载Java时,将获得Java Runtime Environment(JRE)。 JRE由Java虚拟机(JVM),Java平台核心类和Java平台库组成。这三部分都是在计算机上运行Java应用程序所必需的。 

使用Java 7,可以运行桌面应用程序、Java Web Start,浏览器中的Web Embedded应用程序(JavaFX)。

Java编程语言

Java是一种面向对象的编程语言,包括以下功能:

1、平台独立性

Java应用程序被编译为字节码,字节码存储在类文件中并加载到JVM中。由于应用程序在JVM中运行,因此可以在不同的操作系统和设备上运行。

2、面向对象

Java是一种面向对象的语言,借鉴了C和C ++的诸多功能并对其进行了改进。

3、自动垃圾收集

Java自动分配/释放内存,因此程序不用承担管理内存的任务。

4、丰富的功能库

Java包含大量预制对象,可用于执行输入/输出,网络和日期操作等任务。

Java开发工具包

Java Development Kit(JDK)是一组用于开发Java应用程序的工具集合。 使用JDK,可以编译用Java语言编写的程序,并在JVM中运行。此外,JDK还提供用于打包和分发应用程序的工具。

JDK和JRE共享Java应用程序编程接口(Java API),Java API是开发人员创建Java应用程序的功能类库。 Java API通过提供工具来,完成许多常见的编程任务(包括字符串操作,日期/时间处理,网络和实现数据结构(例如,列表,映射,堆栈和队列)),使开发更容易。

Java虚拟机

Java虚拟机(JVM)是​​一种抽象的计算机。 JVM是一个程序,但对于运行在其中的程序而言,它看起来像一台机器。这样,就有了一系列的Java接口和程序库。

JVM针对每个操作系统都有特定的实现,将Java程序指令转换为,可以在本地操作系统上运行的指令/命令。这样,Java程序就实现了跨平台的特性。

第一个Java虚拟机的原型由在Sun公司实现完成,用于模拟Java虚拟机指令集,该指令集运行在一个手持设备上(类似于现在的个人数字助理(PDA)的手持设备)。

Oracle目前提供了运行在手机、桌面电脑和服务器上的Java虚拟机,但Java虚拟机不作任何特定的技术实现,如硬件、操作系统等,它不针对某方面的解释器,但它可以被实现成直接输出CPU指令集,也可以用微代码实现或直接用CPU指令实现。

Java虚拟机并不知道Java编程语言的存在,只知道特定的二进制格式(即class格式文件)。class文件包含Java虚拟机指令(或字节码)和符号表,以及辅助信息。出于安全考虑,Java虚拟机对class文件中的虚拟机指令(或字节码),在语法和结构上都有着很严格的约束。因此,任何符合规范的class文件,都可以运行在Java虚拟机上,而不在意class文件是由哪种语言编译而来。

跨平台的通用特性是Java虚拟机的魅力所在,其他语言可以将Java虚拟机作为其语言的交付工具。

Exploring the JVM Architecture(探索 JVM 体系结构)

Hotspot Architecture(Hotspot体系结构)

HotSpot JVM的架构除了拥有强大的基础功能,还支持实现高性能和大规模的扩展。 例如,HotSpot JVM JIT编译器生成动态优化。他们在Java应用程序运行时,做出优化决策,并生成针对底层系统架构的高性能机器指令。通过对运行时环境,和多线程垃圾收集器持续的改进,HotSpot JVM拥有了高度可扩展性。JVM的主要组件包括类加载器,运行时数据区和执行引擎。

HotSpot-JVM-Architecture

Hotspot 关键组件

与性能相关的JVM的关键组件,在下图中突出显示。

HotSpot-JVM-Architecture-Key-Component

在JVM调优时需要关注的三个组件

堆是存储对象数据的地方,该区域由JVM启动时,选择的垃圾收集器进行管理,大多数JVM调优都是调整堆大小,并根据实际情况选择合适的垃圾收集器。 JIT编译器对性能有很大的影响,但是很少需要对最新版本的JVM进行这方面的调优。

Performance Basics(性能基础知识)

通常针对Java应用程序调优时,需要关注的两个目标是:响应速度和吞吐量,但二者只能取其一, 随着教程的深入,将回顾这些概念。

Responsiveness

Responsiveness是指应用程序,或系统响应的速度。 例如:

桌面UI响应事件的速度有多快,网站返回页面的速度有多快,返回数据库查询的速度有多快,对于关注Responsiveness的应用程序,太长的暂停时间是不可接受的。

Throughput(吞吐量)

Throughput的侧重点在于单位时间内,应用程序能处理的最大作业数量。测量吞吐量的例子包括:

在给定时间内完成的交易数量。批处理程序在一小时内完成的作业数。在一小时内完成的数据库查询数。对于关注吞吐量的应用程序,高暂停时间是可以接受的。由于高吞吐量应用程序,在较长时间内关注于基准测试,因此不考虑快速响应的时间。

The G1 Garbage Collector

G1垃圾收集器

Garbage-First(G1)收集器是一种为服务器设计(server-style)的垃圾收集器,针对拥有大内存及多核处理器的服务器,它可以满足对垃圾收集(GC)暂停时间要求较高的场景,同时实现高吞吐量。Oracle JDK 7 Update 4及更高版本已全面支持G1垃圾收集器。

G1收集器专为以下应用而设计:

可以与应用程序线程(如CMS收集器)并发操作。紧凑的内存空间,没有冗长的GC暂停时间。可预测的GC暂停时间。不以牺牲吞吐量为代价。较大的堆空间不再是必需的标配。G1已成为Concurrent Mark-Sweep Collector(CMS)的长期替代者。

G1与CMS之间的差异

G1给出了更好的解决方案,另一个区别在于G1是压缩收集器,G1通过压缩整理堆空间,从而缩短创建对象时,分配内存所用的时间,不再根据内存区域而区别对待,这大大简化了收集器,消除了潜在的碎片问题。此外,G1在垃圾收集暂停方面,提供了比CMS收集器更多的可预测性,允许用户定义期望的暂停时间。

G1 Operational Overview

旧的垃圾收集器(串行,并行,CMS)都由三部分组成:年轻代,年老代和空间大小固定的永久代。所有内存对象都会在这三个分代的其中一个区域结束生命。

Heap-Structure

G1收集器采用了不同的方法

堆被分割成一系列大小对等的区间,每个区域都是一块连续的虚拟内存。 某一些区域给贴上了原有的角色标签(eden,survivor,old),但它们没有固定的大小。 这为内存管理提供了更高的灵活性。

G1收集器执行垃圾收集时,G1以类似于CMS收集器的方式运行。 G1在标记阶段以并发模式,对堆中各处活跃对象进行标记。标记阶段完成之后,G1知道哪些区域大部分是空的,它会优先收集这些区域,通常这样会获得大量可用空间。 这也是为什么这种垃圾收集方式,被称为Garbage-First的原因。

Garbage-First的原因

顾名思义,G1将收集和压缩集中在,那些充满了可回收对象的活跃区域。 G1使用暂停预测模型,来选择需要收集的区域个数,以满足用户自定义暂停时间的需求。

G1对填满的成熟区域,使用 evacuation[策略] 收集垃圾。G1把一个或多个区域中的对象,复制汇集到单个区域中,这个过程会压缩和释放内存。evacuation[策略]能够在多处理器上并行执行,从而减少暂停时间提高吞吐量。 因此,G1会在用户规定的暂停时间内,持续的进行垃圾收集以减少碎片。

这种方式远超以往的收集方法。 CMS(Concurrent Mark Sweep)垃圾收集器不进行压缩。ParallelOld垃圾收集仅执行整堆压缩,这会导致相当长的暂停时间。值得注意的是,G1不是实时收集器。它会尽可能的满足自定义的暂停时间,但不绝对保证。

G1根据先前垃圾回收集时的数据,评估在满足用户设定的暂停时间内,可以对多少区域进行回收。因此,收集器对需要回收的区域会有一个相当准确的成本模型,并使用该模型来评估,在允许的暂停时间内,可以回收的区域和数量。

注意:G1具有并发(与应用程序线程一起运行,标记,清理)和并行(多线程、暂停)阶段。Full GC 仍然是单线程的,但是如果调优得当,应用程序能够避免Full GC。

G1 Footprint

如果从 ParallelOldGC 或 CMS 收集器迁移到 G1, 则可能会看到更大的 JVM 进程。这主要与  "记帐(accounting)" 数据结构 (如Remembered Sets、Collection Sets) 有关。

Collection Sets或RSet

跟踪对应区域的对象引用,堆中每个区域都有一个RSet。 RSet 实现了一个区域的并行和独立收集。RSets产生的影响占比,不超过总体的5%。

CollectionCollection Sets或CSets

在GC期间用于收集区域中的存活数据,所有的存活数据都被放入一个CSet中被撤离(复制/移动)。这些数据集合可以是伊甸区,幸存区和/或老年代。 CSets占JVM大小不到1%。

Recommended Use Cases for G1

G1的第一个重点是对运行在大内存上,对GC延迟有严格要求的应用程序提供的解决方案。 这表示堆大小约为6GB或更大,稳定且可预测的暂停时间低于0.5秒。如果当下正在运行的应用程序,具有一个或多个以下特征,那么将CMS或ParallelOldGC切换到G1将会从中受益。

1、Full GC持续的时间太长或太频繁。

2、目标分配率或提升率(对象从一个代晋升到另一代)差异很大。

3、不希望垃圾收集时间太长(超过0.5到1秒)。

注意:如果应用程序使用CMS或ParallelOldGC,垃圾回集没有出现过长的暂停时间,那么仍可以使用当前的垃圾收集器。 更改为G1收集器不一定要需要使用最新版的JDK。

Reviewing GC with the CMS

历代GC和CMS回顾

1、并发标记扫描(CMS)收集器(也称为并发低暂停收集器)最终版。 
2、通过与应用程序线程,并行执行垃圾收集的大部分工作,来最小化由于垃圾收集而产生的暂停。
3、通常,并发低暂停收集器不会复制或压缩存活对象。 
4、无需移动活动对象,即可完成垃圾收集。 如果碎片成为问题,那就分配更大的堆。

CMS Collection Phases

CMS收集器在堆的年老代上执行以下阶段:

阶段与描述

(1)初始标记
    (执行暂停)年轻一代可达对象被“标记”为年老代的对象。相对于minor collection来说暂停时间通常持续较短。

(2)并发标记
    遍历对象生成可达路线。从标记的对象开始扫描,从根节点开始找寻可达的对象并标记。
    
(3)重新标记
    (执行暂停)查找并发标记阶段遗漏的对象,在并发收集阶段而错过对Java对象的更新。

(4)Concurrent Sweep
    在标记阶段收集标识为不可达的对象。将对象加入到空闲列表中,以便以后进行分配。注意,存活的对象不会被移动。

(5)Resetting
    通过清除数据结构准备下一次并发收集。

Reviewing Garbage Collection Steps

接下来,逐步回顾CMS收集器的操作,CMS收集器的堆结构,堆被分成三个空间。年轻代分为新生区和交换区。 年老代是一个连续的空间。 对象收集就地完成。 除非有Full GC,否则不会进行压缩。

CMS收集器的堆结构

How Young GC works in CMS

年轻代是浅绿色,年老代是蓝色。 如果应用程序已运行一段时间,CMS看起来就是这样子的。老年代区域里的对象散落一地。

CMS-Collection

使用CMS,年老代的对象被就地释放。 他们不会被到处移动。 除非Full GC,否则不会压缩空间。

Young Generation Collection

幸存对象从新生区和交换区被复制到另一个交换区。 任何达到晋升阈值的老对象都将被放入年老代。

After Young GC

年轻代在经历GC后,新生区被清空,其中一个交换区也会被清空。新晋升的对象在图上以深蓝色显示。 绿色的是年轻代交换区中的对象,还未被提升到年老代。

After Young GC

Old Generation Collection with CMS

两次暂停发生在:初始标记和重新标记阶段,当年老代达到一定的比例时,CMS就会被启动。

(1)初始标记暂停阶段是短暂的,其中标记了活动(可到达)对象。

(2)并发标记在应用程序执行的同时找到活动对象。

最后,在(3)重新标记阶段,查找在(2)阶段并发标记时错过的对象。

Old Generation Collection with CMSOld Generation Collection - Concurrent Sweep

在前一阶段未标记的对象,将被就地释放并且不会对堆空间进行压缩。Note: Unmarked objects == Dead Objects(未被标记的对象就是所谓的垃圾)

Old Generation Collection - Concurrent Sweep

Old Generation Collection - After Sweeping

在(4)清除阶段之后,可以看到大量的内存被释放。 需要注意是这些内存空间并没有进行压缩。

Old Generation Collection - After Sweeping

最后,CMS收集器将回到(5)重置阶段,等待下一次GC的到来。

The G1 Garbage Collector Step by Step

G1收集器采用不同的方式来分配堆空间,通过下图逐步了解G1。

G1 Heap Structure

一个完整的堆内存空间被拆分为若干个固定大小的区域。

G1 Heap Structure

区域的大小可以在JVM启动时调整。 JVM通常由大约2000个,大小在1~32Mb间的内存块组成。

G1 Heap Allocation

实际上,这些内存块会被映射为逻辑上的新生代、年轻代、年老代。

G1 Heap Allocation

图中的颜色代表了那个区域对应的角色。 存活的对象从一个区域撤离(即,复制或移动)到另一个区域。区域块设计初衷是为了并行收集,并在不暂停应用线程的前提下进行垃圾收集工作。

如图所示,区域块可以分配给新生代、年轻代、年老代。此外,还有第四类用于装大对象的区域。 这些区域用于容纳,那些大于标准区域50%的超大对象,它们使用一组连续的区域来存储对象。最后一种类型会用堆中未被使用的区域。

提示:在撰写本文时,尚未对超大对象的收集做出优化。 因此,应该尽量避免创建此类大小的对象。

Young Generation in G1

堆被分成大约2000个区域,每个区域最小为1Mb,最大为32Mb。 蓝色区域存储的是年老代对象,绿色区域存储的是年轻代对象。新的收集器不要求这些块区域是连续的。

Young Generation in G1

A Young GC in G1

将存活对象撤离(即,复制或移动)到一个或多个幸存区域。如果满足年老代的阈值,则将这些对象提升到年老代区域。这是一个(STW)暂停阶段。为下一次年轻代的GC做准备,通过计算新生代和年轻代的大小。 “报告”信息帮助计算这个值。 暂停时间也会被考虑在内。

A Young GC in G1

End of a Young GC with G1

存活的对象已被移动到交换区或年老代。

End of a Young GC with G1

最新晋级的对象为深蓝色,交换区为绿色。

总之,以下在G1中都称之为年轻代:

1、堆是由单个内存区域所组成。

2、年轻代内存由一组非连续区域组成,这样可以在需要时轻松调整大小。

3、年轻代的垃圾收集器(或年轻代的GC),会引发暂停。此操作会暂停应用程序所有的线程。

4、年轻代的GC使用多个线程并行完成。

5、存活对象被复制到新的交换区或年老代区域。

Old Generation Collection with G1

像CMS收集器一样,G1收集器设计之初是为了,在回收年老代对象时实现低暂停,下表是G1在回收年老阶段的过程描述。

G1 Collection Phases - Concurrent Marking Cycle Phases

G1收集器在对堆中年老代执行回收时,各阶段如下所示。提示:有些阶段同样适用于年轻代。

阶段与描述

(1)初始标记(暂停)
    这个期间G1会暂停整个应用,这个动作伴随着对年轻代,进行垃圾回收时就会触发。标记为年轻代的区域(从根区域),所引用的对象有可能在年老代。

(2)根区域扫描
    扫描交换区中引用了年老代中的对象。这个动作发生在应用程序运行时,必须在年轻代GC发生之前完成该阶段。

(3)并发标记
    在整个堆上查找存活对象。这个期间应用程序处在运行状态。这个阶段有可能暂停年轻代垃圾收集工作。

(4)重新标记(暂停应用)
    对堆中存活的对象进行标记。使用一种叫做snapshot-at-the- start (SATB)的算法,比CMS收集器中用的算法要快得多。

(5)清理(暂停应用并且并发执行)  

    对存活的对象和完全释放的区域进行”记录“。    清理内存管理中的辅助数据。    把空闲的区域加入空间列表(以供创建对象时分配内存)

(*)复制(暂停应用程序)
     把存活对象复制或搬运到新的未使用的区域,这个过程,年轻代区域的日志记录为[GC pause (young)]。包含年轻代和年老代的日志为 [GC Pause (mixed)].

G1 Old Generation Collection Step by Step

这个部分描述G1如何对年老代进行管理的

Initial Marking Phase

初始化标记的存活对象,是基于年轻代的垃圾回收基础上。日志中的提示为GC pause (young)(inital-mark).

Initial Marking Phase

Concurrent Marking Phase

如果找到的是空区域(由“X”表示),在重新标记阶段,则立即将它们移除。 并计算“记账”信息(确定活跃度)。

Concurrent Marking Phase

Remark Phase

空区域被移除并回收,现在计算所有区域的活跃度。

Remark Phase

Copying/Cleanup Phase

G1会选择“活跃度”最低的区域,这些区域可以被快速完成回收。 这一点可以通过 日志[GC pause (mixed)]看到。 因此,年轻代和年老代的回收是同时进行的。

Copying/Cleanup Phase

After Copying/Cleanup Phase

那些已经被回收并压缩的区域,在图中被标记为深蓝色和深绿色。

After Copying/Cleanup Phase

限于篇幅的原因,会在下一篇文章《Java G1内存回收系列第二章》中继续讨论这个话题。