垃圾收集器(四)

下图是HotSpot中垃圾收集器搭配使用方式:有连线说明可以搭配。

Serial收集器

串行收集器是最古老,最稳定以及效率高的收集器。可能会产生较长的停顿。

是一个单线程的收集器,它在进行垃圾收集时,必须暂停其他的所有的工作线程,直到收集结束。“Stop The World”这项工作是有虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户的所有工作线程停掉,这对很多应用来说是难以接受的。Serial是虚拟机运行于Client模式下的默认新生代收集器,简单而高效。

参数控制:

-XX:+UseSerialGC 在新生代和老年代使用串行收集器

-XX:+SurvivorRatio 设置Eden和Survivor区大小的比例

-XX:+PretenureSizeThreshold 直接晋升到年老代的对象大小,设置此参数后,超过该大小的对象直接在年老代中分配内存

 

-XX:+MaxTenuringThreshold 直接晋升到年老代的对象年龄,每个对象在一次Minor GC之后还存活,则年龄加1,当年龄超过该值时进入年老代

ParNew收集器

其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其余包括Serial收集器可用的所有控制参数、收集算法、Stop The World、回收策略等都与Serial收集器完全一样。ParNew收集器在单CPU的环境中绝对不会比Serial收集器效果更好。它是许多运行在Server模式下虚拟机的默认新生代收集器。但HotSpot默认组合是PS/PS MS。

新生代并行,老年代串行

注意除了Serial收集器之外,只有它能与CMS收集器配合工作。(详情见上边的图示)

参数控制:

-XX:+UseParNewGC ParNew收集器

-XX:ParallelGCThreads 限制线程数量

 

关于垃圾收集中的并发和并行,可以解释如下:

并行(Parallel):指多条垃圾收集线程并行工作,用户线程仍处于等待状态。

并发(Concurrent):指用户线程与垃圾收集线程同时执行。

Parallel Scavenge收集器

也是使用复制算法新生代收集器,也是并行的多线程收集器。该收集器关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总耗时时间的比值,及吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行100分钟,其中垃圾收集花掉了1分钟那吞吐量就是99%。停顿时间越短就约适合需要与用户交互的程序,而高吞吐量则可以高效率的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。Parallel Scavenge收集器提供两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

不过不要认为把这个值设置得够小就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间换取的;系统把新生代调小一些,收集时间变短,但也导致垃圾收集发生更频繁,原本10S收集一次,一次100MS,现在5S收集一次,每次70毫秒。停顿时间的确下降,但吞吐量也降下来了。由于与吞吐量关系密切,Parallel Scavenge收集器还有一个参数-XX:UseAdaptiveSizePolicy(自适应调节),这是一个开关参数,打开这个开关后,不需要手动设置新生代大小及比例、晋升老年代对象的年龄等,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略。自适应调节策略也是PS与ParNew收集器的一个重要区别。

Serial Old(PS MarkSweep)收集器

注意:Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集,并非直接使用了Serial Old收集器,但是这个PS MarkSweep收集器与Serial Old的实现非常接近。

JDK1.8默认的垃圾收集器为:PS MarkSweep 和 PS Scavenge。也就是说Java8的默认并不是G1。

 

  该收集器是Serial的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义在于给Client模式下的虚拟机使用。如果在Server模式下,那它有两大用途:

1.在JDK1.5以前与PS收集器搭配使用,至今Jvm虚拟机默认的老年代收集器

2.作为CMS收集器后备预案,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old收集器

该收集器是PS收集器的老年代版本,使用多线程和“标记-整理”算法。该收集器在JDK1.6中才开始提供。在此以前,新生代的PS收集器在老年代只能与Serial Old搭配(为什么不能搭CMS?)。由于SO是单线程收集方式,导致PS+SO的吞吐量不一定强过ParNew+CMS。 直到Parallel Old 收集器出现后,“吞吐量优先”收集器有了名副其实的组合。在注重吞吐量及CPU资源敏感的场合,都优先考虑PS + POJDK1.8默认就是这个

CMS(Concurrent Mark Sweep)收集器:

CMS的特点:并发收集,低停顿。是一种以获取最短回收停顿时间为目标的收集器,以获取用户交互体验。该收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器更复杂一些,整个过程分为4个步骤:

1.初始标记(initial mark)

2.并发标记(concurrent mark)

3.重新标记(remark)

4.并发清除(concurrent sweep)

初始标记重新标记步骤仍然需要“Stop The World”,初始标记只是标记一下GC Roots能直接关联到的对像,速度很快。

并发标记阶段就是进行GC Roots Tracing的过程。

重新标记阶段则是为了修正并发标记期间因用户继续运作导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间比初始标记稍长,但远比并发标记短。由于整个过程中最耗时的并发标记和并发清除过程收集器线程都可以与用户线程一起工作。所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的

有三个明显的缺点:

1)CMS收集器对CPU资源非常敏感,由于开启一部分收集线程,有可能占用CPU的大量时间,导致应用变慢。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程占不少于25%的CPU资源。并随CPU数量增加而减少(不知道计算公式)。但是CPU不足4个时,CMS对用户程序的影响就可能变得很大,如果CPU负载本身比较大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%。总结,CMS在多CPU硬件配置系统中会收到更好的效果,反之效果变差

 

2)CMS收集器无法处理浮动垃圾(Floating Garbage), 可能出现”Concurrent Mode Failure“失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,还会有新的垃圾不断产生,这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉他们,只能在下次GC时再清理掉。这部分垃圾称为“浮动垃圾”。对于浮动垃圾,仍需要预留出足够的内存空间给用户使用,因此CMS收集器不能像其他收集器那样等到老年代几乎被填满了再进行收集,而是需要为“浮动垃圾”预留空间。在JDK1.5默认设置下,CMS下老年代使用68%的空间就会进行GC。在JDK1.6中,CMS收集器的启动阈值已经提升到了92%。如果CMS运行期间预留的内存无法满足“浮动垃圾”,就会出现“Concurrent ModeFailure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了

 

  1. CMS是一款基于“标记-清除”算法实现的收集器,这就意味着收集结束时会出现大量的空间碎片产生。可能出现当剩余空间很大,但没有连续的足够大的空间给一个新对象时,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供一个:-XX +UseCMSCompactAtFullCollection开关(默认开启,换言之,默认下CMS还是会进行碎片整理),用于在CMS收集器进行FullGC时开启内存碎片的合并整理过程。内存整理的过程是无法并发的,空间碎片的问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了一个参数:-XXCMSFullGCsBeforeCompacting,这个参数用于设置多少次不压缩的GC后,来一次带压缩的(默认为0,表示每次进入Full GC都进行碎片整理)。

面试题:CMS一共会有几次STW?

首先,回答两次,初始标记和重新标记需要。

然后,CMS并发的代价是预留空间给用户,预留不足的时候触发FUllGC,这时Serail Old会STW。

然后,CMS是“标记-清除”算法,导致空间碎片,则没有连续空间分配大对象时,FUllGC,而FUllGC会开始碎片整理,SWT。

即2次或者多次。

CMS什么时候Full GC

除了直接调用System.gc外,触发Full GC执行的情况有如下四种。

  1. 旧生代空间不足。

旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:

java.lang.OutOfMemoryError: Java heap space

为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

  1. Permanet Generation空间满。

PermanetGeneration中存放的为一些Class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:

java.lang.OutOfMemoryError: PermGen space

为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。

  1. CMS GC时出现promotion failed和concurrent mode failure。

对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。

promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。

应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。

  1. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间。

这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。

例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。

当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。

除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

G1(Gabage-First)收集器

G1是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换*JDK1.5中发布的CMS收集器,从局部看是基于“复制”算法实现的,意味着G1*不会产生内存空间碎片。与其他GC收集器相比,G1最大的特点在于:可预测的停顿,这是G1相对于CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

 

        使用G1收集器时,Java堆的内存被划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代概念,但是新生代和和老年代不再是物理隔离的了,他们都是一部分Region的集合。G1之所有能建立可预测的停顿时间模型,是因为他有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面垃圾的价值大小(回收所得空间大小以及回收所需时间的经验值),在后台维护一个优先列表。每次根据允许的收集时间,优先回收价值最大的Region。这种区域划分内存空间以及优先级的区域回收方式,保证了G1收集器在有限时间内可以获取尽可能高的收集效率。

 

G1把内存“化整为零”的思路实现细节远远没有理解那么简单,从2004年时便已经提出,花费近10年时间才开发出G1的商用版。其中:把Java堆分为多个Region后,垃圾收集是否就能以Region为单位进行?其实不然:Region不可能是孤立的。一个对象分配在某个Region中,他并非只能被本Region中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性分析确定对象是否存活时,岂不是还得扫描整个Java堆才能保证准确性?这个问题并非在G1中才有,只是G1更加突出而已。在之前的收集器中,如果新生代的对象有到老年代的关联,那么回收新生代时就不得不同时扫描老年代的话,那么Minor GC的效率会下降。而在具体实现中,虚拟机都是使用Remembered Set来避免全堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在堆Reference类型的对象进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reeference引用的对象是否处于不同的Region之中(在之前的收集器中就是检查是否老年代中的对象是否引用了新生代中的对象)。如果是,便通过CardTable把相关信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

 

G1收集器的运作大致分为:

 

1.初始标记(Initial Marking)

2.并发标记(Concurrent Marking)

3.最终标记(Final Marking)

4.筛选回收(LiveData Counting and Evacuation)

 

G1的前几个步骤的运作过程和CMS有很多相似之处。初始阶段只是标记一下GC Roots能直接关联到的对象。并发标记是从GC Roots开始对堆中对象进行可达性分析。这个耗时较长,但是能与用户程序并发执行。最终标记阶段则是为了修正在并发阶段期间因为程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set中,这阶段需要线程停顿,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

JVM (Java Virtual Machine) G1 (Garbage-First) 垃圾收集器是一种用于 Java 应用程序的垃圾收集算法。它是自JDK 7u4版本后引入的一种全新的垃圾收集器。 G1垃圾收集器的设计目标是为了解决传统的分代垃圾收集器可能遇到的一些问题,如停顿时间长、内存碎片化等。它采用了一种基于区域的垃圾收集方式,可以将内存划分为多个大小相等的区域,每个区域可以是Eden、Survivor或Old区。 G1垃圾收集器的工作原理如下: 1. 初始标记(Initial Mark):标记所有从根对象直接可达的对象。 2. 并发标记(Concurrent Mark):在并发执行程序的同时,标记那些在初始标记阶段无法访问到的对象。 3. 最终标记(Final Mark):为并发标记阶段中发生改变的对象进行最终标记。 4. 筛选回收(Live Data Counting and Evacuation):根据各个区域的回收价值来优先回收价值低的区域。 G1垃圾收集器具有以下特点: - 并发执行:在执行垃圾收集过程时,尽可能减少应用程序的停顿时间。 - 分区回收:将整个堆划分为多个区域,可以根据需要优先回收垃圾较多的区域,从而避免全堆回收带来的长时间停顿。 - 内存整理:G1垃圾收集器会对内存进行整理,减少内存碎片化,提高内存利用率。 需要注意的是,G1垃圾收集器并不适用于所有情况。在特定的场景下,如大堆情况下的长时间运行、对延迟要求非常高的应用等,可能需要考虑其他垃圾收集器的使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值