深入学习JVM- (2)垃圾收集基础

  • 2.1 概述

    • 垃圾收集需要完成的三件事情:(1)哪些内存需要回收;(2)什么时候需要回收;(3)如何回收?
    • 不需要关注的内存区域:程序计数器、虚拟机栈、本地方法栈生命周期与线程相同,栈帧随着方法的开始和结束执行出栈和入栈操作。这一部分的内存回收具备确定性,当方法或线程结束时,内存自然就跟着回收了;
    • 需要关注的内存区域:Java堆和方法区的内存回收具有很显著的不确定性;
  • 2.2 何为对象已死

    • 2.2.1 对象死亡判断

      • (1) 引用计数算法
        • 基本思路:给每个对象添加一个引用计数器,每当有一个地方引用它时,计数器值加一,引用失效时减一;当值为0时,判断对象为死亡;
        • 问题:循环引用的问题需要配合大量的额外处理才能保证正确的工作;
        • 优点:简单,高效;
      • (2) 可达性分析算法
        • 基本思路:以一系列定义为“GC Roots”的跟对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,所走过的路径称为“引用链”,如果某个节点到GC Roots之间没有任何引用链相连,即从GC Roots到这个对象不可达,则认为对象为死亡;
        • GC Roots包含
          • (1)在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等;
          • (2)在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量;
          • (3)在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用;
          • (4)在本地方法栈中JNI(即通常所说的Native方法)引用的对象
          • (5)Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器;
          • (6)所有被同步锁(synchronized关键字)持有的对象
          • (7)反映Java虚拟机内部情况的JM XBean、JVM TI中注册的回调、本地代码缓存等;
          • (8)除了以上可以固定作为GC Roots的集合外,由于所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其它对象“临时性”加入;
    • 2.2.2 引用类型与对象回收

      • 由于希望描述一类对象:当内存空间充足时保留在内存之中,如果内存空间在进行垃圾收集后依然非常紧张,那么这些对象可以抛弃;
      • (1)强引用Strongly Reference,指代码中普遍存在的引用赋值,只要强引用关系还存在,垃圾收集器就永远不会回收被引用的对象;
      • (2)软引用Soft Reference,描述还有用但非必需的对象,只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常;
      • (3)弱引用Weak Reference,被弱引用关联的对象只能生存到下一次垃圾收集发生为止;
      • (4)虚引用Phantom Reference,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知;
    • 2.2.3 对象死亡过程

      • 真正宣告对象死亡至少要经历两次标记过程:(1)进行可达性分析后对象没有与GC Roots相连接的引用链;(2)筛选出没有必要执行finalize()方法的对象【对象没有被覆盖finalize()方法/方法已经被虚拟机调用过】;
      • 对象被判定为有必要执行finalize()方法时,其会被放置在F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去调用它们的finalize()方法,但不承诺等待方法运行结束;
      • finalize()方法时对象逃脱死亡命运的最后一次机会,即对象可以在finalize()方法中进行一次自救;
    • 2.2.4 回收方法区

      • 方法区垃圾回收的性价比很低,虚拟机可以不实现方法区的垃圾收集;
      • 方法区回收的内容
        • (1)废弃的常量 -- -- -- -- 回收与Java堆中的对象类似,字面量不被任何字符串对象引用时
        • (2)不被使用的类型 -- -- -- -- 回收要求严格,同时满足:
          • ➀ 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
          • ➁ 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的
          • ➂ 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
  • 2.3 垃圾收集算法

    • 2.3.1 分代收集理论基础

      • 分代假说的内容
        • (1)弱分代假说: 绝大多数对象都是朝生夕灭的;
        • (2)强分代假说: 熬过越多次垃圾收集过程的对象就越难以消亡
        • (3)跨代引用假说:跨代引用相对于同代引用来说仅占极少数
      • 分代假说致使的垃圾收集风格
        • 基于假说(1)(2),多款常用的垃圾收集器采用了一致的设计风——虚拟机应该将Java堆分成两个区域,并按照对象的年龄分配到不同的区域中:① 一个区域中存放大多数朝生夕灭的对象,这个区域在回收时只需要关注少数存活的对象而不是大量要被回收的对象,因此这个区域可以以较低的代价进行回收;② 一个区域集中存放了难以消亡的对象,对于这一区域虚拟机可以以较低的频率进行回收;
        • 基于假说(3),使用记忆集来记录这少量的跨代引用,从而使得不需要去遍历GC Roots引用的老年代对象,用少量的空间来换取时间效率
      • 垃圾收集类型
        • (1)Minor GC/Young GC:指目标只是新生代的垃圾收集
        • (2)Major GC/Old GC:指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外注意“Major GC”需按上下文区分到底是指老年代的收集还是整堆收集
        • (3)Mixed GC:指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
        • (4)Full GC:收集整个Java堆和方法区的垃圾收集
    • 2.3.2 标记-清除算法

      • 思想:分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象(或者所有存活对象),标记完成后统一回收所有被标记对象;
      • 优点:简单,是后续其它算法的基础
      • 缺点:(1)执行效率不稳定,两个阶段执行效率都会随着对象数量的增长而降低;(2)内存空间碎片化,不连续的内存空间导致大对象无法分配内存,从而引发另一次垃圾收集动作;
    • 2.3.3 标记-复制算法

      • 思想:将内存分为两个区域,每次只使用其中一块,当一块内存用完了,将其中还存活的对象复制到另一块上,然后把使用过的这一块一次清理掉
      • 优点:(1)解决标记-清除算法面对大量可回收对象时执行效率低的问题;(2)实现简单,运行高效;
      • 缺点:(1)在对象存活率较高时就要进行较多的复制操作,效率将会降低;(2)存在一定的空间浪费;
      • 应用
        • ​​​​​​​这种收集方法主要用于回收新生代,由于新生代中的对象有98%熬不过第一轮收集,因此不需要按照1:1的比例来划分新生代的内存空间
        • 同时使用Appel式回收(将新生代分为一个Eden区和两个Survivor区,每次使用Eden加一块Survivor,发生垃圾收集时将存活对象复制到未使用的Survivor区,并清理Eden和已使用的Survivor),HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1
        • 使用老年代进行分配担保,以防一块Survivor无法容纳Minor GC后存活的对象
    • 2.3.4 标记-整理算法

      • 思想:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
      • 优点:(1)解决内存碎片化问题;(2)移动对象能带来更高的吞吐量(因为不移动对象带来的FullGC极大地降低了用户程序的效率)
      • 缺点:(1)移动存活对象并更新所有引用这些对象的地方是一种极为负重的操作,且这种对象移动操作必须全程暂停用户应用程序才能进行;
      • HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的;
  • 2.4 HotSpot的算法细节实现

    • 2.4.1 根节点枚举

      • 所有收集器在这一步骤必须Stop The World,否则无法保证可达性分析结果的准确性;
      • 目前主流的JVM使用的都是准确式收集==> 当用户线程停顿下来之后并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当有办法直接得到哪些地方存放着对象引用;
      • HotSpot中的解决方案是使用一组称为OopMap的数据结构,OopMap存储两种引用:(1)栈里和寄存器内的引用:在即时编译中,在特定的位置记录下栈里和寄存器里哪些位置是引用;(2)对象内的引用:类加载动作完成时,HotSpot 就会计算出对象内什么偏移量上是什么类型的数据;
    • 2.4.2 安全点

      • 由于为每一条指令都生成对应的OopMap会带来巨大的空间成本,HotSpot在设计时只是在“特定的位置”记录OopMap,这些位置被称为安全点SafePoint;
      • Q1选择安全点:
        • 用户程序必须到达安全点才可以停顿下来进行垃圾收集,安全点的选择不能太少也不能太频繁;
        • 基本上按照“是否具有能让程序长时间执行的特征”这一标准。通常指令序列复用符合这一特征(包含方法调用、循环跳转、异常跳转)
      • Q2线程在垃圾收集时跑到最近安全点的方式:
        • ➀抢先式中断:在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上;
        • ➁主动式中断:当垃圾收集需要中断线程的时候,不直接对线程操作,而是设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。
          • 轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象;
          • 由于轮询操作在代码中会频繁出现,这要求它必须足够高效。HotSpot使用内存保护陷阱的方式,把轮询操作精简至只有一条汇编指令的程度
    • 2.4.3 安全区域

      • 问题:当用户线程处于Sleep或者Blocked状态时,线程无法响应虚拟机的中断请求,无法走到安全点区中断挂起自己,而重新等待线程被激活需要的时间太久,虚拟机垃圾收集显然无法等待这个时间;
      • 安全区域:指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点;
      • 解决思路:用户线程执行安全区域的代码时标识自己已进入安全区域,虚拟机发起垃圾收集时不需要理会已经在安全区域的线程,当线程要离开安全区域时检查虚拟机是否处于垃圾收集需要暂停用户线程阶段,如果不在则继续执行用户程序,否则等待直至收到可以离开安全区域的信号;
    • 2.4.4 记忆集和卡表

      • 记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构;
      • 垃圾收集时如果记忆集记录精度为对象会产生高昂的空间占用,而通常情况下收集器不需要了解跨代指针的全部细节,因此实现记忆集时可以选择不同的记录精度:
        • ➀字长精度:每个记录精确到一个机器字长,该字包含跨代指针;
        • ➁对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针;
        • ➂卡精度【Normally】:每个记录精确到一块内存区域,该区域内有对象含有跨代指针;==>卡表实现记忆集
      • HotSpot虚拟机中卡表的形式是一个字节数组;
      • 一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,则将对应卡表的数组元素值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。
    • 2.4.5 写屏障

      • 是HotSpot虚拟机用来维护卡表状态的技术;
      • 写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。
      • 在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的则叫作写后屏障(Post-Write Barrier)
    • 2.4.6 并发的可达性分析

      • 如果对象图的遍历不能保障在一致性快照上进行,那么将会产生“对象消失”的问题,当以下两个条件同时满足时产生:
        • ➀赋值器插入了一条或多条从黑色对象(已被访问且包含的所有引用都被访问)到白色对象(未被访问)的新引用;
        • ➁赋值器删除了全部从灰色对象(已被访问但不是所有引用都被访问)到该白色对象的直接或间接引用;
      • 我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可。由此分别产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB )
        • ➀增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了;
        • ➁原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值