从G1设计到堆空间调整

简介: 如果你在使用Java8,或者计划使用Java9,有很大可能是要么在评估G1垃圾收集器,要么已经在使用它。本文将从G1设计开始向您介绍系统介绍G1垃圾收集器如何工作,助您更加系统的学习了解G1。本文选自《Java性能调优指南》。

引言:如果你在使用Java8,或者计划使用Java9,有很大可能是要么在评估G1垃圾收集器,要么已经在使用它。本文将从G1设计开始向您介绍系统介绍G1垃圾收集器如何工作,助您更加系统的学习了解G1。
本文选自《Java性能调优指南》。

G1设计

  G1将Java堆分成多个分区。分区的大小可以依据堆的尺寸而改变,但必须是2的幂,同时最小为1MB,最大为32MB。由此得出可能的分区尺寸是1 MB、2MB、4 MB、8 MB、16 MB和32MB。所有分区的大小都一样,在JVM运行过程中它们的尺寸也不会发生变化。分区尺寸是基于Java堆内存的初始值和最大值的平均数来进行计算的,这样对于这个平均堆尺寸就会有2000个左右的分区。举个例子,对一个16G的Java堆使用-Xmx16g -Xms16g命令行选项,G1就会选择采用16GB/2000 = 8MB的分区尺寸。
  如果Java堆内存初始值和最大值相差很远,或者这个堆内存的尺寸非常大,很有可能就会产生远超过2000个的分区。类似地,若堆内存很小,那分区数量会远远小于2000。
  每个分区都有一个关联的已记忆集合(remembered set,该集合用来记录跟踪分区外指向分区内的引用,简称RSet),这样就避免了对整个堆的扫描,使得各个分区的GC更加独立。RSet总体大小有限,但也不容忽视,因此分区的数量对HotSpot的内存空间占用有直接的影响。RSet总体的尺寸严重依赖应用的行为。RSet最少时大概会占用1%左右的堆空间,最多时可能会达到20%。
  一个特定的分区一次只能用于一个目的,但一旦这个分区被包含进一次收集,它就会被彻底转移,同时被释放为一个可用分区。
  G1有多种类型的分区。可用分区是当前未被使用的。eden(新生代)分区组成了年轻代的eden空间,survivor(存活代)分区组成了年轻代的survivor空间。所有eden分区和survivor分区的总的集合,就是年轻代。eden分区或survivor分区的数量随着一次次的垃圾收集发生改变,包括年轻代收集、混合收集或者full收集。老年代分区由绝大部分老年代组成。最后,通常认为巨型分区是老年代的一个组成部分,它用来容纳那些大小达到或超过一个分区50%空间的对象。在JDK 8u40之前,巨型分区是作为老年代的一部分被收集的,但在JDK 8u40里,某些巨型分区是作为一个年轻代的一部分被收集的。本章后续还会提到更多关于巨型分区的细节。
  实际上,一个分区可以用于任何目的,也就是说没有必要把内存堆划分成相邻的年轻代段和老年代段。G1的启发式算法会估算年轻代需要多少个分区,以及按照指定的GC暂停时间估算目前还有多少分区要被回收。一旦应用开始生产对象,G1就选中一个可用分区并将它指定为eden分区,然后从中取出内存块交给Java线程。当这个分区满了之后,另一个未被使用的分区会再被指定为eden分区。这个操作会一直持续下去,直到达到eden分区的上限数量,就触发一次年轻代垃圾收集。
一次年轻代垃圾收集会回收所有年轻代分区,包括eden分区和survivor分区。这些分区里的所有存活对象都会被转移到另外一个新的survivor分区或者老年代分区。在当前转移的目标分区满了之后,就会将新的可用分区标记为survivor分区或老年代分区,继续转移操作。
  一次GC之后,当老年代的空间占用达到甚至超过了堆空间的占用门槛,G1就会启动一次老年代收集。通过命令行选项-XX:InitiatingHeapOccupancyPercent来控制占用门槛,缺省情况是Java堆内存的45%。
  当标记阶段显示某些老年代分区中没有任何存活对象,G1会提前将它们回收。这些分区将被添加到可用分区集合里。那些包含存活对象的老年代分区则被安排到将来的混合收集中。
  G1使用多个并发标记线程,为了尽量避免从应用线程中“偷取”太多CPU,标记线程的工作往往是爆发式的。它们在一个给定的时间段里拼命干活,然后暂定一段时间,让Java线程得以执行。

巨型(Humongous)对象

  G1对大尺寸对象(G1被称为“巨型对象”)分配会做特殊处理。前面讲过,巨型对象就是大小达到甚至超过一个分区50%空间的对象。这个尺寸包括Java对象头。对象头的尺寸在32位和64位的HotSpot虚拟机中是不一样的。一个指定HotSpot虚拟机中某个指定对象的头尺寸可以通过Java对象布局工具来获取,也就是JOL。到写这本书时,在网上已经能找到Java对象布局工具了。
  当发生巨型对象分配时,G1会找出一个连续的可用分区集合,这样就能汇总出足够的内存来容纳巨型对象。第一个分区别被标记为“巨型开始”(humongous start)分区,其他的分区别被标记为“巨型连续”(humongous continues)分区。如果没有足够的连续可用空间,G1就会启动一次full GC来压缩Java堆空间。
  巨型分区被认为是老年代的组成部分,但它们只包含一个对象。这个性质允许G1一旦在并发标记阶段发现该对象已经不再存活,就可以尽早回收这个巨型分区。一旦发生这种情况,所有用来容纳这个巨型对象的分区都将被回收。
  G1面临的一个潜在的挑战,就是某些“短命的”巨型对象虽然已经变成未被引用了,但可能一直没有被回收。JDK 8u40中实现了一个方法,某些情况下在年轻代收集时回收巨型分区。使用G1时避免过于频繁的巨型对象分配,对达成应用性能目标有决定性的帮助。对那些有大量短命巨型对象的应用来说,增强JDK 8u40有一定帮助,但不是最终的解决方案。

Full垃圾收集

  G1里full GC使用的是与串行垃圾收集器相同的算法。当发生full GC时,就会执行对整个内存堆的全面压缩。这确保最大数量的空闲内存可以被系统使用。很重要的一点是G1的full GC活动是单线程的,结果就是可能导致异常长的暂停时间。当然,G1的设计方式也希望使full GC不再是必需的。G1希望不用full GC就能满足应用的性能目标,然后通过不断地调优从而不再需要full GC。

并发周期

  一个G1并发周期包含了几个阶段的活动:初始标记、并发根分区扫描、并发标记,重新标记以及清除。一个并发周期从初始标记开始,到清除阶段结束。除了清除阶段,所有这些阶段都是“标记存活对象图”的组成部分。
  初始标记阶段的目的是收集所有的GC根。根是对象图的起点。为了从应用线程中收集根引用,必须先暂停这些应用线程,所以初始标记阶段是stop-the-world方式的。在G1里,完成初始标记是年轻代GC暂停的一个组成部分,因为无论如何年轻代GC都必须收集所有根。
  标记操作的同时还必须扫描和跟踪survivor分区里所有对象的引用。这也是并发根分区扫描所要做的事。在这个阶段,所有Java线程都允许执行,所以不会发生应用暂停。唯一的限制就是在下一次GC启动前必须先完成扫描。这样做的原因是一次新的GC会产生一个新的存活对象集合,它们跟初始标记的存活对象是有区别的。
  大部分标记工作是在并发标记阶段完成的。多个线程协同标示存活对象图。所有Java线程都可以与并发标记线程同时运行,所以应用就不存在暂停,尽管会受到吞吐量下降的一些影响。
  完成并发标记后就需要另一个stop-the-world方式的阶段来最终完成所有的标记工作。这个阶段被称为“重新标记阶段”,通常它只是一个非常短暂的stop-the-world的暂停。
  并发标记的最终阶段是清除阶段。在这个阶段,找出来的那些没有任何存活对象的分区将被回收。正因为它们没有任何存活对象,这些分区也不会被包含在年轻代或混合GC中,它们会被添加到可用分区的队列里。
  完成标记阶段之后,就能找出哪些对象是存活的,进而确定哪些分区要被包含在混合GC里。既然G1里混合GC是释放内存的基本手段,那么在G1用光可用分区之前完成标记阶段就显得至关重要,如果做不到的话,G1只能退回去发起一次full GC来释放内存,这虽然可靠却很慢。

堆空间调整

  G1里的Java堆尺寸通常是分区尺寸的整数倍。除去这个限制,G1和其他HotSpot垃圾收集器一样,可以在 -Xms与 -Xmx之间动态地扩大或缩小堆大小。
  基于以下几个理由,G1可能会增加Java堆尺寸:

  1. 在一次full GC中,基于堆尺寸的计算结果会调整堆的空间。
  2. 当发生年轻代收集或混合收集,G1会计算执行GC所花费的时间以及执行Java应用所花费的时间。根据命令行配置-XX:GCTimeRatio,如果将太多时间用在垃圾收集上,Java堆尺寸就会增加。这个情况下增加Java堆尺寸,其背后的想法就是允许GC减少发生频度,这样与花在应用上的时间相比,花在GC上的时间也可以随之降低。
    G1中-XX:GCTimeRatio的缺省值为9,而其他所有HotSpot垃圾收集器都缺省使用99。GCTimeRatio的值越大,Java堆尺寸的增长就会更加得积极。其他HotSpot收集器在增加Java堆尺寸的策略上会更加得激进,因为它们的目标是:相对于执行应用的开销,用于GC的时间越少越好。
  3. 如果一个对象分配失败了(甚至是在做了一次GC之后),G1会尝试通过增加堆尺寸来满足对象分配,而不是马上退回去做一次full GC。
  4. 如果一个巨型对象分配无法找到足够的连续分区来容纳这个对象,G1会尝试扩展Java堆来获得更多可用分区,而不是做一次full GC。
  5. 当GC需要一个新的分区来转移对象时,G1更倾向于通过增加Java堆空间来获得一个新的分区,而不是通过返回GC失败并开始做一次full GC来找到一个可用分区。

本文选自《Java性能调优指南》,点此链接可在博文视点官网查看此书。
                    图片描述
  想及时获得更多精彩文章,可在微信中搜索“博文视点”或者扫描下方二维码并关注。
                       图片描述

相关文章
堆的介绍与堆的实现和调整
堆的介绍与堆的实现和调整
80 0
|
6月前
|
Linux
linux内存不足,调整swap空间
linux内存不足,调整swap空间
66 0
|
3月前
|
算法
时间(空间)复杂度(结构篇)
时间(空间)复杂度(结构篇)
39 6
|
5月前
指针\分配动态空间-筛选法求质数
指针\分配动态空间-筛选法求质数
33 5
|
11月前
|
存储 算法 编译器
环境栈空间
环境栈空间(Environment Stack Space)是计算机系统中用于存储和管理程序运行环境的一种数据结构。环境栈空间是一种后进先出(Last In First Out, LIFO)的数据结构,类似于栈。它用于存储和管理程序在执行过程中所需的各种环境信息,如局部变量、函数调用、动态链接库(DLL)加载等。环境栈空间可以确保程序在执行过程中的环境信息得到正确的维护和管理,从而保证程序的正确运行。
50 1
【数据结构】堆的向上调整和向下调整以及相关方法
文章目录 一、堆的概念 二、堆的性质 三、堆的分类 1.大根堆 2.小根堆 四、说明 五、堆的结构 🚩六、堆的向上调整 1.图示 2.代码实现 ⌚️3.时间复杂度分析
[第五空间 2021]WebFTP-白猫
[第五空间 2021]WebFTP-白猫
253 0
|
存储 Java 编译器
Java内存区域介绍以及JDK1.8内存变化
Java内存区域介绍以及JDK1.8内存变化
321 0
Java内存区域介绍以及JDK1.8内存变化
|
存储 自然语言处理 JavaScript
浏览器原理 11 # 栈空间和堆空间:数据是如何存储的?
浏览器原理 11 # 栈空间和堆空间:数据是如何存储的?
125 0
浏览器原理 11 # 栈空间和堆空间:数据是如何存储的?
|
存储 Java
醒酒菜:动画图解核心内存区--堆
醒酒菜:动画图解核心内存区--堆
137 0
醒酒菜:动画图解核心内存区--堆