深入理解JVM—经典垃圾收集器

收集算法是内存回收的方法论,垃圾回收器是内存回收的具体实现

GC演化:随着内存大小的不断增加而演进

 

Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

Serial(串行)收集器是最基本,历史最悠久的垃圾收集器。单线程工作的收集器,“单线程”不仅仅意味着它只会使用一个处理器或一条垃圾收集线程去完成垃圾回收工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(STW),直到它收集结束

新生代采用标记复制算法,老年代采用标记整理算法。

 

Serial依然是HotSpot虚拟机运⾏在客户端模式下的默认新⽣代收集器,优点是简单而⾼效(与其他收集器的单线程相⽐),对于内存资源受限环境,它是所有收集器⾥额外内存消耗(Memory Footprint)最⼩的; 对于单核处理器或处理器核⼼数较少的环境来说,Serial收集器由于没有线程交互的开销,专⼼做垃圾收集自然可以获得最⾼的单线程收集效率。

SerialOld这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使⽤。如果在服务端模式下,它也可能有两种⽤途: ⼀种是在JDK5以及之前的版本中与Parallel Scavenge收集器搭配使⽤,另外⼀种就是作为CMS收集器发⽣失败时的后备预案,在并发收集发⽣Concurrent Mode Failure时使⽤。

Parallel Scavenge收集器(-XX:+UseParallelGC -XX:+UseParallelOldGC)

Parallel收集器是一个并行收集的多线程收集器。默认的收集线程数和CPU核数相同,也可通过参数修改(一般不推荐修改)。

新生代采用标记复制算法,老年代采用标记整理算法。

 

Parallel收集器关注点是达到一个可控制的吞吐量高效的利用CPU,吞吐量(Throughput)就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值)所以Parallel收集器也被称为“吞吐量优先收集器”。而CMS等垃圾收集器的关注点更多的是尽可能的缩短垃圾收集时用户线程的停顿时间(提高用户体验)。   

 

如果虚拟机完成某个任务,⽤户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。停顿时间越短就越适合需要与⽤户交互或需要保证服务响应质量的程序,良好的响应速度能提升⽤户体验; ⽽⾼吞吐量则可以最⾼效率地利⽤处理器资源尽快完成程序的运算任务,主要适合在后台运算⽽不需要太多交互的分析任务。

ParNew收集器(-XX:+UseParNewGC)

并行多线程收集器,与Parallel收集器很类似,区别在于它可以和CMS收集器配合工作

新生代采用标记复制算法,老年代采用标记整理算法。

 

它是不少运⾏在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中⾸选的新⽣代收集器,其中有⼀个与功能、性能无关但其实很重要的原因是: 除了 Serial收集器外,⽬前只有它能与CMS收集器配合⼯作。

从JDK9开始,ParNew和CMS从此只能互相搭配使⽤,再也没有其他收集器能够和它们配合了。ParNew合并⼊CMS,成为它专⻔处理新⽣代的组成部分。

·并⾏(Parallel): 并⾏描述的是多条垃圾收集器线程之间的关系,说明同⼀时间有多条这样的线程在协同⼯作,通常默认此时⽤户线程是处于等待状态。

·并发(Concurrent): 并发描述的是垃圾收集器线程与用户线程之间的关系,说明同⼀时间垃圾收集器线程与⽤户线程都在运⾏。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占⽤了⼀部分系统资源,此时应用程序的处理的吞吐量将受到⼀定影响。

CMS收集器(-XX:+UseConcMarkSweepGC(old))

CMS(Concurrent Mark Sweep)收集器是⼀种以获取最短回收停顿时间为⽬标的收集器。

⽬前很⼤⼀部分的Java应⽤集中在互联⽹⽹站或者基于浏览器的B/S系统的服务端上,这类应⽤通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给⽤户带来良好的交互体验。CMS收集器就⾮常符合这类应⽤的需求。

CMS收集器只能用在老年代,基于标记-清除算法实现。

 

1.初始标记:暂停其他线程(STW),记录GC Roots直接能引用的对象,速度很快。

2.并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,过程较长这个过程耗时较长,但是不需要停顿⽤户线程,可以与垃圾收集线程⼀起并发运⾏。因为用户程序继续运行,可能导致已经标记过的对象状态发生改变。

3.重新标记:STW,修正并发标记期间,因用户程序继续运作而导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,远远比并发标记阶段的时间短;主要用到三色标记里的增量更新算法,做重新标记。

4.并发清除:开启用户线程,同时GC线程开始清理删除掉标记阶段判断的已经死亡的对象。这个阶段如果有新增对象会被标记成黑色不做任何处理。

5.并发重置:重置本次GC过程中的标记数据。

从CMS的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面几个明显的缺点:

1.对CPU资源敏感(会和服务抢资源) 事实上,面向并发设计的程序都对处理器资源⽐较敏感。在并发阶段,它虽然不会导致⽤、用户线程停顿,但却会因为占用了⼀部分线程(或者说处理器的计算能⼒)⽽导致应用程序变慢,降低总吞吐量.

2,无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾及只能等到下一次GC再清理了);

3.它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片立生,当然通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理

4.执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,要是CMS运⾏期间预留的内存无法满足程序分配新对象的需要,也许没回收完就再次触发full GC,也就是concurrent mode failure(并发失败),此时会进入STW,用Serial old垃圾收集器来回收,但这样停顿时间就很长了。

三色标记算法

在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。

引入“三色标记”,把可达性分析遍历对象过程中遇到的对象,按照“是否访问过"这个条件标记成以下三种颜色:

·黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。

·灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

·白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。

 

多标-浮动垃圾

在并发标记过程中,如果由于方法运行结束导致部分局部变量(gc root)被销毁,这个gc root号引用的对象之前又被扫描过(被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为浮动垃圾。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。

另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分

漏标-读写屏障

漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)。

增量更新就是当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。

原始快照(STAB)就是当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮GC清理中能存活下来,待下一轮GC的时候重新扫描,这个对象也有可能是浮动垃圾),这可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那⼀刻的对象图快照来进⾏搜索。

以上无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的。

如CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现。

Garbage First收集器: 

G1将Java堆划分为多个大小相等的独立区域(Region),每⼀个Region都可以根据需要,扮演新⽣代的Eden空间、Survivor空间,或者⽼年代空间。收集器能够对扮演不同⻆⾊的Region采⽤不同的策略去处理。G1保留了年轻代和老年代的概念,它们都是(可以不连续)Region的集合。

一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。Region中还有⼀类特殊的Humongous区域,专门用来存储⼤对象。G1认为只要⼤⼩超过了⼀个Region容量⼀半的对象即可判定为⼤对象。

G1可以面向堆内存任何部分来组成回收集(Collection Set,⼀般简称CSet)进⾏回收,衡量标准不再是它属于哪个分代,⽽是哪块内存中存放的垃圾数量最多,回收收益最⼤,这就是G1收集器的Mixed GC模式。

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),比如一个Region:花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可能尽可能高的收集效率。

 

 

G1收集器一次GC的运作过程大致分为以下几个步骤:

·初始标记(initial mark,STW):暂停所有的其他线程,并记录下GC roots直接能引用的对象,速度很快;

·并发标记(Concurrent Marking):从GC Root开始对堆中对象进⾏可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较⻓,但可与⽤户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引⽤变动的对象

·最终标记(Remark,STW):对用户线程做另⼀个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。

·筛选回收(Cleanup,STW):负责更新Region的统计数据,对各个Region的回收价值和成本进⾏排序,根据⽤户所期望的停顿时间来制定回收计划,可以⾃由选择任意多个Region构成回收集,然后把决定回收的那⼀部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这⾥的操作涉及存活对象的移动,是必须暂停⽤户线程,由多条收集器线程并⾏完成的。 

G1垃圾收集分

Young GC

Young GC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数-XX:MaxGCPauseMills设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数-XX:MaxGCPauseMills设定的值,那么就会触发Young GC

MixedGC

不是Full GC,老年代的堆占有达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次。

Full GC

停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region:来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)

CMS和G1的比较:

相⽐CMS,G1的优点有很多,暂且不论可以指定最⼤停顿时间分Region的内存布局按收益动态确定回收集这些创新性设计带来的红利,单从最传统的算法理论上看,G1也更有发展潜⼒。与CMS 的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法 实现的收集器,但从局部(两个Region之间)上看⼜是基于“标记-复制”算法实现,⽆论如何,这两种算法都意味着G1运作期间不会产⽣内存空间碎⽚,垃圾收集完成之后能提供规整的可⽤内存。这种特性有利于程序长时间运⾏,在程序为大对象分配内存时不容易因⽆法找到连续内存空间⽽提前触发下⼀次收集。

不过,G1相对于CMS仍然不是占全⽅位、压倒性优势的,从它出现几年仍不能在所有应用场景中代替CMS就可以得知这个结论。比起CMS,G1的弱项也可以列举出不少,如在⽤户程序运⾏过程中,G1⽆论是为了垃圾收集产⽣的内存占⽤(Footprint)还是程序运⾏时的额外执⾏负载(Overload)都要⽐CMS要⾼。

目前在⼩内存应⽤上CMS的表现⼤概率仍然要会优于G1,⽽在⼤内存应⽤上G1则⼤多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB⾄8GB之间。

为什么G1用SATB? CMS用增量更新?

SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。

收集器的权衡:

看更关注吞吐量还是停顿时间

1.优先调整堆的大小让服务器自己来选择

2.如果内存小于100M,使用串行收集器

3.如果是单核,并且没有停顿时间的要求,串行或JVM自己选择

4.如果允许停顿时间超过1秒,选择并行或者JVM自己选

5,如果响应时间最重要,并且不能超过1秒,使用并发收集器

6.4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

欣欣uvo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值