jvm 垃圾收集器

深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明

为什么Java需要垃圾收集器?

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。

深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明

因为Java所有的对象的内存都是有JVM自动分配的,Java中的finalize()方法并不等同于C++中的析构函数,C++中内存自己分配,对象的内存在哪个时刻被回收,是可以确定的。但是Java对象的生灭有垃圾收集器决定,finalize()方法会由垃圾回收调用一次,并且并不需要等待finalize()执行完,对象就被回收了。

判断对象是否存活的算法

引用计数法:在对象中添加引用计数器,被引用则值加一,为零则表示不再被使用。但是引用计数法有缺陷:互相引用

可达性分析算法:

基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

垃圾回收算法

跨代引用

由于分代的理论,除了GC ROOTs之外还需要考虑老年代跨代引用的问题,也就是老年代引用新生代,

但是虚拟机又不能遍历所有的老年代对象,太慢了。

跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

依据这条假说,只需要在新生代上建立一个全局的数据结构(该结构被称
“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GCRoots进行扫描。

部分概念

部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,读者需按上下文区分到底是指老年代的收集还是整堆收集。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。

整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。一般GC失败的时候会触发Full GC

标记算法

标记清除Mark-Sweep:执行效率不稳定,新生代中大量对象需要回收,如果执行清除操作需要消耗大量时间;内存碎片化。CMS采用标记清除算法,关注延迟(STW,stop the world 时间),CMS收集器面临空间碎片过多,碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次。

标记复制Mark-Copy:Serial、ParNew。这也是新生代Eden,2个Survivor空间划分的由来,HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1。

标记整理Mark-Compact:Parallel-Scavenge。

经典垃圾收集器

Serial收集器

两个步骤都需要暂停用户线程(STW)。

但是它是所有收集器里额外内存消耗(Memory Footprint)[1]最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。

Serial/Serial Old

ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规
则、回收策略等都与Serial收集器完全一致。

ParNew/Serial Old收集器运行示意图

所以说parNew相对于Serial的区别只是新生代收集的时候才有多线程并行垃圾收集,而且还是需要暂停用户线程(STW)。

但是ParNew收集器是JDK7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有ParNew能与CMS收集器配合工作。

与Serial相比,ParNew只在真正的多核情况下并行收集才会比Serial有更好的效果,注意这里用的是并行,也就是Parallel ,而不是并发(Concurrent)。并行需要多个核心。

Parallel Scavenge收集器

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。

parallel scavenge 关注吞吐量,CMS关注最短暂停时间

停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良
好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。

停顿时间短并不代表垃圾收集时间少,比如CMS收集器大部分时间都是与用户线程并发执行的。所以CMS由于占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量。

Parallel Scavenge收集器也经常被称作“吞吐量优先收集器”。

Parallel Scavenge收集器的一个参数-XX:+UseAdaptiveSizePolicy值得我们关注。这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数,Parallel Scavenge收集器可以动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。

如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用[1],另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。搭配Parallel Scavenge使用。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。较为关注服务的响应速度。是基于标记-清除算法实现的,但是碎片化严重的时候会调用一次标记整理算法(如在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理)。

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

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。

初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快;

并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;

重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;

最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

下面链接中的并发的可达性分析。

G1 , garbage first

G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,
换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才能担当起“全功能收集器”的重任与期望。

  • 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
  • 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
  • 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
  • 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

低延迟垃圾收集器

衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency)。





除非注明,否则均为一叶呼呼原创文章,转载必须以链接形式标明本文链接

本文链接:http://www.yiyehu.tech/archives/2019/02/14/jvm-gc

发表评论

电子邮件地址不会被公开。 必填项已用*标注