垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄漏。对内存堆中已经死亡的,或者长时间没有使用的对象进行清理和回收,提升内存的利用率。
既然我们要做垃圾回收,那么我们首先要搞清楚垃圾是怎么被定义的。定义垃圾在Java早期的版本中较多使用的是引用计数法,计算引用次数,如果引用次数为0,那么JVM就回收该对象。后来则偏向于可达性分析,若GC Roots节点到某个节点不可达,则证明该对象需要回收。
引用计数(Reachability Counting)是通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。如果该对象呗其它对象引用,则它的引用计数+1,如果删除对该对象的引用,那么它的计数就-1,当该对象的引用计数为0时,那么该对象就会被回收。
String m = new String("hello world!"); m = null; 复制代码
上面的代码就经历了一次引用计数从+1到-1为0的过程:
hello world hello world
引用计数算法看上去很美好,但是为什么JVM将其放弃不用呢?我们可以看下面的例子:
public class ReferenceCountingGC { public Object instance; public ReferenceCountingGC(String name){} } public static void testGC(){ ReferenceCountingGC a = new ReferenceCountingGC("objA"); ReferenceCountingGC b = new ReferenceCountingGC("objB"); a.instance = b; b.instance = a; a = null; b = null; } 复制代码
我们可以看到, testGC()
的最后,这两个对象都已经置空且无法访问了,但由于他们互相引用着对方,导致它们的引用计数永远都不为0,永远都不会通知垃圾收集器来回收它们。
可达性分析算法(Reachability Analysis)的基本思路是:通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(从GC Roots节点到该节点不可达),则证明该对象不可用。
通过可达性算法,成功解决了引用计数所无法解决的循环依赖的问题,只要你无法与GC Roots建立直接或间接的连接,系统就会判定你为可回收对象。
那么,还有一个问题,GC Root是什么?
在Java中可作为GC Root的对象包括以下4种:
虚拟机栈(栈帧中的本地变量表)中引用的对象
public class StackLocalParameter { public StackLocalParameter(String name){} } public static void testGC(){ StackLocalParameter root = new StackLocalParameter("localParameter"); root = null; } 复制代码
上述代码中的root,就是GC Root。当root置空时,localParameter对象也断掉了与GC Root的引用链,它在后续的运行中将被回收。
方法区中类静态属性引用的对象
public class MethodAreaStaicProperties { public static MethodAreaStaicProperties m; public MethodAreaStaicProperties(String name){} } public static void testGC(){ MethodAreaStaicProperties root = new MethodAreaStaicProperties("properties"); root.m = new MethodAreaStaicProperties("parameter"); root = null; } 复制代码
上述代码中的root,就是GC Root。当root被置为null后,root所指向的properties对象由于无法与GC Root建立关系而被回收。
而m作为类的静态属性,也属于GC Root,parameter对象依然与GC Root建立着连接,所以此时parameter对象并不会被回收
方法区中常量引用的对象
public class MethodAreaStaicProperties { public static final MethodAreaStaicProperties m = MethodAreaStaicProperties("final"); public MethodAreaStaicProperties(String name){} } public static void testGC(){ MethodAreaStaicProperties root = new MethodAreaStaicProperties("staticProperties"); root = null; } 复制代码
上述代码中的root,就是GC Root。当root被置null后,final对象不会因为没有与GC Root建立联系而被回收。
本地方法栈中引用的对象
任何Native接口都会使用某种本地方法栈,实现的本地方法接口是使用 C
连接模型的话,那么它的本地方法栈就是 C
栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单的动态连接并直接调用指定的本地方法。
在确定了那些垃圾可以被回收后,垃圾收集器就要开始进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集,我们来讨论以下几种垃圾收集算法:
标记—清除算法
复制算法
标记-整理算法
Java堆(Java Heap)是JVM所管理的内存中最大的一块,也是垃圾收集器管理的主要区域,这里我们主要分析一下Java堆的结构。
Java堆主要分为2个区域:年轻代和老年代。其中年轻代又分Eden区和Survivor区,而Survivor又由From和To区构成。下面介绍一下各个区:
Eden区
大多数情况下,对象会在新生代Eden区中进行分配。研究表明,有将近98%的对象是朝生夕死,无法在应用中停留很长时间,因此这些新对象从建立到回收,可能一辈子待都在Eden区。
当Eden区没有足够空间进行分配时,虚拟机会发起一次Minor GC清空Eden区,这时,Eden区中绝大部分对象会被回收,而那些通过上面我们说过的可达性分析的对象,将会活着进入到Servivor区的From区中,若From区空间不足,对象将直接进入Old区。
Survivor区
Survivor区相当于Eden区和Old区的一个缓冲,它分成两个区:From和To。为什么经过第一次Minor GC存活下来的类不直接送进老年代呢,因为存活下来的对象可能在第二次、第三次Gc时就被清除了。过早的将这些对象移入老年代,增加垃圾存活的时间,是不可取的。因而在缓冲区Survivor,一般设置成将经过16次Minor GC后还能存活的对象移入老年代。
那么什么需要分成两个区呢?设置成两个Survivor区最大的好处就是解决内存碎片化问题。每次Minor GC,会将之前Eden区和From区中的存活对象复制到To区,第二次又从To区复制到From区,如此反复。整个过程中,永远有一个Survivor Space是空的,另一个非空的Survivor是无碎片的。
老年代
老年代占据着2/3的堆内存空间,只有在Major GC的时候才会进行清理,每次GC都会触发"Stop-The-World"(STW)。内存越大,STW的时间也越长,所以内存也不仅仅是越大越好。老年代采用标记-整理算法。
除了上述所说,JVM还有一个内存担保机制。在这个机制下,无法安置的对象会直接进入老年代,以下几种情况也会进入老年代。
大对象指占用大量连续内存空间的对象,因为过于巨大,一经创建直接进入老年代。因为Eden区和2个Survivor装不下或者装下之后会发生大量的内存复制,影响性能。
Byte[] bytes = new Byte[4*1024*1024]; 复制代码
一般情况下,上面建立的4M大的对象是直接被送进老年代的。
JVM给每个对象定义了一个对象年龄(Age)计数器。正常情况下对象会不断在Survivor的From和To区之间移动,对象在Survivor区中每经历一次Minor GC,年龄就增加一岁,当年龄值增加到16岁时,就会被转移到老年代。
JVM并不必须要求对象年龄到15岁,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代了,无需成年。
Garbage-First(G1)收集器是一种服务器式垃圾收集器,适用于具有大容量存储器的多处理器机器。它以高概率满足垃圾收集(GC)暂停时间目标,同时实现高吞吐量。Oracle JDK 7 Update 4及更高版本完全支持G1垃圾收集器。G1收集器专为以下应用而设计:
G1计划作为Concurrent Mark-Sweep Collector(CMS)的长期替代品。将G1与CMS进行比较,存在差异,使G1成为更好的解决方案。一个区别是G1是压缩收集器。G1使用分区进行充分压缩,而不是细粒度的自由列表自由分配。它简化了部分收集器,可以最大程度的消除潜在的回收后碎片化的问题。更进一步的的说,G1提供了比CMS收集器更加可预测的垃圾回收停顿时间,同时也允许用户指定停顿的目标。
在JVM启动时会计算并定义堆区域大小。它基于这样的原则:尽可能接近2048个区域,每个区域的大小为1到64 MB之间的2的幂。
更简单地说,对于12 GB的堆,默认情况下,JVM将分配3072个区域,每个区域能够容纳4 MB:
第一步、计算每个区域的大小:12288 MB / 2048(区域)= 6 MB - 这不是2的幂
第二步、计算出需要分成多少个区域:
12288 MB / 8 MB = 1536个区域 - 通常太低
12288 MB / 4 MB = 3072个区域 - 可接受
分完区后的堆见下图
执行垃圾回收时,G1以类似于CMS收集器的方式运行。G1执行并发全局标记阶段以确定整个堆中对象的活跃度。在标记阶段执行完成之后,G1知道哪些区域基本上是空的。 它首先收集这些区域,使其产生大量的自由空间。 这就是为什么这种垃圾收集器被称为Garbage-First的原因。顾名思义,G1将其收集和压缩活动集中在堆可能充满可回收对象的区域(即垃圾所在区域)。G1使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要手机的区域数。
上文说堆是分成区域的单个内存空间,可以在需要时轻松调整大小。年轻代也是一样,它是由一组非连续的区域组成的。
年轻代在垃圾回收过程中,是使用多线程并行回收的,而且整个事件都是STW(Stop The World)的,所有的应用程序都会停止操作,等待年轻代回收完成。回收之后,活动对象被复制到新的Survivor区域或老年代中。
我们来看下其垃圾收集的过程:
垃圾回收完成了:
清理垃圾
疏散(复制)活跃对象到Survivor区或老年代
合并存活的年轻代至新的堆内存
G1收集器在老年代堆上执行以下阶段。注意,某些阶段是年轻代收集的一部分。
阶段 | 描述 |
---|---|
(1)初始标记(Stop-The-World事件) | 作用在正常的年轻GC上,标记可能被引用至老年代的survivor区域(根区域)。 |
(2)根区扫描 | 扫描survivor区域查找将被引入老年代的类。程序运行时扫描,且在年轻代GC发生之前完成该阶段。 |
(3)并发标记 | 全堆扫描以查找存活对象。程序运行时扫描,且这个阶段可能被年轻代垃圾收集所打断。 |
(4)重新标记(Stop-The-World事件) | 完成对堆中存活对象的标记。使用开始时快照(SATB)的算法,该算法比CMS收集器中使用的算法快得多。 |
(5)清理(Stop-The-World事件且并发执行) | 1.对存活对象和完全空闲区域进行计数。(Stop-The-World)2.清除Remember Sets(Stop-The-World)。3.重置空白区域并将其返回到空闲列表(并行)。 |
(*)复制(Stop-The-World事件) | Stop-The-World时将存活对象复制到新的未使用区域。可以通过记录为的年轻代区域来完成[GC pause (young)]。或者记录为的年轻代和老年代地区[GC Pause (mixed)]。 |
下面请看详细示例图:
1.初始标记阶段:初始标记存活对象在年轻代垃圾回收时进行。在日志中标记为GC pause(young)(initial-mark)。
2.并发标记阶段:如果找到空区域(由“X”表示),则在重新标记阶段立即将它们移除。同时,还要收集确定活跃度的“计数”信息。
3.重新标记阶段:空区域被移出并回收。并且在这个阶段计算所有区域的活跃度。
4.复制/清理阶段:G1选择具有最低“活跃度”的区域(可以最快收集的区域)进行垃圾收集。然后,这些区域与年轻代的GC同时收集。这在日志中表示为[GC pause (mixed)]。因此,这个阶段垃圾收集时同时收集年轻代和老年代。
5.清理完成:选择的区域已经被收集并压缩成图中所示的深蓝色区域和深绿色区域。
我们可以对老年代的G1垃圾收集提取出几个要点:
1.并行标记阶段
2.重新标记阶段
3.复制/清理阶段
我们将在这里深入研究垃圾收集(GC)日志选项的列表。为了确定可以应用调优的位置,你需要了解集合中涉及的不同步骤,这些步骤的含义以及它们对总体收集时间的影响。为此,我们将可用选项分解为三个不同的类别,以便查看日志的含义以及在何种情况下应使用它们:
强制性:没有这些设置,任何人都不应该在生产中运行。
高级应用:根据应用程序的成熟度和负载的增长率,这些是在需要进行额外调整时在生产中运行的注意事项。
调试:这些选项旨在解决特定问题或性能问题,并且除了在其他任何地方无法再现问题的情况之外,不会在生产中使用。
下面是一些你可能会用到的配置:
配置 | 语义 |
---|---|
-Xloggc:/path/to/gc.log | 写入GC日志的路径 |
-XX:+ UseGCLogFileRotation | 启用GC日志文件轮换 |
-XX:NumberOfGCLogFiles = <值> | 要保留的已旋转GC日志文件数 |
-XX:GCLogFileSize = <大小> | 每个GC日志文件的大小以启动轮换 |
-XX:+ PrintGCDetails | 详细的GC日志 |
-XX:+ PrintGCDateStamps | 集合的实际日期和时间戳 |
-XX:+ PrintGCApplicationStoppedTime | GC期间应用程序停止的时间 |
-XX:+ PrintGCApplicationConcurrentTime | 应用程序在GC之间运行的时间 |
-XX:-PrintCommandLineFlags | 打印GC日志中的所有命令行标志 |
在这些配置中,为日志文件数和每个日志文件的大小选择适当的值将确保维护垃圾收集日志时,能够合适的找到这些历史记录。建议保留至少一周的日志,因为它提供了应用程序在给定周内的执行情况。
让我们深入了解通过'-XX:+ PrintGCDetails'生成的日志的解剖结构,分解为六个关键点:
1.第一点概述了四个关键信息:
a.事件发生的实际日期和时间,通过设置'-XX:+PrintGCDateStamps'记录- 2016-12-12T10:40:18.811-0500
b.自JVM开始以来的相对时间戳 - 29.959
c.收集类型 - G1疏散暂停(年轻代) - 将此识别为疏散暂停和年轻代回收
d.收集的时间 - 0.0305171秒
2.第二点概述了所有并行任务:
a.并行时间-从收集开始到最后一个GC Worker结束时完成的停止世界(STW)并行任务所花费的时间 - 26.6ms
b.GC Workers-并行GC工作器的数量,由-XX:ParallelGCThreads - 4定义 i.默认为CPU数,最多8个。对于8个以上的CPU,默认为⅝线程与CPU的比率
c.GC Worker Start- GC Worker开始时自JVM启动以来的最小/最大时间戳。差异表示第一个和最后一个线程开始之间的时间(以毫秒为单位)。理想情况下,您希望它们能够在同一时间快速启动
d.Ext Root Scanning-扫描外部根(线程堆栈根,JNI,全局变量,系统字典等)所花费的时间,以查找到达当前集合集的任何内容
e.更新RS(记忆集或RSet)-每个区域都有自己的记忆集。它跟踪保存对区域的引用的卡的地址。当写入发生时,写后屏障通过将新引用的卡标记为脏并放入日志缓冲区或脏卡队列来管理区域间引用的变化。一旦完整,并发细化线程将并行处理这些队列以运行应用程序线程。更新RS进入以使GC工作人员能够处理在集合开始之前未处理的任何未完成的缓冲区。这可确保每个RSet都是最新的 i. 已处理缓冲区 -显示更新RS期间处理了多少个更新缓冲区
f.扫描RS- 扫描每个区域的记忆集以查找指向集合集中区域的参考
g.代码根扫描-扫描已编译源代码的根以获取对集合集的引用所花费的时间
h.对象复制-在疏散暂停期间,必须撤离收集集中的所有区域。对象副本负责将所有剩余的活动对象复制到新区域
i.终止-当GC工作人员完成时,它进入终止例程,在那里它与其他工作人员同步并试图窃取未完成的任务。时间表示工人第一次尝试终止和实际终止时所花费的时间 i.终止尝试 -如果一名工人成功窃取任务,它会重新进入终止例程并试图窃取更多工作或终止。每次任务被盗并重新进入终止时,终止尝试的次数将增加
j.GC Worker Other-表示未计入先前任务的任务所花费的时间
k.GC Worker Total-显示每个并行工作线程花费的时间的最小值,最大值,平均值,差值和总和
l.GC Worker End- GC Worker结束时自JVM启动以来的最小/最大时间戳。diff表示第一个和最后一个线程结束之间的时间(以毫秒为单位)。理想情况下,您希望它们能够在同一时间快速结束
3.第三点描述连续任务:
a.根修复-移动标记的方法指向CSet以修复在GC期间可能已移动的任何指针
b.根清除-清除代码根表中的条目
c.清除CT-卡表(Card Table)清除脏卡数据
4.第四点概述了以前没有考虑过的其他任务。它们也是连续的。
a.选择CSet-选择Collection Set的区域
b.Ref Proc-处理STW参考处理器发现的任何soft / weak / final / phantom / JNI引用
c.Ref Enq-循环引用并将它们排入待处理列表
d.Reditry Cards-通过收集过程修改的卡被标记为脏卡
e.Humongous Register-启用'G1ReclaimDeadHumongousObjectsAtYoungGC'(在JDK 8u60中添加默认true /功能)G1将尝试在Young GC期间迫切地收集Humongous区域。这表示jvm在评估Humongous区域是否是马上回收的候选区域并记录回收它们需要多长时间。有效候选区域是没有现有的根节点代码引用,并且在Rsets中只有少量条目。每个候选人都会将其记忆集Rset刷新到脏卡队列,如果清空,该区域将被添加到当前Cset中
f.Humongous Reclaim-确保大对象死亡和清理所花费的时间,释放区域,重置区域类型并将区域返回到自由列表并考虑释放的空间
e.空闲CSet-撤离的区域被添加回空闲列表
5.第五点描述了分代如何变化以及如何根据当前的回收机制调整它们:
i.这显示当前的Young Collection被触发,因为Eden空间已满 - 分配的1097.0M(1097.0M)
ii.它表明所有Eden空间都被撤离,收集的使用量减少到0.0B
iii.它还表明,对于下一个集合,伊甸园空间的总分配已减少到967.0M
i.由于年轻代的疏散,Survivor空间从13.0M增长到139.0M
i.在收集时,总堆分配为1694.4M(2048.0M)
ii.收集后,整个堆分配减少到736.3M,最大堆不变(2048.0M)
6.第六点代表收集所用的时间:
i.在收集过程中在进程内的用户代码中花费的CPU时间量。这会占所有CPU的所有线程。这不包括在流程外花费的时间或等待的时间。根据并行线程的数量,用户时间将比实时高很多
i.在进程内核中花费的CPU时间量。这会占所有CPU的所有线程。这不包括在流程外花费的时间或等待的时间
i.这是从集合开始到结束的真实时钟时间。这还包括在其他过程中花费的时间和等待的时间
你可能遇到的下一个事件是并发标记。如第1部分所述,并发标记可以通过几种不同的方式触发,但它始终执行相同的工作。
①.第一点表示标记的开始
a.GC暂停(G1疏散暂停)(年轻代)(初始标记)
要利用STW暂停并跟踪所有可到达的对象,初始标记是作为Young Collection的一部分完成的。初始标记设置成两个top-at-mark-start(TAMS)变量,以区分现有对象和并发标记期间分配的对象。顶部TAMS上方的任何对象都被隐含地视为此循环的实时对象
②.第二点和第一点是并发事件
a.GC并发根区域扫描启动 区域根扫描从初始标记中获取新的Survivor区域并扫描它们以获取参考。所有标记这些“根”区域找到的任何引用。
b.GC并发根区域扫描结束
③.第三点表示并发标记:
a.GC并发标记启动
它与应用程序线程并行运行。默认情况下,并发线程数是并行线程数的25%.它也可以通过-XX:ConcGCThreads显式设置
跟踪堆并在位图中标记所有活动对象。因为顶部TAMS上方的所有对象都是隐式生存的,所以我们只需要标记低于该阈值的内容
考虑标记期间的并发更改。为了使SATB工作,它必须能够在拍摄初始快照时通过指针跟踪对象。为了实现这一点,预写屏障将原始值记录到SATB缓冲区,该缓冲区在满时将添加到由并发标记线程定期处理的全局列表中
实时数据计数与标记过程同时发生。每个区域的消耗空间被制成表格,因为活动对象被标记以便建立活跃度比率
b.GC并发标记结束
④.第四点是STW阶段:
a.GC备注/完成标记/GC ref-proc/卸载
⑤.第五阶段也是STW阶段
a.GC清理
1.最终的活动对象计数完成。这是使用标准的并行线程集在每个区域完成的
2.为准备下一个标记,交换前一个和下一个位图
3.释放并清除具有零活字节的死旧区域
4.擦除没有活动对象的Remember Sets区域
5.为了准备下一个周期,旧区域根据其活动性进行收集集选择的排序
6.从metascape同时卸载没有引用类
⑥.第六点再次是并发阶段 a.GC并发清理--启动 1.它执行第五步中处理的空区域的最终清理 i.清理每个区域的Remember Set:这包括稀疏和粗略条目,来自card缓存和代码根表 ii.当区域被完全清理时,它们被添加到临时列表中。清理完成后,临时列表将合并到辅助空闲区域列表中,等待将其添加回主空闲列表
b.GC并发清理结束
一旦并发标记完成,你将看到一个Young GC,紧接着一个是一个混合GC(这假定符合正确的标准,如第一部分所述)。正如您在下面的日志中所看到的,“混合收集”的任务与之前解释Young Collection相同。Young和Mixed Collections之间只有两处不同: 1.此集合被表示为"混合" a.GC暂停(G1疏散暂停)(混合) 2.集合集将包含通过并发标记确定的旧区域
你可能遇到第三种类型的集合,是我们正在努力避免的集合,是Full GC。在G1中,Full GC是单线程Stop The World(STW)暂停,他将清理并压缩所有区域。你可以在Full GC日志中获得三条重要信息。
1.GC原因(分配失败)告诉你出发Full GC的原因。这将有助于你进行调整。另一个流行的原因是元数据GC阈值
2.发生的频率。每隔几天使用一次Full GC可能完全不是问题,而如果每隔一个小时就进行一次,那么可能就需要进行优化了
3.每次Full GC耗时。根据要求而定,如果没有花费大量的时间完成GC,则Full GC可能不是一个关键问题
想要触及的最后一个日志是从'-XX:+PrintGCApplicationStoppedTime'和'-XX:+PrintGCApplicationConcurrentTime'生成的。这些标志提供了三个有用的数据点:
1.第一点告诉我们在安全点期间应用程序线程停止时间 2.第二点告诉我们将所有线程带到安全点并暂停它们所花费的时间 3.第三点告诉我们应用程序线程在安全点之间运行了多长时间
继续使用高级标志,下面选项可提供有关值和阈值的宝贵见解:
-XX:+ PrintAdaptiveSizePolicy | 有关收集器的详细信息 |
---|---|
-XX:+ PrintTenuringDistribution | 幸存者空间的使用和分配 |
-XX:+ PrintReferenceGC | 处理引用的时间 |
从'-XX:+ PrintAdaptiveSizePolicy'开始,此标志将有关G1的详细信息添加到之前讨论的每个事件中。这提供了对收集集选择和暂停时间估计的深入了解。
查看Young Collection,原始详细日志中添加了五条新信息:
1.第一点告诉我们脏卡队列中仍需要处理的卡数。它还提供了对处理所需时间的预测。预测包括更新RS和扫描RS的时间。
2.第二点告诉我们将包含在此集合中的区域数量。时间预测包括对象副本的估计。
3.第三点为集合提供了最终的CSet和时间预测。预测是基准时间和年轻区域CSet时间的组合。
4.第四点是在某些条件下记录的,因此您可能偶尔会看到它。如果您执行GC工作与应用程序工作所花费的时间大于特定阈值,G1将尝试扩展堆。注意:如果您的最小/最大堆相同,则不能进行扩展。
5.仅在请求并发标记时记录第五个点。这个日志有几个变化围绕GC原因,例如大量分配或者标记试图在它已经运行时启动。我们看到的最常见的形式是堆占用率大于我们的IHOP,因此开始标记。
在请求并发周期的Young Collection之后,您将立即看到要求且具有Young Collection初始标记的Concurrent Cycle。这个Young系列的其余细节保持不变。
标记完成后,您将看到一个Young Collection,其中包含有关混合集合的符合人体工程学的日志。
1.第一点告诉我们,我们将开始混合GC,因为可回收百分比(22.62%)高于我们的默认G1HeapWastePercent(5%)。
a.注意:如果可回收百分比低于5%,您会看到类似的日志:不要启动混合GC,原因:可回收百分比不超过阈值
现在混合回收将开始
1.包括CSet的收集和年轻代区域的增加和Young Collection相同。
2.第二点概述了为混合收集添加到CSet的旧区域。在这种情况下,CSet选择结束,因为命中了最大旧区域阈值。默认情况下,G1只会将最多10%的旧区域添加到CSet,由-XX:G1OldCSetRegionThresholdPercent = X定义。
3.第三点提供最终的CSet和暂停时间预测。
4.第四点为我们提供了混合GC循环状态的详细信息。在这种情况下,我们仍然可以收集535个旧区域,相当于305363768字节或堆的14.22%。鉴于这仍高于我们的垃圾百分比,下一个系列将再次混合。
以下混合收集看起来与前一个相同,但也可以选择结束混合GC:
此混合收集的第一点显示可回收百分比现已降至5%阈值以下,混合收集将不再继续。以下系列将再次成为Young。
最后,让我们看一下Full GC:
1.第一点告诉我们主要或次要空闲列表中没有空闲区域,因此,分配请求失败并且已请求堆扩展。
2.第二点记录扩展请求的数量。值得注意的是,在第1点和第2点,实际上还没有尝试过扩展。
3.第三点告诉我们不会尝试堆扩展。当可用的未提交区域的数量为0时,这会使扩展逻辑短路。由于分配失败,我们最终会执行完整GC。
4.第四点出现在最小堆小于最大堆的情况下。在这种情况下,G1将尝试在完整GC后将堆缩小到70%。
5.第五点告诉我们堆正在缩小以及收缩多少。
PrintTenuringDistribution标志提供有关每个集合期间的Survivor空间布局和阈值的信息。这很有用,因为它可以让你查看对象是如何老化的。
1.这些数据向我们展示了幸存者空间的三个重要方面:
所需的幸存者大小,等于幸存者大小乘以TargetSurvivorRatio(默认为50%)
目标阈值,也称为对象可能保留的年龄或年轻GC的数量。这是通过将每个年龄中的对象的大小相加来计算的,直到大小大于期望的幸存者大小
年龄分布,包括每个年龄段所有对象的大小以及幸存者空间使用量的增量总和
最后,我们有诊断和实验的配置。这些配置可以添加大量日志记录,只应在必要时以及在尝试调试特定问题时使用:
'-XX:+ G1PrintHeapRegions'的输出添加了大量的日志记录。下图表示一组紧凑的事件,用于说明可能发生的各种类型。某些事件发生在Young GC下,而其他事件发生在Full GC期间。
只有在尝试调试非常具体的问题时才需要打印堆区域事件,例如:
调试疏散失败和失败区域的数量
确定Humongous对象的大小和频率
跟踪和评估作为CSet的一部分分配和收集的伊甸园,幸存者和老年代的数量
1.COMMIT
2.ALLOC(Eden)
3.CSET
4.CLEANUP
5.UNCOMMIT
6.ALLOC(Old)
7.RETIRE
8.REUSE
9.ALLOC(Survivor)
10.EVAC-FAILURE
11.POST-COMPACTION(Old)
12.ALLOC(SingleH)
13.ALLOC(StartsH)
14.ALLOC(ContinuesH)
'-XX:+ G1PrintRegionLivenessInfo'的输出也会产生大量额外的日志记录,但它仅限于并发标记周期的频率。如果您想要在并发标记之后分析旧区域的内容,以及在对CSet选择的效率进行分类之后,它非常有用。
后标记输出,也可以包括EDEN,FREE,SURV,HUMS和HUMC区域类型,概述了与对象活力相关的前一个和下一个标记开始(TAMS)级别的每个区域详细分类用于标记周期。输出中定义了八条信息:
区域类型:可以是Old,Eden,Survivor,Humongous Start, Humongous Continues and Free
区域范围:区域的地步和结束值
已使用:区域中已使用字节的总数,以区域底部和当前顶部之间的总和来衡量
Prev-Live:从前一个标记周期的角度看的活动字节数,以前一个TAMS和当前顶部之间的总和加上前一个标记周期的已知活动对象(先前标记的)测量
Next-Live:从当前标记周期的角度看的实时字节数,以下一个TAMS和当前顶部之间的总和加上当前标记周期中已知的活动对象(已标记)来衡量
GC-Eff:这是通过将可回收字节(已知实时字节减去总区域容量)除以收集区域的估计时间(RS扫描,对象复制和其他时间)来测量的。效率越高,该区域作为候选者越好
重新设置:区域记忆集的大小,通过将区域表的大小与位图的大小相乘,乘以堆字大小来衡量
Code-Roots:强code root所占用的区域内存量(以字节为单位)
可以通过设置'-XX:G1SummarizeRSetStatsPeriod = XX'来控制' -XX:+ G1SummarizeRSetStats' 的详细程度,它定义两次总结之间GC循环的数量。在调试特定于RSet的问题时,此设置很有用,例如在扫描或更新RS活动期间花费的时间过长。
此日志定义了3个关键数据部分:
1.第一个与并发细化统计有关。在这种情况下,从94个缓冲区处理了506张卡,并且100%的工作由并发线程完成。最后一块,粗化,告诉我们有多少RSets被粗化。实质上,当许多区域引用一个区域中的对象时,您可能没有足够的空间来容纳新的区域位图。在这种情况下,引用区域位图被标记为粗化。这很昂贵,因为必须扫描整个引用区域以获取传入引用;
2.基于每个区域类型定义整体Remember set统计信息
3.第三部分基于每个区域类型定义整体代码根集统计信息
对于标准集合,许多行项目由几个基础任务组成。如果一项任务花费的时间超过预期,则可以启用“ -XX:G1LogLevel = finest”以提供每项操作的详细分类。此外,它还增加了Worker在每项任务上花费的详细执行时间。
我们要触及的最后一个设置是'-XX:+ G1TraceEagerReclaimHumongousObjects'。如果您正在经历大量的Humongous分配,则可以启用此选项以提供有关收集器认为哪些Humongous对象为Dead或Live以进行急切回收的详细信息。JDK8u60引入了对Humongous地区的急切回收