HotSpot的算法实现和垃圾收集器

HotSpot的算法实现和垃圾收集器


仅作为笔记


前言

随着线程消失而消失的有:程序计数器、虚拟机栈、本地方法栈,这三个地区不需要考虑内存回收的问题,而java堆和方法区不一样,多个实现类需要的内存不一样,方法的多个分支需要的内存也不一样,这些都只能在程序处于运行期间才能确定,意味着这些内存都是动态分配的,所以垃圾收集器关注的只是这部分。
仅作为笔记


一、HotSpot的算法实现

基于对象存活判定和垃圾收集算法,研究Hot Spot如何实现这些算法。

1.1、枚举根节点

  • 可达性分析之前提到过,实际上这也是现代主流的判断对象死亡的方法。可达性分析需要查看的是当前对象的状态,意味着这需要记录的是这一个时刻对象的引用情况,这就不得不将时间封装起来从而产生停顿
  • 准确式GC:这是目前主流java虚拟机采用的方法,属于所有线程停顿后的操作。准确式GC指的是:并不需要一个不漏的遍历所有的上下文和全局的引用位置,应该是有具体的地方存放着引用关系,直接查看保存着引用关系的地方就行了(OopMap数据结构)
  • Hot Spot的准确式GC:使用OopMap数据结构来存储引用信息,GC在扫描的时候可以直接看这里
  • OopMap:首先要知道在源代码(编译前)里面每个变量都是有类型信息的,但是编译之后的代码就只有变量在栈上的位置信息了。而oopMap是代码的一个附加的信息,告诉你栈上哪个位置本来(在编译前)是个什么东西(包括其类型信息),也就是从外部记录下类型信息,存成映射表。 这个信息是在JIT编译时跟机器码一起产生的。因为只有编译器知道源代码跟编译后的代码的对应关系。 每个方法可能会有好几个oopMap,这是根据safepoint把一个方法的代码分成几段,每一段代码一个oopMap,作用域自然也仅限于这一段代码。 循环中引用多个对象,肯定会有多个变量,编译后占据栈上的多个位置。那这段代码的oopMap就会包含多条记录。
  • 每个被JIT编译过后的方法也会在一些特定的位置记录下OopMap,记录了执行到该方法的某条指令的时候,栈上和寄存器里哪些位置是引用。这样GC在扫描栈的时候就会查询这些OopMap就知道哪里是引用了,总之OopMap是准确式GC得以实现的核心。

1.2、安全点

  • OopMap可以很好的帮助完成GC Roots枚举,但是前面我们提到了这个过程是需要停顿的(判定引用链是否断裂时需要暂停时间来判断当前引用情况),这就会产生两个问题,1)产生过多的OopMap会使用大量的内存空间2)OopMap太多,也会花费大量的时间去检查。为了解决这个问题那就需要设置一个安全的地方进行OopMap存储,叫安全点(上一节1.1提到的safepoint)
  • 安全点的初始目的并不是让其他线程停下,一是为了减少OopMap太多而导致占用过多内存和花费大量时间,二是找到一个稳定的执行状态。在这个执行状态下,Java 虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析。举个例子,当 Java 程序通过 JNI(本地接口) 执行本地代码时,如果这段代码不访问 Java 对象、调用 Java 方法或者返回至原 Java 方法,那么 Java 虚拟机的堆栈不会发生改变,也就代表着这段本地代码可以作为同一个安全点。只要不离开这个安全点,Java 虚拟机便能够在垃圾回收的同时,继续运行这段本地代码。
  • 除了执行 JNI 本地代码外,Java 线程还有其他几种状态:解释执行字节码执行即时编译器生成的机器码、以及线程阻塞。阻塞的线程由于处于 Java 虚拟机线程调度器的掌控之下,因此属于安全点
  • 哪里设置安全点:以是否具有让程序长时间执行的特征为标准,最明显的特征是指令序列复用,如:方法调用,循环跳转,异常跳转。由这些功能的才会产生安全点Safepoint。
  • 如何停下:1)抢先式中断:当需要执行GC时,立即让所有线程中断,如果发现有线程中断的地方不是安全点,那就恢复线程让其跑到安全点。注意:这个方案几乎没人用。2)主动式中断:当GC需要中断线程时,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

1.3、安全区域

  • 为了解决有的线程处于sleep或者Blocked状态无法执行到safepoint而提出的。
  • 安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。
  • 在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了(是不是阻塞或者sleep都可以进行回收)。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

二、垃圾收集器

  • 这里讨论的收集器是JDK1.7 Update 14之后的HotSpot虚拟机
  • 一个虚拟机肯定不只是一个收集器,要完成那么多的垃圾回收,需要各种各样的收集器。下面是虚拟机中包含的收集器,连线的表示可以搭配使用的
    在这里插入图片描述

2.1、Serial收集器

  • 第一代垃圾收集器,收集时会暂停掉所有线程。
  • 特点:与其他收集器的单线程相比,他简单而高效。Serial收集器没有线程交互,专心于垃圾收集。
  • 适用环境:Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的
    过程如下所示:
    在这里插入图片描述

2.2、ParNew收集器

  • 只是Serial收集器的多线程版本,其余的和Serial没什么两样。
  • 特点:运行在新生代,目前唯一可以和老年代收集器——CMS收集器配合的收集器
  • 适用环境Server模式下选择这个收集器是首选,原因是他可以配合CMS
    示意图如下:
    在这里插入图片描述

2.3、Parllel Scavenge收集器

  • 吞吐量:吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务。
  • 特点并行的多线程的新生代收集器,侧重点在于吞吐量的控制。有两个参数用于精确控制吞吐量,分别是:设置垃圾收集停顿时间的**-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的****-XX:GCTimeRatio参数。如果调小-XX:MaxGCPauseMillis参数,虽然能缩短GC停顿时间,但是这会降低新生代的大小以及降低吞吐量**。
  • 适用场景:主要适合在后台运算不需要太多交互的任务
  • GCTimeRatio参数的值:是一个大于0且小于100的整数,是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1/(1+19)),默认值为99,就是允许最大1%(即1/(1+99))的垃圾收集时间。

2.4、Serial Old收集器

  • Serial是新生代的收集器,而Serial old是老年代的,同样是单线程收集器,使用的是“标记-整理算法”。
  • 应用场景主要是应用在Client模式下,如果使用在Server模式下,主要作用:1)在JDK1.5之前是配合Parallel Scavenge收集器使用。2)作为CMS收集器的后被预案,在并发收集发生Concurrent Mode Failure时使用。

2.5、Parallel Old收集器

  • 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记——整理”算法。诞生于JDK1.6。
  • 适用场景吞吐量优先,在注重吞吐量和CPU资源敏感的场合,优先考虑Parallel Scavenge加Parallel Old组合

2.6、CMS收集器

先有如下概念:

  • GC RootsTracing过程:判断对象是否可回收或者说死亡了的算法,基于可达性分析算法
  • Concurrent Mode FailureCMS收集器特有的错误老年代正在清理,从年轻代晋升了新的对象,或者直接分配大对象年轻代放不下导致直接在老年代生成,这时候老年代也放不下,则会抛出“concurrent mode failure”
  • 关于Full GC老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。

再接着详细看CMS:

  • 前面提到过收集器侧重问题,比如吞吐量优先收集器,这里提到的就是以最短回收停顿算法为目标的响应优先收集器
  • 主要应用场景:互联网站或者B/S系统的服务端上,这类应用都是希望停顿时间短,有更好的用户使用体验。
  • CMS基于“标记-清除”算法,分为以下四个步骤:1)初试标记,仅仅标记 GC Roots 能直接关联到的对象。2)并发标记,对初始标记标记过的对象,进行 trace(进行追踪,得到所有关联的对象,进行标记)。3)重新标记,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,之前在并发标记时,因为是 GC 和用户程序是并发执行的,可能导致一部分已经标记为 从 GC Roots 不可达 的对象,因为用户程序的(并发)运行,又可达 了,Remark 的作用就是将这部分对象又标记为可达对象,简言之,重新标记是检查前两个阶段被标记为不可达的是不是真的不可达。4)并发清除。其中,初试标记重新标记是需要“Stop The World”的。虽然有两个步骤需要停止世界,但是这两个步骤是耗时最短的,耗时相对长的是并发标记并发清除两个步骤,但是这两个步骤是收集器线程可与用户线程一起工作的,意味着总体来说CMS收集器的内存回收过程是并发的
  • 缺点:1)对CPU资源非常敏感,虽然在两个并发阶段用户线程还是可以和收集线程并发执行的,但是毕竟有回收线程在占用CPU,这依然会降低用户程序的执行速度。诞生过增量式并发收集器,思想是看待单核CPU那样,让收集器和用户线程交互使用CPU,减少线程独占CPU的时间,但是这个在时间过程中还是没能解决这个问题,所以被废弃了。2)无法处理浮动垃圾浮动垃圾就是在并发标记阶段产生的垃圾(浮动垃圾”,因为 CMS 在 并发标记 时是并发的,GC 线程和用户线程并发执行,这个过程当然可能会因为线程的交替执行而导致新产生的垃圾(即浮动垃圾)没有被标记到;而 重新标记 的作用只是修改之前 并发标记 所获得的不可达对象,所以是没有办法处理 “浮动垃圾” 的,并且由于并发清理也是不会stop the word的,也会产生浮动垃圾),这部分是不会被发现的。3)内存碎片问题,由于基于的是“标记-清除”算法,容易在老年代产生大量的内存碎片,会出现老年代还有大量的空间但是没有合适的来为对象分配空间时就会提前触发Full GC
  • 为了解决因为内存碎片导致的提前出现Full GC问题,提供了一个开关,用于CMS收集器顶不住了要开始Full GC时进行内存碎片整理,这个过程是没办法并发的,意味着执行时间变长了。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。
  • Card Table(卡表) 在 CMS GC 中也有使用到,但是这里的卡表是单向的,记录的仅仅是老年代对新生代的引用。有一块区域用来记录老年代中的每个 Card 指向新生代的引用(points-out 结构),在进行 YGC 时,这一块区域的对象作为 GC roots,而不需要扫描整个老年代,用来避免GC Roots时全盘扫描。

2.7、G1收集器

  • 是唯一一个同时用于新生代和老年代的垃圾收集器,但是这个收集器和之前的不同,不把内存区域在物理上就划分为新生代和老年代,而是各种Region(在G1收集器中叫Region,为堆内存最小可用粒度,在CMS里面也叫做卡表(Card Table))。这个卡表实际上在CMS收集器上也有使用,只不过在G1上面的是双向卡表,记录的不仅仅是老年代引用新生代的,还包括新生代引用老年代的原理是并没有在物理上区分新老年代,而是依靠的region,而region的rset表记录了各种引用)。

  • 采用标记-整理算法,避免碎片。该收集器将堆内存分为不同大小相等的region,并维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大(可得到的内存以及所需时间的估计值)的Region,把内存化整为零。

  • 但是由于关系引用的存在,始终存在如何避免全局扫描问题,这里采用每一个Region提供一个remembered Set进行记录引用关系避免可达性分析阶段的全区域垃圾扫描。如下图
    在这里插入图片描述

  • G1大致分为下面四个步骤:初试标记,并发标记,最终标记,筛选回收。初试标记和CMS一样的,最终标记阶段,将并发标记阶段产生的引用变化记录在Remembered Set Logs里面,这样可以解决CMS的浮动垃圾问题,最终把Remembered Set Logs合并到Remembered Set中这一阶段需要停顿线程但是可并行执行

  • 筛选回收,对每一个region的价值和成本进行筛选,根据用户期望GC停顿时间,得到最好的回收方案并回收。

  • 特点:并发性强、分代收集、标记整理进行空间整合,可以预测停顿时间。特别是可预测停顿时间

  • 堆内存分代后,会根据他们的不同特点来区别对待,进行垃圾回收的时候会使用不同的垃圾回收方式,针对新生代的垃圾回收器有如下三个:Serial、Parallel Scavenge、Parallel New,他们采用的都是标记-复制的垃圾回收算法。 针对老年代的垃圾回收器有如下三个:Serial Old 、Parallel Old 、CMS,他们使用的都是标记-整理(压缩)的垃圾回收算法。

2.8、关于空间担保和老年代新生代的一些杂记

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlerPromotionFailure这个参数设置的值(true或flase)是否允许担保失败(如果这个值为true,代表着JVM说,我允许在这种条件下尝试执行Minor GC,出了事我负责)。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlerPromotionFailure为false,那么这次Minor GC将升级为Full GC

总之:如果老年代最大可用的连续空间大于历次晋升到老年代对象的平均大小,那么在HandlerPromotionFailure为true的情况下,可以尝试进行一次Minor GC,但这是有风险的,如果本次将要晋升到老年代的对象很多,那么Minor GC还是无法执行,此时还得改为Full GC。

注意:
JDK 6Update 24之后,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行minorGC,否则进行Full GC.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值