《深入理解Java虚拟机》读书笔记(三)--垃圾收集器与内存分配策略(下)

目录

一、垃圾收集器

1.1 Serial收集器

1.2 ParNew收集器

1.3 Parallel Scavenge

1.4 Serial Old收集器

1.5 Parallel Old收集器

1.6 CMS收集器

1.7 G1收集器

二、GC日志

三、GC分类

四、对象分配与空间分配担保

五、总结


一、垃圾收集器

Java虚拟机规范中对垃圾收集器应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己应用的特点和要求组合出各个分代所使用的收集器。这里总结的收集器是基于JDK 1.7Updaate 14之后的HotSpot虚拟机。这个虚拟机包含的所有收集器如下所示:

HotSpot虚拟机的垃圾收集器(网图)

垃圾收集器中的并发与并行:

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

并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行,比如单CPU环境),用户程序在继续运行,而垃圾收集线程运行于另一个CPU(多CPU环境)。

1.1 Serial收集器

单线程垃圾收集器,是最基本、发展历史最悠久的垃圾收集器。只会使用一个CPU或一条收集线程去完成GC工作,并且它在工作时必须暂停其它所有的工作线程,直到它GC结束。用于新生代垃圾收集,采取复制算法

它有着优于其它收集器的地方:简单而高效(与其它收集器的单CPU环境相比)。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做GC工作,自然可以获得最高的单线程效率。在桌面应用场景中,分配给JVM的内存一般来说不会很大,停顿时间完全可以控制在一百多毫秒以内,只要不是频繁发生,那么是可以接受的。所以Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

1.2 ParNew收集器

ParNew收集器可以看做是Serial收集器的多线程版本,默认开启的收集线程数与CPU的数量相同,同时可以使用-XX:ParallelGCThreads参数限制垃圾收集的线程数。除了使用多条线程进行垃圾收集之外,其余行为都与Serial收集器一样。在实现上,这两种收集器也共用了相当多的代码。用于新生代垃圾收集,采取复制算法

它是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中一个重要的原因是,除了Serial收集器之外,目前只有它能与CMS收集器配合工作。

事实上,ParNew收集器在单CPU环境中不会比Serial收集器有更好的效果,甚至由于存在线程交互开销,该收集器在通过超线程技术实现的两个CPU环境中都不能保证可以超越Serial收集器。不过随着CPU数量的增加,它对于GC时系统资源的利用还是很有好处的。

1.3 Parallel Scavenge

多线程垃圾收集器,采取复制算法,用于新生代。Parallel Scavenge收集器的关注点于其它收集器不同,它的目标是达到一个可控制的吞吐量(Throughput),也经常称为“吞吐量优先”收集器。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中GC花掉1分钟,那吞吐量就是99%。

停顿时间越短越适合需要与用户交互的程序,良好的响应速度能提升用户体验;而高吞吐量则可以高效率利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge提供两个参数用于精确控制吞吐量:

-XX:MaxGCPauseMillis:大于0的毫秒数,收集器会尽可能保证GC花费时间不超过设定值。但是牺牲了吞吐量和新生代空间:系统把新生代调小一些,内存越小,GC停顿也就越短,但由于内存变小了,那么GC也会更加频繁,最终导致吞吐量下降,虽然单次GC停顿时间也的确在下降。更加关注最大停顿时间可以设置此参数。

-XX:GCTimeRatio:大于0且小于100的整数,表示GC时间占总时间的比率,相当于吞吐量的倒数。如果参数设置为19(1:19),那允许的最大GC时间占总时间的5%(即1 / (1 + 19)),默认值为99(1:99),表示允许最大1%(1 / (1 + 99))的GC时间。更加关注吞吐量可以设置此参数。

另外还有一个开关参数,-XX:+UseAdaptiveSizePolicy,这个参数打开之后,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整新生代大小、Eden和Survivor的比例、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数,无需手动设置,这种调节方式称为GC自适应的调节策略。自适应调节策略也是Parallel Scavenge收集器和ParNew收集器的一个重要区别。

1.4 Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法。同样也是主要给Client模式下的虚拟机使用。

1.5 Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程标记-整理算法,在JDK 1.6中开始提供。在此之前,如果新生代选择了Parallen Scavenge收集器,那么老年代只能选择Serial Old(PS MarkSweep)收集器(Parallel Scavenge收集器架构中本身有PS MarkSweep收集器进行老年代收集,并非直接使用Serial Old收集器,只是两者实现非常接近,书中直接以Serial Old代替PS MarkSweep进行讲解)。

注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Scavenge+Parallel Old的组合。

1.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,其主要优点就是并发收集低停顿。适合重视服务响应速度的应用的服务器。基于标记-清除算法,用于老年代。算法的整个过程分为四个步骤:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

其中,初始标记和重新标记仍然需要“Stop The World”。初始标记只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记阶段就是进行GC Roots Tracing的过程;而重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于在整个过程耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

缺点:

1.对CPU资源非常敏感。实际上,面向并发设计的程序都对CPU资源比较敏感。在并发阶段虽然不会导致用户线程暂停,但会因占用了一部分线程(或者说CPU资源)而导致应用程序变慢。CMS默认启动的回收线程数为:(CPU数量+3)/ 4。

2.无法处理浮动垃圾(Floating Garbage)。由于在CMS并发清理阶段用户线程还在运行中,同时也就会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,所以CMS无法在当次收集中处理它们,只有待下一次GC时再清理,这一部分垃圾称之为浮动垃圾。同时,由于GC阶段用户程序需要运行,还需要给用户线程预留足够的内存空间,不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,可以使用参数:-XX:CMSInitiatingOccupancyFraction配置触发百分比。如果CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,JVM将启动后备预案:临时启用Serial Old收集老年代,这样停顿时间就长了。所以该参数如果设置的太高容易导致大量的“Concurrent Mode Failure”,性能反而降低。

3.CMS基于标记-清除算法,收集结束会有大量的空间碎片产生。如果空间碎片过多,将会影响大对象的分配,往往会出现老年代还有很大空间剩余,但是由于无法找到足够大的连续空间,而不得不触发Full GC。为了解决这个问题,CMS提供了一个参数-XX:UseCMSCompactAtFullCollection,用于在顶不住要进行Full GC时整理内存碎片,整理过程无法并发,所以停顿时间就变长了。同时还有另外一个参数-XX:CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的,默认值为0,表示每次进入Full GC都进行碎片整理。

1.7 G1收集器

G1(Garbage-First)收集器是一款面向服务端的垃圾收集器。具备以下特点:

  • 并发与并行:能充分利用多CPU,多核环境下的硬件优势,多个GC线程并行处理,缩短STW时间,同时可通过并发的方式让Java程序继续执行。
  • 分代收集:保留了分代的概念,不需要其他收集器配合就能独立管理整个堆。它将整个Java堆划分为多个大小相等的独立区域(Region),虽然保留了新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,他们都是一部分Region(不需要连续)的集合。
  • 空间整合:从整体上看,基于标记-整理算法,从局部(两个Region)上看,是基于复制算法的。两种算法都不会产生内存碎片,没有CMS的内存碎片问题。
  • 可预测的停顿:降低停顿时间是G1和CMS共同的关注点,但是G1除此之外,还能建立可预测的停顿时间模型,能让使用者明确在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒。G1之所以能做到这点,是因为它会跟踪各个Region里面的垃圾堆积的价值大小(回收所获空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(Garbage-First名称的来由),可以有计划的避免在整个Java堆中进行全区域的垃圾收集。

一个对象分配在某个Region中,它可能与整个Java堆任意的对象发生引用关系,那如何做可达性分析呢(这个问题在之前的分代收集中也存在,只是这里更加突出)?当然全堆扫描能解决问题,不过明显不可取。实际上JVM是使用了Rememberd Set来避免全堆扫描:G1中每个Region都有一个对应的Rememberd Set,JVM发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region中(在分代的例子中就是检查是否老年代的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

如果不计算维护Remembered Set的操作,G1收集器的运作大致可以划分为以下步骤:

  • 初始标记(Initial Marking):标记一下GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。此阶段需要停顿线程,但耗时很短。

  • 并发标记(Concurrent Marking):从GC Roots开始对堆中对象进行可达性分析,找出存活对象,此阶段耗时较长,但可与用户线程并发执行。

  • 最终标记(Final Marking):修正在并发标记期间因用户线程继续运作而导致标记产生变动的那一部分标记记录,JVM将这段时间对象变化记录在线程Remembered Set Logs里,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中。此阶段需要停顿线程,但可并行执行。

  • 筛选回收(Live Data Counting and Evacuation):对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

String对象去重

Java堆中一般都会存活大量的String对象,而且大量的String对象都是重复的:string1.equals(string2) == true,这很浪费内存。在G1收集器中实现了自动对重复的String对象进行去重,避免浪费内存。使用参数-XX:+UseStringDeduplication开启去重。

二、GC日志

每一种垃圾收集器的日志形式都是由他们自身的实现决定的,不同的收集器,其GC日志格式可以不一样,可使用参数-XX:+PringGCDetails告诉JVM在GC时打印日志。但虚拟机设计者为了方便用户阅读,将各个收集器的日志都维持一定的共性,例如以下两段典型的GC日志:

33.125: [GC [DefNew: 3324K -> 152K(3712K), 0.0025925 secs] 3324K -> 152K(11904K), 0.0031680]

100.667: [Full GC [Tenured: 0K -> 210K(10240K), 0.0148142 secs] 4603K -> 210K(19456K), [Perm : 2999K -> 2999K(21248k)], 0.0150007 SECS] [Times: user=0.01,sys=0.0, real=0.02 secs]

1.最前面的数字 33.125和100.667,代表了GC发生的时间,含义是从JVM启动以来经过的秒数

2.日志开头的“[GC”和“[Full GC”,说明了此次GC的停顿类型,而不是用来区分新生代GC和老年代GC的。如果有“Full”,说明此次GC是发生了STW的。比如,若新生代出现了分配担保失败,对象无法进入老年代,也会导致STW,出现“Full GC”字样。如果是调用System.gc()方法所触发的GC,那么会显示“[Full GC(System)”。

3.接下来的"[DefNew"、"[Tenured"和"[Perm"表示的GC发生的区域,这里显示的区域名称与使用的GC收集器密切相关。比如上例中,"[DefNew"表示的是Serial收集器中的新生代,全名称为"Default New Generation";如果是ParNew收集器,新生代名称会变为"[ParNew,全名为"Parllel New Generation";如果是Parallel Scavenge收集器,那它配套的新生代称为"PSYoungGen"。老年代和方法区同理,名称也是由垃圾收集器决定的。

4.后面括号内部的"3324K -> 152K(3712K)",含义为"GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)"。

5.而在方括号之外的"3324K -> 152K(11904K)",表示"GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)"。

6." 0.0025925 secs"表示该内存区域GC所占用的时间,单位是秒。有的收集器会给出更具体的时间数据,如"[Times: user=0.01,sys=0.0, real=0.02 sec]",这里的user、sys、real分别代表用户态消耗的CPU时间、内核态消耗的CPU时间、操作从开始到结束所经过的墙钟时间(Wall Clock Time)。墙钟时间包括各种非运算的等待耗时,比如等待磁盘IO、等待线程阻塞,而CPU时间不包括这些耗时。但是当系统有多CPU或多核时,系统会叠加这些CPU时间,所以user或sys时间有可能超过real时间。

三、GC分类

以下内容主要摘自知乎R大的一个回答,原文地址:https://www.zhihu.com/question/41922036/answer/93079526。

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

  • Partial GC:并不收集整个GC堆的模式,部分收集
    • Young GC:只收集新生代的GC,也叫做Minor GC
    • Old GC:只收集老年代的GC。只有CMS的concurrent collection是这个模式
    • Mixed GC:收集整个新生代以及部分老年代的GC。只有G1有这个模式
  • Full GC:收集整个堆,包括新生代、老年代、方法区等所有部分的模式。

Major GC通常是跟Full GC是等价的,收集整个Java堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“Major GC”的时候一定要问清楚他想要指的是上面的Full GC还是Old GC。

四、对象分配与空间分配担保

大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间分配时,JVM将发起一次Minor GC。对于那种需要大量连续内存空间的大对象,比如很长的字符串或者数组,JVM提供了一个-XX:PretenureSizeThreshold参数,令大于这个值的对象直接在老年代分配,避免在Eden和两个Survivor之间发生大量的内存复制。需要注意的是,该参数只对Serial和ParNew两款收集器有效。

虚拟机给每个对象都定义了一个年龄(Age)计数器。如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor中,并且对象年龄设为1。对象在Survivor区中每熬过一次GC,年龄就增加1岁,当年龄增加到一定程度(默认为15),将会晋升到老年代中。对象晋升老年代的年龄阈值可以通过参数-XX:MaxTenuringThreshold设置。

另外,为了更好地适应不同程序的内存状况,虚拟机并不是永远要求对象的年龄必须达到阈值才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,那么年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到阈值要求的年龄。

在发生Minor GC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。否则,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,如果允许,那么会继续检查老年代最大连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,即使有风险(老年代可能无法接纳这些对象导致担保失败,此时还是要进行Full GC,就相当于绕了一圈,但是基于这种动态概率的手段,在大部分情况下,分配担保是不会失败的,避免了频繁Full GC);如果小于或者HandlePromotionFailure设置为不允许冒险,那么此时要改为执行一次Full GC。

五、总结

这章分了上下两片博文进行总结,主要讲了垃圾回收算法、几款垃圾收集器的特点以及运作原理。

猜你喜欢

转载自blog.csdn.net/huangzhilin2015/article/details/113874260
今日推荐