在介绍垃圾收集器之前,先了解一下评估GC的性能指标
评估GC的性能指标
主要是根据吞吐量和暂停时间评估
- 吞吐量:运行用户代码的时间占总运行时间的比例 。
- (总运行时间:程序的运行时间+内存回收的时间)
- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
- 收集频率:相对于应用程序的执行,收集操作发生的频率。
- 内存占用:Java堆区所占的内存大小。
- 快速:一个对象从诞生到被回收所经历的时间。
标准:一般在暂停时间一定范围内,尽量提高吞吐量:或者相反
如何查看默认的垃圾收集器
-XX:+PrintCommandLineFlags
JDK8默认使用的年轻代:Parallel 老年代 Parallel Old
JDK9默认使用的G1
-XX:G1ConcRefinementThreads=8 -XX:InitialHeapSize=132362624 -XX:MaxHeapSize=2117801984 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
Serial和Serial Old垃圾回收器
简单概述:Serial收集器(所有新生代垃圾收集器都是采用复制算法)串行回收垃圾收集时会Stop-the-World
- 新生代采用复制算法,Stop-The-World
- 老年代采用标记-整理算法,Stop-The-World
优势在于
配置:
-XX:InitialHeapSize=132362624 -XX:MaxHeapSize=2117801984 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC
总结:
- 大家知道,现在都是多核CUP。所有使用范围变小了
- 对于交互较强的应用而言,这种垃圾收集器是无法被接受的。一般在java web应用程序中不会使用Serial
ParNew垃圾回收器
- ParNew收集器其实就是Serial收集器的多线程版本
- Serial和PraNew效率的比较
配置:
- -XX:+UseParNewGC
Parallel和Parallel Old垃圾回收器
吞吐量优先:
参数配置:(了解即可)
-XX:+UseParallelGC
-XX:InitialHeapSize=132362624 -XX:MaxHeapSize=2117801984 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
CMS(Concurrent-Mark-Sweep)垃圾收集器(非常重要)
概述:在JDK1.5时期,HotSpot推出的,它是第一次实现了让垃圾收集线程与用户线程同时工作的 JDK9被废弃,JDK14移除
特点:低延迟 集中在互联网或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,给用户较好的体验
算法:标记-清除
适用于:老年代
工作原理
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,如下
- 初始标记: 暂停所有的其他线程需要"Stop The World",初始标记仅仅标记GC Roots能直接关联到的对象,速度非常快;
- 并发标记:
- 从GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程以前并发进行;
- 重新标记:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因用户程序继续运作而导致标记发生变动的那一部分对象的标记记录,这一阶段的停顿时间比初始标记稍长一些,但远比并发标记短,需要"Stop The World",
- 并发清除: 开启用户线程,同时GC线程开始对为标记的区域做清扫,回收所有的垃圾对象,释放内存空间;
CMS特点和缺点
特点:并发
参数配置:
指定使用CMS收集器 "-XX:+UseConcMarkSweepGC"
问题:
G1垃圾收集器(最重要) 特点:区域化分代式
官方给G1设定的目标是在延迟可控的情况下获取尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望
为什么名字叫做Garbage First(G1)呢?
- 因为G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续)。使用不同的Region来表示Eden、幸存者0区,survivor1区,老年代等。
- G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1 跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
- 由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First)。
G1垃圾收集器的优点
上图:每个Region的大小是相同的且不变,当Region被整理后,可以变为其他代,如:S->O
Humongous是用来存放大对象的,如果一个H存放不了,那么G1会寻找连续的H来存储,如果找不到,就进行FullGC
其他GC收集器相比,G1使用了全新的分区算法,其特点如下:
并行与并发
- 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
- 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况。
分代收集
- 从分代看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区,但从堆的结构上看,它不要求整个Eden区、年轻代、或者老年代都是连续的,也不再坚持固定大小和固定数量。
- 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
- 和之前的各类收集器不同,它同时兼顾年轻代和年老代。其他收集器则工作在年轻代或者工作在老年代;
空间整合
- G1将内存划分为一个个的Region。内存的回收是以Region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-整理(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
可预测的停顿时间模型
- 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
- G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
- 相比于CMS GC,C1未必能做到CMS在最好情况下的延迟停顿,但是比CMS最差情况要好很多。
G1垃圾收集器的缺点
- 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。
- 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。
参数配置:
操作步骤:
应用场景
- 面向服务端应用,针对具有大内存、多处理器的机器;
- 最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;
- 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;(因为只收集部分Region)
具体什么情况下应用G1垃圾收集器比CMS好,可以参考以下几点(但不是绝对):
- 超过50%的Java堆被活动数据占用;
- 对象分配频率或年代的提升频率变化很大;
- GC停顿时间过长(长于0.5至1秒);
HotSpot垃圾收集器里,除了G1外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用程序帮助加速垃圾回收过程。
G1回收器垃圾回收过程
G1 GC的垃圾回收过程主要包括如下三个环节:
- 年轻代 GC (Young GC)
- 老年代并发标记过程 (Concurrent Marking)
- 混合回收 (Mixed GC)
- Full GC(如果需要、单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制)
- 年轻代 GC (Young GC)
- 当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式(STW)收集器。活着的对象移动到Survivor区间或者老年区间。
- 老年代并发标记过程 (Concurrent Marking)
- 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。
- 混合回收 (Mixed GC)
- 标记完成马上开始混合回收过程。对于一个混合回收器,G1 GC从老年区间移动存活对象到空闲区间,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。
Remembered Set
一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?
在其他的分代收集器,也存在这样的问题(而G1更突出):回收新生代也不得不同时扫描老年代?
这样的话会降低Minor GC的效率;
- 解决方法:
无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:
每个Region都有一个对应的Remembered Set;
每次Reference类型数据写操作时,都会产生一个Write Barrier(写屏障)暂时中断操作;
然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);
如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;就可以保证不进行全局扫描,也不会有遗漏。
G1 垃圾回收过程详解
G1回收过程一:年轻代GC
- 第一阶段,扫描根
- 根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同RSet记录的外部引用作为扫描存活对象的入口。
- 第二阶段,更新RSet
- 处理dirty card queue中的card,更新RSet。此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引
- 什么是dirty card queue:对于应用程序的引用赋值语句Object.field=Object,JVM会在之前和之后执行特殊的操作以在dirty card queue中入队一个保存了对象引用信息的card。在年轻代回收的时候,G1会对Dirty Card Queue中所有的card进行处理,以更新RSet,保证RSet实时准确的反映引用关系。
- 第三阶段,处理RSet
- 识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。
- 第四阶段,复制对象
- 此阶段,对象树被遍历,Eden区内存段内存活的对象会被复制到Survivor区中,Survivor区内存活的对象如果年龄未达阈值,年龄会加1,达到阈值会被复制到Old区中。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
- 第五阶段,处理引用
- 处理Soft,Weak,Phantom,Final等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,索引复制过程可以达到内存整理的效果,减少碎片。
G1回收过程二:并发标记过程
- 1.初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。
- 2.根区域扫描(Root Region Scanning):G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在young GC之前完成。
- 并发标记(Concurrent Marking):在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)
- 再次标记(Remark):由于应用程序持续进行,需要修正上一次的标记结果。是STW的,G1中采用了比CMS更快的初始化快照算法:SATB(snapshot-at-the-beginning)
- 独占清理(cleanup,STW):计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是STW的。
- 并发清理阶段:识别并清理完全空闲的区域。
G1回收过程三:混合回收
G1回收可选的过程四:Full GC
G1的初衷就是要避免Full GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应该程序停顿时间会很长。
要避免Full GC的发生,一旦发生需要进行调整。什么时候回发生Full GC呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到full gc,这种情况可以通过增大内存解决。
导致G1 Full GC的原因可能有两个:
- Evacuation(回收阶段)的时候没有足够的to-space来存放晋升的对象;
- 并发处理过程完成之前空间耗尽
G1 优化建议:
总结:
感谢:康师傅^_^