G1总结
- 多数情况下对 Java 程序进行调优, 主要关注两个目标之一: 响应速度(responsiveness) 和 吞吐量(throughput)。
吞吐量关注在一个特定时间段内应用系统的最大工作量。
a 给定时间内完成的事务数.
b 每小时批处理系统能完成的作业(jobs)数量.
c 每小时能完成多少次数据库查询
2、可以像CMS收集器一样,GC操作与应用的线程一起并发执行
紧凑的空闲内存区间且没有很长的GC停顿时间.
需要可预测的GC暂停耗时.
不想牺牲太多吞吐量性能.
3、 G1是一款压缩型的收集器.G1通过有效的压缩完全避免了对细微空闲内存空间的分配,不用依赖于regions,这不仅大大简化了收集器,而且还消除了潜在的内存碎片问题。除压缩以外,G1的垃圾收集停顿也比CMS容易估计,也允许用户自定义所希望的停顿参数。
4、 G1在全局标记阶段(global marking phase)并发执行, 以确定堆内存中哪些对象是存活的。标记阶段完成后,G1就可以知道哪些heap区的empty空间最大。它会首先回收这些区,通常会得到大量的自由空间. 这也是为什么这种垃圾收集方法叫做Garbage-First(垃圾优先)的原因
5、 G1分为两个阶段: 并发阶段(concurrent, 与应用线程一起运行, 如: 细化 refinement、标记 marking、清理 cleanup) 和 并行阶段(parallel, 多线程执行, 如: 停止所有JVM线程, stop the world). 而 FullGC(完整垃圾收集)仍然是单线程的, 但如果进行适当的调优,则应用程序应该能够避免 full GC。
6、如果从 ParallelOldGC 或者 CMS收集器迁移到 G1, 您可能会看到JVM进程占用更多的内存(a larger JVM process size). 这在很大程度上与 “accounting” 数据结构有关, 如 Remembered Sets 和 Collection Sets.
Remembered Sets 简称 RSets, 跟踪指向某个heap区内的对象引用. 堆内存中的每个区都有一个 RSet. RSet 使heap区能并行独立地进行垃圾集合. RSets的总体影响小于5%.
Collection Sets 简称 CSets, 收集集合, 在一次GC中将执行垃圾回收的heap区. GC时在CSet中的所有存活数据(live data)都会被转移(复制/移动). 集合中的heap区可以是 Eden, survivor, 和/或 old generation. CSets所占用的JVM内存小于1%.
7、CMS --> 年轻代(Young generation)分为 1个新生代空间(Eden)和2个存活区(survivor spaces). 老年代(Old generation)是一大块连续的空间, 垃圾回收(Object collection)就地解决(is done in place), 除了 Full GC, 否则不会进行压缩(compaction) -- > 这就是cms 为什么存在内存碎片的原因。
CMS 老年代过程:
阶段
|
说明
|
(1) 初始标记 (Initial Mark)
|
(Stop the World Event,所有应用线程暂停) 在老年代(old generation)中的对象, 如果从年轻代(young generation
)中能访问到, 则被 “标记,marked” 为可达的(reachable).对象在旧一代“标志”可以包括这些对象可能可以从
年轻一代。暂停时间一般持续时间较短,相对小的收集暂停时间.
|
(2) 并发标记 (Concurrent Marking)
|
在Java应用程序线程运行的同时遍历老年代(tenured generation)的可达对象图。扫描从被标记的对象开始,直到
遍历完从root可达的所有对象. 调整器(mutators)在并发阶段的2、3、5阶段执行,在这些阶段中新分配的所有对象
(包括被提升的对象)都立刻标记为存活状态.
|
(3) 再次标记(Remark)
|
(Stop the World Event, 所有应用线程暂停) 查找在并发标记阶段漏过的对象,这些对象是在并发收集器完成
对象跟踪之后由应用线程更新的.
|
(4) 并发清理(Concurrent Sweep)
|
回收在标记阶段(marking phases)确定为不可及的对象. 死对象的回收将此对象占用的空间增加到一个
空闲列表(free list),供以后的分配使用。死对象的合并可能在此时发生. 请注意,存活的对象并没有被移动.
|
(5) 重置(Resetting)
|
清理数据结构,为下一个并发收集做准备.
|
8、 CMS的老年代回收(Old Generation Collection)
两次stop the world事件发生在: 初始标记(initial mark)以及重新标记(remark)阶段. 当老年代达到一定的占有率时,CMS垃圾回收器就开始工作.
(1) 初始标记(Initial mark)阶段的停顿时间很短,在此阶段存活的(live,reachable,可及的) 对象被记下来.
(2) 并发标记(Concurrent marking)在程序继续运行的同时找出存活的对象. 最后, 在第(3)阶段(remark phase), 查找在第(2)阶段(concurrent marking)中错过的对象.
9、cms老年代回收 - 并发清理(Concurrent Sweep)
在前面阶段未被标记的对象将会就地释放(deallocated in place). 此处没有压缩(compaction).
10、G1 的堆空间被划分成多个heap 区域,(E、S、O、H 区域) 设计成 heap 区的目的是为了并行地进行垃圾回收(的同时停止/或不停止其他应用程序线程).
11、G1中的一次年轻代GC
存活的对象被转移(copied or moved)到一个/或多个存活区(survivor regions). 如果存活时间达到阀值,这部分对象就会被提升到老年代
此时会有一次 stop the world(STW)暂停. 会计算出 Eden大小和 survivor 大小,给下一次年轻代GC使用. 清单统计信息(Accounting)保存了用来辅助计算size. 诸如暂停时间目标之类的东西也会纳入考虑
12、G1的年轻代收集归纳如下:
堆一整块内存空间,被分为多个heap区(regions).
年轻代内存由一组不连续的heap区组成. 这使得在需要时很容易进行容量调整.
年轻代的垃圾收集,或者叫 young GCs, 会有 stop the world 事件. 在操作时所有的应用程序线程都会被暂停(stopped).
年轻代 GC 通过多线程并行进行.
存活的对象被拷贝到新的 survivor 区或者老年代.
13、
-XX:MaxGCPauseMillis=n
|
设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal),
JVM 会尽量去达成这个目标.
|
14、G1 老年代
和 CMS 收集器相似, G1 收集器也被设计为用来对老年代的对象进行低延迟(low pause)的垃圾收集. 下表描述了G1收集器在老年代进行垃圾回收的各个阶段.
G1 收集器在老年代堆内存中执行下面的这些阶段. 注意有些阶段也是年轻代垃圾收集的一部分.
阶段
|
说明
|
(1) 初始标记(Initial Mark)
|
(Stop the World Event,所有应用线程暂停) 此时会有一次 stop the world(STW)暂停事件. 在G1中,
这附加在(piggybacked on)一次正常的年轻代GC. 标记可能有引用指向老年代对象的survivor区
(根regions).
|
(2) 扫描根区域(Root Region Scanning)
|
扫描 survivor 区中引用到老年代的引用. 这个阶段应用程序的线程会继续运行. 在年轻代GC可能
发生之前此阶段必须完成.
|
(3) 并发标记(Concurrent Marking)
|
在整个堆中查找活着的对象. 此阶段应用程序的线程正在运行. 此阶段可以被年轻代GC打断
(interrupted).
|
(4) 再次标记(Remark)
|
(Stop the World Event,所有应用线程暂停) 完成堆内存中存活对象的标记. 使用一个叫做
snapshot-at-the-beginning(SATB, 起始快照)的算法, 该算法比CMS所使用的算法要快速的多.
|
(5) 清理(Cleanup)
|
(Stop the World Event,所有应用线程暂停,并发执行)
在存活对象和完全空闲的区域上执行统计(accounting). (Stop the world)
擦写 Remembered Sets. (Stop the world)
重置空heap区并将他们返还给空闲列表(free list). (Concurrent, 并发)
|
(*) 拷贝(Copying)
|
(Stop the World Event,所有应用线程暂停) 产生STW事件来转移或拷贝存活的对象到新的未使用
的heap区(new unused regions). 只在年轻代发生时日志会记录为 `[GC pause (young)]`.
如果在年轻代和老年代一起执行则会被日志记录为 `[GC Pause (mixed)]`.
|
15、老年代GC(Old Generation GC)总结
总结下来,G1对老年代的GC有如下几个关键点:
并发标记清理阶段(Concurrent Marking Phase)
活跃度信息在程序运行的时候被并行计算出来
活跃度(liveness)信息标识出哪些区域在转移暂停期间最适合回收.
不像CMS一样有清理阶段(sweeping phase).
再次标记阶段(Remark Phase)
使用的 Snapshot-at-the-Beginning (SATB, 开始快照) 算法比起 CMS所用的算法要快得多.
完全空的区域直接被回收.
拷贝/清理阶段(Copying/Cleanup Phase)
年轻代与老年代同时进行回收.
老年代的选择基于其活跃度(liveness).
16、 要启用 G1 收集器请使用: -XX:+UseG1GC
17.TLAB
JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
也就是说,Java中每个线程都会有自己的缓冲区称作TLAB(Thread-local allocation buffer),每个TLAB都只有一个线程可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。
关于对象分配的JDK源码可以参见JVM 之 Java对象创建[初始化]中对OpenJDK源码的分析。
18、java 对象的分配过程
- 编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是在堆上分配,则进入选项2.
- 如果tlab_top + size <= tlab_end,则在在TLAB上直接分配对象并增加tlab_top 的值,如果现有的TLAB不足以存放当前对象则3.
- 重新申请一个TLAB,并再次尝试存放当前对象。如果放不下,则4.
- 在Eden区加锁(这个区是多线程共享的),如果eden_top + size <= eden_end则将对象存放在Eden区,增加eden_top 的值,如果Eden区不足以存放,则5.
- 执行一次Young GC(minor collection)。
- 经过Young GC之后,如果Eden区任然不足以存放当前对象,则直接分配到老年代。
对象不在堆上分配主要的原因还是堆是共享的,在堆上分配有锁的开销。无论是TLAB还是栈都是线程私有的,私有即避免了竞争(当然也可能产生额外的问题例如可见性问题),这是典型的用空间换效率的做法。
-XX:MaxGCPauseMillis=200 指定期望的停顿时间
G1 使用了停顿预测模型来满足用户指定的停顿时间目标 ,G1 采用增量回收的方式,每次回收一些区块,而不是整堆回收。