【浅度渣文】JVM——G1收集器

来源:互联网 发布:it服务外包最大 编辑:程序博客网 时间:2024/05/17 18:42

原文链接:http://www.dubby.cn/detail.html?id=9059

1. 概述

硬件和软件要求

  • 操作系统要求Windows XP或者更高,Mac OS X和Linux都可以。请注意,这些测试操作是在Windows 7上完成的,尚未在所有平台上进行测试。 但是,一切都应该在OS X或Linux上正常工作。 当然,你的机器有一个以上的核心就更好了。
  • Java 7 Update 9或者更高版本。
  • 最新的Java 7 Demos和示例Zip。

准备内容

  • 安装好Java 7u9 或者更高版本。
  • 从官网下载下来示例代码,解压后,比如放在C:\javademos下。

2. Java和JVM

Java预览

Java是Sun Microsystems在1995年首次发布的编程语言和计算平台。它是支持Java程序(包括通用工具,游戏和商业应用程序)的基础技术。 Java运行在全世界超过8.5亿台个人计算机上,并在全球数十亿台设备上运行,包括移动和电视设备。 Java由许多关键组件组成,总体而言,它们共同组成了Java平台。

Java运行时版本

当你下载Java时,你就已经获得了Java运行时环境(JRE)。JRE是由Java虚拟机(JVM),Java核心类库和辅助性的Java类库组成。如果你想要在你的电脑上运行Java程序,那么这三个组成部分都是需要安装好的。使用Java 7的时候,你可以在操作系统上运行Java应用程序,或者使用Java Web Start从Web上安装然后运行Java应用程序,或者作为一个Web嵌入式应用程序运行在浏览器里(JavaFX)。

Java编程语言

Java是一个面向对象的编程语言,有下面这些特性。

  • 平台无关性——Java应用被编译成字节码,存储在class文件里,然后被JVM加载。由于Java应用是运行在JVM中,而不是直接运行在操作系统上,所以他们可以运行在各个操作系统上。(译者:也就是一次编写,到处运行,JVM帮我做了平台兼容,当然,不可能真的平台无关性)
  • 面向对象——Java吸收了C和C++的很多特性,并做了一些优化。
  • 自动垃圾回收——Java会自动分配和释放内存,程序员不会有负担。(译者:但是多了了解GC机制的负担,不然你也不会看这篇文章了)
  • 丰富的标准库——Java拥有很多预先设计好的类,我们可以直接用,比如:输入输出,网络,日期等等。

JDK

Java开发工具包(JDK)是开发Java应用所需的一系列的工具包。有了JDK,你可以编译你用Java写的程序,并且运行。除此之外,JDK还提供了打包和分发应用程序的工具。

JDK和JRE公用了Java应用程序接口(Java API)。Java API是预先打包好的类库,开发者可以直接使用。Java API让开发者的开发工作变得更简单,比如:string的处理,时间的处理,网络,各种数据结构的集合(例如:lists, maps, stacks, and queues)。

JVM

Java虚拟机(JVM)是一个抽象的计算机。 JVM是一个看起来像一个计算机的程序,可以执行写入到JVM中的程序。 这样,Java程序就被写入到同一组接口和库中。 针对特定操作系统的每个JVM实现,将Java编程指令转换为在本地操作系统上运行的指令和命令。 这样,Java程序就实现了平台独立性。

Sun公司完成的Java虚拟机的第一个原型实现,模拟了由类似当代个人数字助理的手持设备托管的软件中的Java虚拟机指令集。 Oracle的当前虚拟机实现了移动端,桌面和服务器设备上的Java虚拟机。但Java虚拟机不承担任何特定的实现技术,主机硬件或主机操作系统。 它没有一个固有的解释,(只是一个规范),你也可以通过将其指令集编译为硅CPU来实现。 它也可以用微码或直接用硅来实现。

Java虚拟机对Java编程语言一无所知,只知道特定的二进制格式,即类文件格式class。 类文件包含Java虚拟机指令(或字节码)和符号表以及其他辅助信息。

为了安全起见,Java虚拟机对类文件中的代码施加了强烈的语法和结构限制。 但是,Java虚拟机可以托管任何具有可以用有效的类文件表示的功能的语言。正因如此,很多其他语言的实现者,为了享受JVM带来的遍历,他们可以把自己的语言编译成class文件交给JVM来执行。

探索JVM架构

Hotspot的架构

HotSpot JVM拥有支持强大功能和功能的基础架构,并支持实现高性能和大规模可扩展性的能力。 例如,HotSpot JVM JIT编译器会生成动态优化。 换句话说,他们在Java应用程序运行时做出优化决策,并生成针对底层系统体系结构的高性能本地机器指令。 此外,通过其运行时环境和多线程垃圾收集器的成熟发展和持续工程,HotSpot JVM即使在最大的可用计算机系统上也具有很高的可扩展性。

JVM的主要组件包括类加载器,运行时数据区和执行引擎。

Hotspot的关键组件

下图高亮显示了与性能相关的JVM的关键组件。

在调整性能时,JVM有三个重点关注的组件。 堆是你的对象数据存储的地方。 这个区域由启动时选择的垃圾收集器管理。 大多数调优选项都是针对堆的大小以及为您的情况选择最合适的垃圾收集器。 JIT编译器对性能也有很大的影响,但很少需要使用较新版本的JVM进行调优。

性能基础

通常,在调整Java应用程序时,重点是两个主要目标之一:响应性或吞吐量。 随着教程的进展,我们将回顾这些概念。

响应性

响应性指的是一个应用程序或者一个系统可以多快的响应一个请求。举个例子:

  • 桌面应用响应UI事件(点击,滑动等)的速度。
  • 一个网站返回页面的速度。
  • 数据库查询结果返回的速度。

对于一个关注响应性的应用,是不能接受长时间停顿的。优化的目标一般是加快响应速度。

吞吐量

吞吐量关注的是在一定时间内,应用程序或系统可以完成的工作量。举个例子:

  • 给定时间,完成的事物数量。
  • 一个小时内,一个批处理可以完成的job数量。
  • 一个小时内,数据库可以完成的查询量。

长时间的停顿对于关注吞吐量的应用来说,是可以接受的。因为关注的是一个更长时间的的工作效率,而不是尽快结束一个请求。

3. G1收集器

G1收集器(Garbage-First Collector)是一个适合服务端,多处理器,大内存的场景。G1收集器可以很大概率的满足预期的停顿时间,同时实现高吞吐。G1收集器在JDK 7 update 4之后就已经支持了。G1收集器设计主要用于以下应用:

  • 可以与CMS收集器等应用程序线程同时运行。
  • 在较短的停顿时间内,完成空闲内存碎片的整理。
  • 需要更可预测的GC暂停持续时间。
  • 不想牺牲过多的吞吐量。
  • 不需要更大的Java堆(译者:可参考复制算法)。

G1计划作为并发商标扫描收集器(CMS)的长期替代品。 比较G1和CMS,有一些差异使得G1成为更好的解决方案。 一个区别是G1是一个压缩算法的实现。 G1充分压缩空间以避免使用细粒度的自由列表进行分配,而是依赖于区域。 这大大简化了收集器的实现,并且大部分消除了潜在的碎片问题。 此外,G1提供比CMS收集器更多的可预测的垃圾收集暂停,并允许用户指定所需的暂停目标。

G1概述

之前的垃圾收集器(serial, parallel, CMS)都会把堆构造成三个区域:新生代,老年代,永久代。

所有的对象都在在其中一个块里死亡。

而G1收集器采用一个不一样的方式来划分堆内存。

堆被分割成一组相等大小的堆区域,每个区域都是连续的虚拟内存范围。 每个区域被分配成eden, survivor或者old,但是他们没有固定的大小。 这提供了更大的内存使用灵活性。

在执行垃圾收集时,G1的运行方式类似于CMS收集器。 G1执行一个并发的全局标记阶段来确定整个堆中对象的活性。 标记阶段完成后,G1知道哪些区域大部分是空的。 它首先收集这些地区,这往往产生大量的自由空间。 这就是为什么这种垃圾收集方法称为垃圾优先。 顾名思义,G1将其收集和压缩活动集中在可能充满可回收对象的堆的区域,即垃圾。 G1使用暂停预测模型来满足用户定义的暂停时间目标,并基于指定的暂停时间目标选择要收集的区域的数量。

由G1标记的回收时机成熟的区域就是要被回收的垃圾。 G1将对象从堆的一个或多个区域复制到堆上的单个区域,并在此过程中压缩并释放内存。 这种撤离在多处理器上并行执行,以减少暂停时间并提高吞吐量。 因此,对于每个垃圾收集,G1不断地减少碎片,在用户定义的暂停时间内工作。 这超出了以前的两种方法的能力。 CMS(并发标记扫描)垃圾收集器不会执行压缩。 ParallelOld垃圾收集只执行全堆压缩,导致相当多的暂停时间。

请注意,G1不是实时收集器。 它以高概率满足设定的暂停时间目标,但不是绝对确定的。 根据以前收集的数据,G1会估算在用户指定的目标时间内可以收集多少个区域。 因此,收集者具有相当准确的收集区域成本的模型,并且使用该模型来确定在停留时间目标内停留时收集哪些区域和收集多少区域。

注意:G1具有并发(与应用程序线程一起运行,例如细化,标记,清除)和并行(多线程,例如stop the world)阶段。 Full GC仍然是单线程的,但是如果调整得当,应用程序应该可以避免Full GC。

G1 的内存占用

如果你是从ParallelOldGC或者CMS迁移到G1的话,你会发现,你似乎拥有了一个更大内存。这主要与“统计”数据结构有关,例如Remembered Sets和Collection Sets。

Remembered Sets或者RSets追踪对象应用在哪里区域里。每个堆的区域都有一个TSet。RSet可以并行的,独立的手机一个区域的对象引用。RSets的内存占用少于5%。

Collection Sets或者CSets将会在一个GC中被回收。所有活着的对象会被疏散(copied/moved)。CSets可以是Eden, survivor和old generation。CSets对内存的占用少于1%。

推荐使用G1的场景

G1的第一个关注点就是为运行应用程序的用户提供一个解决方案,这些应用程序需要能保证有限GC延迟,并且是个大堆。 这意味着堆大小约6GB或更大,稳定可预测的暂停时间低于0.5秒。

现在使用CMS或者ParallelOldGC垃圾收集器运行的应用程序如果应用程序具有以下一个或多个特性,将有益于切换到G1。

  • Full GC持续时间太长或太频繁。
  • 对象分配率或提升率明显不同。
  • 不想要长时间GC停顿(超过0.5到1second)

注意:如果你使用的是CMS或者ParallelOldGC,并且你的应用也没有经历过长时间的GC停顿,你完全可以保持不变(译者:不需要为了用G1还来折腾自己,何必呢)。就算不使用G1收集器,你依然可以使用最新的JDK。

4. 复习CMS收集器

回顾分代GC和CMS

并发标记扫描(CMS)收集器(也称为并发低暂停收集器)收集终身代。 它试图通过与应用程序线程同时执行大部分垃圾收集工作来尽量减少由于垃圾收集造成的暂停。 通常情况下,并发的低暂停收集器不会复制或压缩活动对象。 垃圾收集完成时不移动活动对象。 如果碎片成为问题,请分配一个更大的堆。

注意:年轻一代的CMS收集器使用与并行收集器相同的算法。

CMS的收集阶段

CMS在收集老年代时,会执行下面的步骤:

阶段 描述 1、初始化标记(Stop the World) 老一代的对象被“标记”为可达,包括年轻一代可能到达的对象。停顿时间一般较短。 2、并发标记 在应用程序线程执行时,并发的遍历老年代对象,生成可达对象的对象图。这个可达性分析在阶段2,3,5都会执行,并且被扫描到的对象都会被立即标记成活着。 3、重新标记(Stop the World) 查找并发标记阶段错过的对象,也就是在收集器完成了对对象的跟踪后,然后Java应用程序线程更新的对象。 4、并发清除 收集那些在标记阶段已经被标记为不可达的对象。死亡对象会被添加到Free List中,以供后续分配使用。在这个时候可能会对死对象进行合并。注意,不会移动活着的对象。 5、重置 清空这一次收集的统计信息,为下次收集做准备。

复习垃圾回收的步骤

1. CMS的堆结构

堆被拆成3个部分。

新生代被拆成Eden和两个suvivor区域。老年代是一个连续的空间。一般情况下不会进行对象整理(译者:整理内存碎片),除非是进行一次Full GC。

2. Young GC怎么工作

新生代被标记成绿色,老年代是蓝色(译者:希望你不是蓝绿色盲)。如果你的应用程序已经运行了一段时间之后,你的虚拟机内存看起来应该是这个样子。在老年代,内存是很分散的。

使用CMS时,老年代的对象会在适当的时候被回收掉,再次强调,除非进行一次Full GC,否则不会整理活着的对象的。

3. 新生代收集

活着的对象会从Eden区和suvivor被复制到另一个suvivor区。如果对象的年龄已经达到了阈值,就会晋升到老年代。

4. Young GC之后

在一次Young GC之后,Eden区和其中一个suvivor会被清空。

图中,深蓝色的是刚刚从新生代晋升到老年代的对象。新生代中绿色的对象是还没有达到晋升条件的对象(译者:突然感觉我们就是一个个对象,如果没有被回收,熬啊熬,就会晋升,哈哈~)。

5. CMS老年代收集

有两个阶段会Stop the World:初始标记,重新标记。当老年代的对象空间占用量达到一个阈值,CMS就拉开帷幕了。

(1)初始标记会有一个短暂的停顿,用来标记可达对象。(2)并发标记阶段是在应用程序执行时,并发的标记活着的对象。然后是(3)重新标记,找到(2)阶段遗漏的活着的对象。

6. 老年代收集——并发清除

释放掉之前几个阶段都没有标记的对象,不会整理内存。

注意:未标记对象 == 死对象

7. 老年代收集——清除之后

在阶段(4)收集之后,你可以看到很多对象都被释放了。你也可以注意到内存碎片现象还是存在。(译者:我实在是受不了了,这句话已经出现几万次了)

然后CMS完成(5)重置工作,等待着下一次GC的到来。

5. 一步一步走近G1

G1收集器分配堆内存和以往的不一样了。

1. G1堆结构

堆内存是一个被拆分成很多固定大小的内存区域。

每个区域的大小是JVM启动时决定的。JVM通常会化成出2000个区域,每个区域大小是1 ~ 32Mb。

2. G1 内存分配

每个小的区域代表Eden,suvivor或者old。

图片上的颜色展现了,每个区域代表的意义。收集时,会把活的对象从一个区域转移到另一个区域。每个区域可以并行(Stop the World)或者不并行的收集。

每个小的区域可以代表Eden,suvivor或者old。除此之外,还有第四种类型的区域,用来存储大对象。一般是大小超过单个区域50%的对象会被分配到第四种区域里。这第四种区域是连续的很多个区域的集合。第四种区域就是我们看到的未分配的区域。

注意:在写这篇文章的时候,大对象收集还没有最优化,所以,建议尽量避免这种大对象的分配。

3. G1 中的新生代

堆内存被拆分成2000个小区域,大小最小是1Mb,最大是32Mb。蓝色代表老年代,绿色代表新生代。

注意:不需要和以前的收集器一样,把新生代,来年代分配在连续的内存上,在G1下,是新生代和老年代是可以分散的。

4. G1 中的 Young GC

活着的对象会被转移(复制/移动)到另一个或多个suvivor区域。如果年龄到了阈值,就会被分配到Old区域。

这个过程是Stop the World的。这个过程会统计很多信息,比如Eden大小,suvivor大小,还有这次收集的停顿时间等等,这是为了下一次收集做准备。

这种方式,可以很容易的resize(重新定义大小)各个区域的大小。

5. G1 的 Young GC之后

活着的对象被转移到其他suvivor或者old区域了。

总结一下,G1的Young GC的特点:

  • 堆被拆分成多个区域。
  • 新生代有一些并不连续的区域组成。这样可以很容易的扩容或收缩新生代的大小。
  • Young GC会Stop the World。
  • Young GC是多线程并行的。
  • 活着的对象会被复制移动到suvior或者old区域。

G1的老年代收集

和CMS一样,G1也是被设计成一款低停顿的GC收集器。下面的表格描述了G1的老年代收集阶段。

G1收集阶段 —— 并发标记循环阶段

G1的老年代收集步骤如下,请注意,其中有一些步骤是Young GC的一部分。

阶段 描述 1、初始标记(Stop the World) 这会Stop the World。他会搭着Young GC的顺风车,顺便标记那些新生代(根区域/root regions)可以引用到的老年代中的对象。 2、根区域扫描 扫描新生代,找到老年代中哪些对象被新生代中的对象引用。这个阶段不会中断应用程序的执行。这个阶段必须在Young GC发生之前完成。 3、并发标记 找到整个堆中的活着的对象。这个和应用程序并发执行。但是,这个阶段是可能被Young GC中断的。 4、重新标记(Stop the World) 完成活对象的标记。使用SATB算法(snapshot-at-the-beginning)(这个算法比CMS使用的算法快很多) 5、清除(Stop the World也是并发) 1.统计活对象和完全空闲的区域(Stop the World);2.清空RSets(Stop the World);3.重置空闲区域,并且回收到Free List上(并发) *、复制(Stop the World) Stop the World,把活着的对象复制移动到新的未使用的区域。如果只疏散了新生代,那么日志是GC pause (young),如果新生代和老年代都疏散了,日志记为GC Pause (mixed)

我们大约了解了各个阶段的定义,现在我们来仔细看看每一步究竟是干什么的。

6. 初始标记阶段

初始标记是搭着Young GC的顺风车一起执行的,看GC日志的话,是GC pause (young)(inital-mark)

7. 并发标记阶段

如果有空区域(标记为”X”,也就是里面的对象都死了)被发现,那么就在重新标记阶段直接移除。同样的,这些信息也会被统计,用来优化下一次GC。

8. 重新标记阶段

空区域会被直接移除回收。并且计算出所有区域的对象的活跃度(liveness)。

9. 复制/清除阶段

G1收集器会选择对象活跃度最低的区域进行收集。新生代和老年代同时被回收。这种情况下,GC日志是GC pause (mixed)。这样,新生代和老年代同事被回收了。

10. 复制/清除阶段之后

选中的区域被回收,并压缩之后,就是图中深蓝色的和深绿色的。

总结老年代GC

G1的老年代GC的特点是:

  • 并发标记阶段
    • 在应用程序运行时,并发的计算出各个区域的活跃度。
    • 根据活跃度判断出哪些区域是最值得回收的。
    • 没有类似CMS的清除阶段。
  • 重新标记阶段
    • 使用Snapshot-at-the-Beginning (SATB) 算法,这个算法比CMS的算法更高效。
    • 完全空的区域会被回收。
  • 复制/清除阶段
    • 新生代和老年代同时被回收。
    • 老年代的选择是根据活跃度来确定的。

6. 命令行选项和最佳实践

基本的命令行

为了使用G1收集器,我们需要使用-XX:+UseG1GC

这里我们用demo来演示(首先你需要进入你demo的目录demo/jfc/Java2D下),

java -Xmx50m -Xms50m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar  Java2Demo.jar

主要参数介绍

-XX:+UseG1GC——告诉JVM使用G1收集器。
-XX:MaxGCPauseMillis=200——设置一个最大停顿时间。这是个软目标,也就是JVM会尽最大的努力去满足你的目标(译者:实在满足不了,你也拿他没办法)。因此,有时候可能无法达到你的要求。默认值是200ms。
XX:InitiatingHeapOccupancyPercent=45——堆总量使用比例到达这个值时,开始一趟GC。是堆总量的占用比例,而不是某一个代的占用比例。0代表一直循环执行GC,默认值是45。

最佳实现

这里有一些使用G1的最佳实践的建议。

不要设置新生代的容量

如果你使用-Xmn来指定新生代大小,干预G1的行为(译者:G1就会很生气,后果很严重)。

  • G1将不会遵从你预期的停顿时间,也就是说,这个选项会关闭-XX:MaxGCPauseMillis
  • G1将不能动态的扩展和收缩你的新生代,因为已经指定了。

使用响应时间来作为标准

不要使用平均响应时间来设置XX:MaxGCPauseMillis=<N>,考虑使用你期望的响应时间的90%甚至更高的值来设置。也就是说90%的用户(客户端/?)请求响应时间不会超过预设的目标值。因为,这个值只是一个目标值,并不能精确保证满足。

转移失败?

对 survivors 或 promoted objects 进行GC时如果JVM的heap区不足就会发生晋升失败(promotion failure)。堆内存不能继续扩充,因为已经达到最大值了。可以使用-XX:+PrintGCDetails,这样在转移失败时,会打印to-space overflow。这种操作很昂贵!

  • GC任然要继续,所以空间必须被释放。
  • 拷贝失败的对象必须放到合适的地方。
  • CSet区域中任何更新过的RSets都必须重新生成。
  • 所有这些操作代价都是很大的。

如何避免转移失败?

  • 增大堆内存。
    • 增大-XX:G1ReservePercent=n,默认是10.
    • G1使用一个保留的内存,创建出一个假的内存上限,当内存失败时,就会使用这个保留的内存。(译者:凡事留一线,日后好相见)
  • 更早的执行GC。
  • 使用-XX:ConcGCThreads=n来增加GC的执行线程。

完整的G1命令行选项

下面给出G1的完整命令行选项,使用时,请记住上面的最佳实践。

选项和默认值 描述 -XX:+UseG1GC 使用G1收集器 -XX:MaxGCPauseMillis=n 设置一个预期的停顿时间,记住这只是个软目的,JVM会尽力去实现 -XX:InitiatingHeapOccupancyPercent=n 启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示”一直执行GC循环”. 默认值为 45. -XX:NewRatio=n 新生代和老年代的大小比例(new/old),默认是2 -XX:SurvivorRatio=n eden/suvivor的比例,默认是8 -XX:MaxTenuringThreshold=n 对象晋升的年龄,默认是15 -XX:ParallelGCThreads=n 收集器并发阶段使用的线程数。默认值是取决于JVM运行的平台 -XX:ConcGCThreads=n 设置收集器的线程数。默认值是取决于JVM运行的平台 -XX:G1ReservePercent=n 设置G1保留内存,防止转移失败 -XX:G1HeapRegionSize=n G1收集器把堆内存细分成很多个大小一致的小区域。这个选项是设置每个区域的大小默认值是根据堆的总量,算出的。范围是1 Mb ~ 32 Mb
原创粉丝点击