引言:如果你在使用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性能调优指南》,点此链接可在博文视点官网查看此书。
                    
  想及时获得更多精彩文章,可在微信中搜索“博文视点”或者扫描下方二维码并关注。
                       

从G1设计到堆空间调整相关推荐

  1. JVM学习笔记之-堆,年轻代与老年代,对象分配过程,Minor GC、Major GC、Full GC,堆内存大小与OOM,堆空间分代,内存分配策略,对象分配内存,小结堆空间,逃逸分析,常用调优工具

    堆的核心概述 概述 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域.Java堆区在JVM 启动的时候即被创建,其空间大小也就确定了.是JVM管理的最大一块内存空间. 堆内存的大小是可 ...

  2. java虚拟机堆空间

    堆是一个进程唯一的,是内存管理的核心区域 jvm启动时 堆 就会被创建,大小也就确定了,是jvm中管理的最大的一块内存,堆的大小是可以调节的 <java虚拟机规范>规定,堆可以处于物理上不 ...

  3. Java堆空间(Heap Space)

    Java 堆空间(Heap Space) 概述 在Java程序中,堆是JVM内存空间中最大的一块,同时我们知道,每个线程都拥有一个虚拟机栈,但是堆不同,Java堆是被所有线程共享的一块内存区域,在虚拟 ...

  4. Java 堆空间(Heap Space)

    Java 堆空间(Heap Space) 概述 在Java程序中,堆是JVM内存空间中最大的一块,同时我们知道,每个线程都拥有一个虚拟机栈,但是堆不同,Java堆是被所有线程共享的一块内存区域,在虚拟 ...

  5. 45.JVM调优策略、常见问题:内存泄漏(年老代堆空间被占满、持久代被占满、堆栈溢出、线程堆栈满、系统内存被占满)优化方法:优化目标、优化GC步骤、优化总结;案例分析(公司系统参数、网上给的配置参数)

    45.JVM调优策略 45.1.常见问题 45.1.1.内存泄漏 45.1.1.1.年老代堆空间被占满 45.1.1.2.持久代被占满 45.1.1.3.堆栈溢出 45.1.1.4.线程堆栈满 45. ...

  6. qotd服务_QOTD:Java线程与Java堆空间

    qotd服务 以下问题很常见,并且与OutOfMemoryError有关:在JVM线程创建过程和JVM线程容量期间无法创建新的本机线程问题. 这也是我向新技术候选人(高级职位)提出的典型面试问题. 我 ...

  7. QOTD:Java线程与Java堆空间

    以下问题很常见,并且与OutOfMemoryError有关:在JVM线程创建过程和JVM线程容量期间无法创建新的本机线程问题. 这也是我向新技术候选人(高级职位)提出的典型面试问题. 我建议您在查看答 ...

  8. 栈空间和堆空间的区别

    栈空间用于存储函数参数和局部变量,所需空间由系统自动分配,回收也由系统管理,无需人工干预:堆空间用于存储动态分配的内存块,分配和释放空间均由程序员控制,有可能产生内存泄漏. 栈空间作为一个严格后进先出 ...

  9. Java JVM堆空间的概述

    Java JVM堆空间的概述 1.设置堆空间初始值和最大值 2.堆的核心概述 内存细分 3.堆空间大小的设置 4.新生代与老年代 5.图解对象分配的过程 6.常用调优工具 7.Minor GC.Maj ...

  10. java堆空间(内存)

    当Java程序开始运行时,JVM会从操作系统获取一些内存.JVM使用这些内存,这些内存的一部分就是堆内存.堆内存通常在存储地址的底层,向上排列.当一个对象通过new关键字或通过其他方式创建后,对象从堆 ...

最新文章

  1. sc delete:指定的服务已经标记为删除
  2. 兰州大学第一届 飞马杯 体育课排队 二分 + 最大流 + 输出路径
  3. python简单代码需要写多久_python基本语法?初学Python要多久才能入门?
  4. Meta Company在公开信中谴责Facebook改名
  5. 找出数组中两数之和为指定值的所有整数对
  6. [WebApp开发]基础教程-Web App开发入门
  7. 浅析类的const成员函数,类的const对象
  8. python爱好者社区 周琦_这么多年来,我算想明白了!
  9. 海贝思蓝牙接收器Linux,Hagibis海备思 蓝牙音频接收 耳机怎么样,评测
  10. 南开大学2019年数学分析考研试题
  11. jxt - 强结构文档数据表示协议
  12. JupyterNotebook关闭时报Python.exe应用程序错误
  13. 几款入夏品牌包包可以看看
  14. 机器学习之L1正则化和L2正则化(附源码解析)
  15. python项目开发案例精粹-Python金融实战案例精粹
  16. ➢ 微信公众号运营教程(一)申请一个微信公众号
  17. 导致请求失败 设备硬件出现致命错误_硬盘提示无法访问设备硬件出现致命错误,导致请求失败,里面的数据如何找到...
  18. 【RAC】关闭Clusterware 遇到CRS-2529,ORA-15097:
  19. watchguard xcs 项目组内部招聘!
  20. 开源是容器安全面临的最大挑战?|Anchore 软件供应链安全报告解读

热门文章

  1. 项目中比较常用的数据筛选场景
  2. 关于pycharm安装出现的interpreter field is empty,无法创建项目存储位置
  3. iBATIS使用$和#的一些理解
  4. [bzoj1269]文本编辑器editor [bzoj1500]维修数列
  5. 启动Tomcat出现Using CATALINA_BASE
  6. 当滚动页面到一定程度时,页顶菜单浮动固定在页面顶部
  7. 2.1. sql增删查改
  8. eclipse引入jar类包的源代码
  9. 返回List的分页方法
  10. Sharepoint学习笔记--- 快速确定VisualStudio2010当前Project的assembly name