深入理解Java虚拟机读书笔记(7): 深入理解垃圾收集器

深入理解Java虚拟机读书笔记(7): 深入理解垃圾收集器

前面讨论的都是理论方面的只是,垃圾收集器则是内存回收的具体实现。在介绍这些不同的垃圾收集器之前,先介绍一些其他比较重要的概念。

一、重要概念

GC停顿

GC停顿时,整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证。Sun公司称之为“Stop The World”,主要发生在枚举根节点中。在前面介绍的可达性分析中,从GC Roots节点找引用链,可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。

目前主流的Java虚拟机中,当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。 在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。 这样,GC在扫描时就可以直接得知这些信息了。

安全点

借助OopMap,HotSpot可以快速准确地完成GC Roots枚举。但是,不可能为每一条指令都生成对应的OopMap,那将占用大量额外空间,增大GC空间成本。

前面说的GC停顿,只是在“特定的位置”,只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。 安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、 循环跳转、 异常跳转等,所以具有这些功能的指令才会产生Safepoint。

对于安全点来说,GC的时候需要让所有线程都“跑”到最近的安全点上再停顿下来。这里一般有两种方案:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)。

抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。 这种方式很不安全,现在几乎没有虚拟机采用。

主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。 轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

安全区域

SafePoint机制对于程序“不执行”的状态无法解决,所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。 对于这种情况,就需要安全区域(Safe Region)来解决。

安全区域是指在一段代码片段之中,引用关系不会发生变化。 在这个区域中的任意地方开始GC都是安全的。 可以把Safe Region看做是被扩展了的Safepoint。

在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。 在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

Minor GC、Major GC 和 Full GC

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

并发和并行

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

二、HotSpot中的垃圾收集器

img

图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。

2.1 Serial收集器

Serial是一个单线程的收集器,它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。运行示意图如下所示:

img

此收集器简单而高效,对于限定单个CPU的环境来说,单线程模式下Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。它是虚拟机运行在Client模式下的默认新生代收集器。

2.2 ParNew收集器

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

img

ParNew收集器是Server模式下虚拟机首选的新生代收集器,因为目前只有它能与CMS收集器配合工作。ParNew收集器也是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

2.3 Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器

**特点:**目标是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

高吞吐量可以高效率利用CPU时间,尽快完成运算任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。 GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的,MaxGCPauseMillis越小,垃圾收集变得更频繁。停顿时间下降,导致吞吐量也下降了。

GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。 如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1/(1+19)),默认值为99,就是允许最大1%(即1/(1+99))的垃圾收集时间。

由于上面的特性,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。

Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得关
注。 这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、 晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)

2.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。 这个收集器主要有两个作用:

  • 给Client模式下虚拟机使用
  • Server模式下
    • 在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用
    • 作为CMS收集器的后备预案

收集器工作过程如下图:

img

2.5 CMS收集器

CMS(Concurrent Mark Sweep) 收集器,是使用最广泛的收集器之一,其目标是获取最短回收停顿时间,所以,目前很大一部分的Java应用集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,就可以采用CMS收集器。

CMS收集器是基于“标记-清除”算法实现的,其运作过程分为四个步骤:

  • 初始标记(CMS initial mark )

    仅仅标记一下GC Roots能直接关联到的对象,速度很快

  • 并发标记(CMS concurrent mark)

    进行GC RootsTracing的过程

  • 重新标记(CMS remark)

    为了修正并发标记期间因用户程序继续运作而导致标记产生变动的一部分对象的标记记录,这个阶段停顿时间一般比初始标记时间长,但远小于并发标记时间

  • 并发清除(CMS concurrent sweep)

    清除对象

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

耗时最长的是并发标记和并发清除两个阶段,但这两个过程中收集器线程都可以与用户线程一起工作 ,CMS收集器的内存回收过程是与用户线程一起并发执行的。 CMS收集器运行示意图如下:

img

CMS的优点:并发收集、低停顿

缺点:

  1. 对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。 CMS默认启动的回收线程数是(CPU数量+3)/4 ,当CPU大于4时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是CPU小于4个时,需要分出接近一半的运算李去执行收集器线程,让人无法接受。

  2. 无法处理浮动垃圾(Floating Garbage)。可能出现“Concurrent Mode
    Failure”失败而导致另一次Full GC的产生。所谓“浮动垃圾”是指CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。

    也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。 要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

  3. 产生大量空间碎片。这是由于CMS是基于“标记-清除”算法实现造成的。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC。

    为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,接着来一次带压缩的(默认值为0,表示每次进入FullGC时都进行碎片整理)。

2. 6 G1收集器

G1收集器主要面向服务端应用,主要有以下特点:

  1. **并行与并发:**G1能充分利用CPU多核的优势,缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  2. **分代收集:**G1甚至可以不用其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、 熬过多次GC的旧对象以获取更好的收集效果。
  3. **空间整合:**G1从整体来看是基于“标记-整理”算法实现,从局部(两个Region之间)来看是基于“复制”算法实现,两种算法都意味着不会产生内存空间碎片
  4. **可预测的停顿:**G1可以建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

与其他收集器不同,G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

G1建立可以预测的停顿时间模型,基础是它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。 G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。

由于Java堆被划分为以Region为单位,但是Region不可能是孤立的。一个对象分配在某个Region中,它并非只能被本Region中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。 这样一来,做可达性判定对象是否存活的时候,需要扫描整个Java堆才行。

为了解决这个问题,引入了Remembered Set。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

G1收集器运作流程大致如下:

  • 初始标记(Initial Marking)

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

  • 并发标记(Concurrent Marking)
    从GC Root开始对堆中对象进行可达性分析,找出存活的对象,耗时较长,但可与用户程序并发执行

  • 最终标记(Final Marking)

    为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行

  • 筛选回收(Live Data Counting and Evacuation)

    首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

G1收集器运行示意图如下:

img

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值