从G1设计到堆空间调整

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

时间: 2024-11-05 15:53:47

从G1设计到堆空间调整的相关文章

matlab中增加Java VM 的堆空间(解决xml_io_tools出现的OutOfMemory问题)

今天用MATLAB写程序,调用了xml_io_tools(很赞的一个xml读写工具包)中的函数,但是由于我要书写的文件比较大,5m左右,运行时不知道xml_io_tools中的哪一块超出了java中的内存限制,于是就来研究下怎么增加matlab中Java VM的堆空间,首先用英文在墙外搜了半天,google搜出来的前几条都是使用Jconsole来分配空间的,但是需要下载相应的matlab的版本的JDK的,中间各种曲折,详见文尾,最后放弃治疗的用中文搜了一下,发现早就有了官方的解答了,汗,将文章复

初学JAVA——栈空间堆空间的理解

1.Person pangzi;    //这是在“开拓空间”于栈空间 pangzi=new Person();    //这是赋值于堆空间 上两步就是在做与空间对应的事. 2.值类型直接存入栈空间,如AF,引用类型存入堆空间,在栈空间存有“索引地址”,如当需要B时,在栈空间寻找“索引地址”后对应寻找堆空间的“详细内容”. 故,值类型“快”,引用类型“灵活”. 例String S = “ABCDEFG........Z",则S对应栈空间,“ABCDEFG........Z"对应堆空间.

堆空间的分配与释放

堆空间的分配和释放 #include <stdlib.h> malloc.calloc.realloc.free malloc void *malloc(size_t size); 功能:在堆中分配 size 字节的连续空间 参数:size_字节数 返回值:成功返回分配空间的首地址,失败返回 NULL free void free(void *ptr); 功能:释放由 malloc.calloc.realloc 分配的空间 参数:ptr_空间的首地址 返回值:无 注意: 1.每个空间只能释放一

logo设计公司谈“精微调整”技巧

首先我们得了解什么是vi设计中的精微调整原理:  在企业logo设计的时候,免费logo设计中的运用比例,对比,复制三个工具如果使用得当,我们就可以制作出精美的作品.而专业设计师与新手或业余爱好者的区别,更重要地体现在后期的精微调整上.一个缺少精微调整的形体,初看也许不错,但它的魅力却不能长久,一旦我们多看几眼它就显得平常而繁琐了.  而经过精微调整的作品却是另外一种结果,它温暖,恬静,优雅,人性化,我们的眼睛早就忙于去找寻它的奥妙而暇顾及其他:嗯,这组线条是平行的;这种颜色与那种颜色一样;这两

你必须知道的指针基础-8.栈空间与堆空间

一个由C/C++编译的程序占用的内存分为以下几个部分: 1.栈区(stack):又编译器自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构的栈. 2.堆区(heap):一般是由程序员分配释放,若程序员不释放的话,程序结束时可能由OS回收,值得注意的是他与数据结构的堆是两回事,分配方式倒是类似于数据结构的链表. 3.全局区(static):也叫静态数据内存空间,存储全局变量和静态变量,全局变量和静态变量的存储是放一块的,初始化的全局变量和静态变量放一块区域,没有初始化的在相邻

VMware 虚拟机Red Hat 5.9 交换区及硬盘空间调整

首先要通过VMware设置简单实现内存扩大.但是系统中的/swap应该如何设置呢? 1. 创建swap 文件 使用如下命令: #dd if=/dev/zero of=/swap/swapfile bs=1M count=3072  dd命令作用是用指定大小的块拷贝一个文件,并在拷贝同时进行指定的转换. 语法:dd [选项] if =输入文件(或设备名称). of =输出文件(或设备名称). ibs = bytes 一次读取bytes字节,即读入缓冲区的字节数. skip = blocks 跳过读

关于栈空间和堆空间的问题

操作系统对于内存的两种管理方式 如鹏网 <C语言也能干大事>http://www.rupeng.com/Courses/Index/12 第三章透彻讲指针 之  第 15 节: 栈空间 平时我们定义的变量都是分布在栈空间里,如下面的程序所示 1 #include <stdio.h> 2 int main(int argc, char *argv[]) 3 { 4 int i=5; 5 char s[] = "afasdfsfwfw"; 6 return 0; 7

Java堆空间Vs栈内存

之前我写了几篇有关Java垃圾收集的文章之后,我收到了很多电子邮件,请求解释Java堆空间,Java栈内存,Java中的内存分配以及它们之间的区别. 您可能在Java,Java EE书籍和教程中看到很多有关堆和变量内存的参考,但是几乎没有就程序而言完全解释堆和栈的内存分配的. Java堆空间 Java运行时使用Java堆空间为对象和JRE类分配内存.每当我们创建任何对象时,它总是在堆空间中创建. 垃圾回收在堆内存上运行以释放没有任何引用的对象使用的内存.在堆空间中创建的任何对象都具有访问权限,并

Exception in thread &quot;main&quot; java.lang.OutOfMemoryError: Java heap space(Java堆空间内存溢出)解决方法

http://hi.baidu.com/619195553dream/blog/item/be9f12adc1b5a3e71f17a2e9.html问题描述Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 解决方案[转] 一直都知道可以设置jvm heap大小,一直用eclipse写/调试java程序.一直用命令行or console加参数跑程序.现象:在eclipse的配置文件eclipse.