G1摘要

G1

启动参数示例

-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:+PrintTenuringDistribution -XX:+UseG1GC -XX:+DisableExplicitGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:ParallelGCThreads=4 -Xloggc:/export/Logs/gclogs

  • 打印GC日志是很重要的,对性能基本没有影响
  • 设置一个合理的GC线程数是很有必要的,特别是当前容器环境,jvm可能获取到的是物理机的处理器个数来作为gc线程个数的基准,而实际上容器只是一个2核4G内存的虚拟机,无疑,GC线程数过多,反而对cpu的利用率不高,并且多线程回收时造成无谓的cpu竞争、切换上下文。

诊断性配置

-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:+PrintTenuringDistribution -XX:+UseG1GC -XX:+DisableExplicitGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:ParallelGCThreads=4 -XX:+PrintGCApplicationConcurrentTime -XX:+PrintHeapAtGC -XX:+UnlockDiagnosticVMOptions -XX:+G1PrintRegionLivenessInfo -XX:MaxGCPauseMillis=200 -Xloggc:/export/Logs/gclogs

特点

G1采用分区的思路,用内存分为若干个大小相等的区域,每一块区域都可以为年轻代、老年代服务

并发标记流程

  • 初始标记(STW事件) 普通的young gc会捎带这个步骤。标记 Survivor regions which may 引用了老年代 GC pause (young)(inital-mark)
  • 并发标记阶段 找到所有在heap上的存活对象,这个过程可以被年轻代收集打断
  • 重新标记 完成标记存活对象 in the heap,使用snapshot-at-the-beginning算法
  • 清理(STW + Concurrent事件) 分为3小步:1.执行记录存活对象,并且完全的释放regions;2.刷洗rset;3.重置空的regions,返回给free list,最后这个操作是并发的
  • 复制 将存活的对象copy到未使用的region,年轻代收集或mixed收集都可以做这个事情

最佳实践

  • 不要设置年轻代大小,它会使停顿时间配置无效;无法在需要时扩展年轻代空间
  • XX:MaxGCPauseMillis=,N不要设置成一个100%的目标,应该设置为能满足90%或以上的情况的目标,这是个目标,不能保证总是会被满足,所以设置的时间稍微小一点
  • 防止浮动垃圾导致Full GC,多开点gc线程,-XX:ConcGCThreads=n;提前开始标记;多分配点heap内存给进程

Region

OldEdenSurvivorHumongous(H直接分配到Old,防止反复拷贝移动,大小大于等于region一半的对象)
一个Region的大小可以通过参数-XX:G1HeapRegionSize设定

After the mark phase completes, G1 knows which regions are mostly empty(这里应该说的是被回收后的情况,感觉应该是垃圾比较多的集合). It collects in these regions first, which usually yields a large amount of free space. This is why this method of garbage collection is called Garbage-First.
G1 concentrates its collection and compaction activity on the areas of the heap that are likely to be full of reclaimable objects, that is, garbage

http://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
G1在疏散被标记为可回收的Regions时,将1个或多个Region的对象copy到一个region中,在这个过程中,压缩了和释放干净内存。
这个疏散操作是并行在多处理器上执行的,减少了停顿时间,增加了吞吐量。

参数说明

-XX:MaxGCPauseMillis:每次年轻代垃圾回收的最长时间(最大暂停时间), 如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值.
-XX:GCTimeRatio:设置垃圾回收时间占程序运行时间的百分比,设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。

Card Table

在cms中,为了在并发标记阶段结束后,可以快速找到哪些card有引用更新,因此将老年代内存分为很多个Card,再使用Card Table维护每个card是否在并发标记阶段有引用变化,如果变化了,我们就说这个Card是Dirty的,这样可以提高重新标记阶段的速度

如果引用了发生了变化,则标记1。标记时,采用了bit位的方式标记,大大节省了空间,如byte=3的二进制位0000 0011,从右到左,第1位可以表示dirtyCard,第2位可以表示有引用年轻代,这样在minor gc时,也可以快速找到需要扫描的老年代card。(很多博文上说一个Card管理512Bytes的heap)

RSet

已记忆集合 Remember Set (RSet):一个谁引用了我的机制,记录引用了当前region的分区内对象的卡片索引,当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,来确定本分区内的对象存活情况,如被引用的分区内对象全是垃圾了,则当前分区内的对象可能也是垃圾了。
Remembered Set是辅助GC过程的一种结构,典型的空间换时间工具,和Card Table有些类似。还有一种数据结构也是辅助GC的:Collection Set(CSet),它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。在GC的时候,对于old->young和old->old的跨代对象引用,只要扫描对应的CSet中的RSet即可。
逻辑上说每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。而Card Table则是一种points-out(我引用了谁的对象)的结构,每个Card 覆盖一定范围的Heap(一般为512Bytes)。G1的RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。 这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。

RSet究竟是怎么辅助GC的呢?在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。 而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量。

RSET数据伪结构

[
{"region:[region`s card index]}...
]

rset记录了引用了当前region的其他region,并精确到了card
card table记录了当前region引用了哪些region

G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的。

Young GC

选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。当Eden区无法申请新的内存时,开始Young GC

Mixed GC

选定所有年轻代里的Region,外加根据global concurrentt marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。
当堆(总的堆)的使用比例占总Heap(整个堆,包括young)的比例超过InitiatingHeapOccupancyPercent,默认45之后,就会开始ConcurentMarking, 完成了Concurrent Marking后,G1会从Young GC切换到Mixed GC, 在Mixed GC中,G1可以增加若干个Old区域的Region到CSet中

Full GC

由上面的描述可知,Mixed GC不是full GC,它只能回收部分老年代的Region,如果mixed GC在并发阶段,无法跟上程序分配内存的速度,导致新的对象占满了所有空间,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。我们可以知道,G1是不提供单独full GC的,只提供了降级的单线程 old full gc,这次回收可能是很慢的。

  • CMS的Initial Marking和Remarking两个STW阶段在Heap区越来越大的情况下需要的时间越长,并且由于内存碎片,需要压缩的话也会造成较长停顿时间。所以需要一种高吞吐量的短暂停时间的收集器,而不管堆内存多大。如果没有Full GC,老年代的空间不会压缩整理。
  • TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。
    对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。
  • Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性
    一个对象a,如果在gc前,被一个还未标记完成的对象b引用了,gc过程中,并发执行时,如果应用程序将b对a的引用删除,同时,改为另一个已经标记完成的对象c来引用a,garbage collector可能会漏标a,把它当成一个已经没人其他对象引用的白对象,这时严重的错误,为防止这个问题,gc加入了写屏障,将旧的引用关系b <- a记录下来,这样a还是被b引用,就不会被回收了,但是也产生了浮动垃圾

日志

-XX:G1HeapRegionSize=n
诊断性配置
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:+PrintTenuringDistribution -XX:+UseG1GC -XX:+DisableExplicitGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:ParallelGCThreads=4 -XX:+PrintGCApplicationConcurrentTime -XX:+PrintHeapAtGC -XX:+UnlockDiagnosticVMOptions -XX:+G1PrintRegionLivenessInfo -XX:MaxGCPauseMillis=200 -Xloggc:/export/Logs/gclogs

Java HotSpot(TM) 64-Bit Server VM (25.20-b23) for linux-amd64 JRE (1.8.0_20-b26), built on Jul 30 2014 13:13:52 by "java_re" with gcc 4.3.0 20080428 (Red Hat 4.3.0-8)
Memory: 4k page, physical 263727076k(113563148k free), swap 16777212k(16371860k free)
CommandLine flags: -XX:+DisableExplicitGC  -XX:+G1PrintRegionLivenessInfo -XX:+HeapDumpOnOutOfMemoryError   -XX:InitialHeapSize=2147483648 -XX:MaxHeapSize=2147483648 -XX:MaxMetaspaceSize=268435456 -XX:MetaspaceSize=134217728 -XX:ParallelGCThreads=4 -XX:+PrintGC -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+UnlockDiagnosticVMOptions -XX:+UnlockExperimentalVMOptions -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC 
2018-12-07T14:41:12.624+0800: 0.148: Application time: 0.0834746 seconds
2018-12-07T14:41:12.637+0800: 0.161: Application time: 0.0125164 seconds
2018-12-07T14:41:13.069+0800: 0.593: Application time: 0.4322354 seconds
2018-12-07T14:41:13.169+0800: 0.693: Application time: 0.0993848 seconds
2018-12-07T14:41:13.169+0800: 0.693: Application time: 0.0004720 seconds
2018-12-07T14:41:13.176+0800: 0.700: Application time: 0.0065544 seconds
2018-12-07T14:41:13.186+0800: 0.710: Application time: 0.0103469 seconds
2018-12-07T14:41:13.197+0800: 0.721: Application time: 0.0102892 seconds
2018-12-07T14:41:13.949+0800: 1.473: Application time: 0.7518253 seconds
2018-12-07T14:41:14.481+0800: 2.005: Application time: 0.5315235 seconds
{Heap before GC invocations=0 (full 0):
 garbage-first heap   total 2097152K, used 105472K [0x0000000080000000, 0x0000000100000000, 0x0000000100000000)
  region size 1024K, 102 young (104448K), 0 survivors (0K)  //年轻代一共102个,由于第一次回收,其中全是eden
 Metaspace       used 13539K, capacity 13806K, committed 13952K, reserved 1060864K
  class space    used 1471K, capacity 1563K, committed 1664K, reserved 1048576K
2018-12-07T14:41:14.481+0800: 2.005: [GC pause (G1 Evacuation Pause) (young)   //撤空暂停,表示从eden copy到survior的产生的停顿,survior装不下就会gc
Desired survivor size 6815744 bytes, new threshold 15 (max 15)
, 0.0215033 secs]
   [Parallel Time: 11.1 ms, GC Workers: 4]
	  [GC Worker Start (ms): Min: 2005.0, Avg: 2005.0, Max: 2005.0, Diff: 0.0]
	  [Ext Root Scanning (ms): Min: 0.6, Avg: 0.8, Max: 1.2, Diff: 0.6, Sum: 3.3]  //扫描非堆root用的时间,如classloaders,jni引用,jvm system roots....
	  [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
		 [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
	  [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
	  [Code Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.3, Sum: 0.8]  //从code调用来的root扫描时间  How long it took to scan the roots that came from the actual code: local vars, etc.
	  [Object Copy (ms): Min: 9.7, Avg: 9.9, Max: 10.2, Diff: 0.4, Sum: 39.8] //把存活的对象copy出被收集的regions的时间
	  [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] //判断是否可以安全停止,没有更多的工作需要做的时间
	  [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
	  [GC Worker Total (ms): Min: 11.0, Avg: 11.0, Max: 11.0, Diff: 0.0, Sum: 44.1]  //工作线程一共工作了多次时间
	  [GC Worker End (ms): Min: 2016.0, Avg: 2016.0, Max: 2016.0, Diff: 0.0]
   [Code Root Fixup: 0.7 ms]
   [Code Root Migration: 2.1 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.2 ms]
   [Other: 7.5 ms]
	  [Choose CSet: 0.0 ms]
	  [Ref Proc: 6.8 ms]  //执行非强引用:清理他们或者不需要清理
	  [Ref Enq: 0.1 ms]	//非强引用入队时间
	  [Redirty Cards: 0.0 ms]
	  [Free CSet: 0.2 ms]
   [Eden: 102.0M(102.0M)->0.0B(89.0M) Survivors: 0.0B->13.0M Heap: 103.0M(2048.0M)->22.5M(2048.0M)]
Heap after GC invocations=1 (full 0):
 garbage-first heap   total 2097152K, used 23027K [0x0000000080000000, 0x0000000100000000, 0x0000000100000000)
  region size 1024K, 13 young (13312K), 13 survivors (13312K)
 Metaspace       used 13539K, capacity 13806K, committed 13952K, reserved 1060864K
  class space    used 1471K, capacity 1563K, committed 1664K, reserved 1048576K
}
 [Times: user=0.05 sys=0.01, real=0.03 secs] 

https://liuzhengyang.github.io/2017/06/07/garbage-first-collector/

-XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=256m -XX:+PrintTenuringDistribution -XX:+UseG1GC  -XX:+DisableExplicitGC   -XX:+PrintGCDateStamps -XX:+PrintGCDetails  -XX:ParallelGCThreads=4 -XX:+PrintHeapAtGC


-XX:MaxMetaspaceSize=256m -XX:+PrintTenuringDistribution -XX:+UseG1GC  -XX:+DisableExplicitGC   -XX:+PrintGCDateStamps -XX:+PrintGCDetails  -XX:ParallelGCThreads=4 -XX:+PrintHeapAtGC -Xloggc:/export/Logs/gclogs

2018-07-05T20:02:34.115+0800: 2.949: [GC pause (G1 Evacuation Pause) (young) 表示从eden copy到survior的停顿

-XX:MetaspaceSize=2m时,会有很多mixed gc;
[GC pause (G1 Evacuation Pause) (young)
[GC pause (G1 Evacuation Pause) (mixed)

-XX:MaxMetaspaceSize=16m 2018-07-16T11:43:08.780+0800: [GC pause (Metadata GC Threshold) (young) (initial-mark)

设置-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m 8m是一个不足的内存

2018-07-16T15:55:09.036+0800: [Full GC (Last ditch collection)  1718K->1718K(8192K), 0.0078003 secs]
   [Eden: 0.0B(4096.0K)->0.0B(4096.0K) Survivors: 0.0B->0.0B Heap: 1718.9K(8192.0K)->1718.9K(8192.0K)], [Metaspace: 7989K->7989K(1056768K)]
Heap after GC invocations=1132 (full 809):
 garbage-first heap   total 8192K, used 1718K [0x0000000088e00000, 0x0000000088f00040, 0x0000000100000000)
  region size 1024K, 0 young (0K), 0 survivors (0K)
 Metaspace       used 7989K, capacity 8134K, committed 8192K, reserved 1056768K
  class space    used 926K, capacity 990K, committed 1024K, reserved 1048576K
}
 [Times: user=0.02 sys=0.00, real=0.01 secs]
  • metaspace回收:classloader无效时才会回收,相比于jdk7的permgen空间,metaspace不是一块连续的内存空间,full gc时,会根据classloader是否能回收,来确定相关联的class能否回收,即使相关联的class由于重复创建导致的错误,并未正常给用户使用,并且已满足class回收条件(此时错误class并未引用classloader,在创建class后进行的约束检查中会失败),仍然不会被回收掉。而permgen空间在full gc时,会和老年代一起做可用性分析,如果无引用,会被回收掉,这也是很多应用升级成jdk8产生metaspace oom的原因:一些框架会由于weakhashmap等缓存结构在某些情况下key被回收,导致判断class是否已存在时失败,重复创建class,但又无法通过jvm的约束校验,此时生成了重复的class,在jdk7时会在full gc时被回收,在jdk8中metaspace在full gc时,却没有回收

GC root

  • 虚拟机栈栈帧中的变量引用的对象,扫描栈时,hotspot使用oopmap加速扫描,而不用遍历每个栈帧
  • 类的常量,如果类在,这个常量引用的对象就得在
  • 静态变量引用的对象
  • 本地方法栈中引用的对象

备注

Metadata GC Threshold:metaspace空间不能满足分配时触发,这个阶段不会清理软引用;
Last ditch collection:经过Metadata GC Threshold触发的full gc后还是不能满足条件,这个时候会触发再一次的gc cause为Last ditch collection的full gc,这次full gc会清理掉软引用。
-XX:InitiatingHeapOccupancyPercent是基于整个堆的,而不是某一代
H区域会存放大于RegionSize 一半的大对象,当xms=xmx=4g时,regionSize=2M,region个数=2048

不要设置年轻代大小-Xmn,
设置-XX:NewRatio=1或者2 后,启动时的用 gc变少了,gc平均耗时立即突破200ms,达到209ms,因此,设置NewRatio也会导致MaxGCPauseMillis失效

参考文章

oopmap emeberset - >https://dsxwjhf.iteye.com/blog/2201685

https://blogs.oracle.com/poonam/understanding-g1-gc-logs

原文地址:https://www.cnblogs.com/windliu/p/9317277.html