3 垃圾收集算法

时间:2022-09-21 20:15:46

  1. 哪些内存需要回收:死去的对象需要回收
  2. 什么时候回收
  3. 如何回收

按照jvm内存区域划分原则:程序计数器、虚拟机栈、本地方法栈3个区域的内存随线程创建而划分,因此线程结束时,内存也自动释放。
本章节分析的是Java堆和方法区的内存管理策略

1、虚拟机栈、本地方法栈,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。
  每一个栈帧中分配多少内存基 本上是在类结构确定下来时就已知的(尽管在运行期会由即时编译器进行一些优化,但在基于概念模 型的讨论里,大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收都具备确定性。
2、堆和方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,
只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。

2 对象存活判定算法

回收堆,也就是回收对象,判断对象是否需要回收,也就是判断对象是否死亡,有两种策略:引用计数算法和可达性分析算法

2.1 引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的

缺点

  1. 当对象存在相互引用时,该判断方法失效
  2. 当前较少java虚拟机应用该算法

关于引用的说明

分类 定义 垃圾回收
强引用 程序代码之中普遍存在的引用赋值 不回收
软引用 一些还有用,但非必须的对象。SoftReference修饰 将要发生溢出时,才会被回收
弱引用 强度比软引用 弱一点。 WeakReference 修饰 垃圾收集器启动,就会被回收,而不管是否发生溢出
虚引用 目的只是为了能在这个对象被收集器回收时收到一个系统通知 垃圾收集器启动,就会被回收 【设置虚引用的目的仅是为了在对象被回收时收到一个通知】

2.2 可达性分析算法

通过GC Root节点,根据引用关系向下遍历,当存在对象不在引用连上,则该对象可能不在被引用。
注意:当前下,根节点选举,还是需要暂停所有用户线程,以便保证快照一致性

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

  • 虚拟机栈中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
  • 方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
  • 方法区中常量引用的对象,譬如字符串常量池里的引用
  • 本地方法栈中Native方法引用的对象
  • 所有被同步锁(synchronized)持有的对象

2.2.1 不可达对象的后置处理

当对象被判断为不可达对象后,它仍有可能不被回收:调用了finalize()方法并且在方法里调用其它存活对象
因此,不可达对象在第一次标志后,还会有一个执行判断过程:

  1. 当对象被判定为不可达对象后,进行第一次标记。
  2. 对已经被标记的对象筛选出来,判断是否需要执行finalize()方法,需要就放到执行队列里面。
  3. 在finalize(),如果产生对存活对象的引用,jvm会将该不可达对象移除待回收的集合。

过程如下所示:
3 垃圾收集算法

关于finalize()方法

  1. 它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此不推荐使用
  2. finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好

2.3 方法区回收判定

  1. 方法区的回收条件比较苛刻,成本高,《Java虚拟机规范》不要求实现方法区域的垃圾回收
  2. HotSpot虚拟机中的元空间或者永久代是没有垃圾收集行为
  3. 方法区的垃圾收集主要回收两部分内容:废弃的常量不再使用的类型

一、判断常量是否被废弃

  1. 没有任何对象引用常量池中的这个常量
  2. 虚拟机中也没有其他地方引用这个常量

二、判断类型是否不再使用

  1. 类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
  2. 加载该类的类加载器已经被回收
  3. 类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是 和对象一样,没有引用了就必然会回收。
关于是否要对类型进行回收,HotSpot虚拟机提供了- Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX: +TraceClassUnLoading查看类加载和卸载信息

5 垃圾收集算法介绍

5.1 分代收集理论

!!!重要重要重要

  1. 将堆内存按照区域划分,存储不同"年龄"对象。(即分为新生代老年代);
  2. 新生代对象可以转到老年代去;
  3. 对于新生代,回收时只关注少量需要保留的对象;
  4. 对于老年代,使用低频率来进行回收;
  5. 由于分区的出现,促使回收可以针对特定区域进行,或者不同的区域使用不同的回收算法。
  6. 对于跨代对象,在新生代建立记忆表,存储老年代哪些区域存在跨带引用,在回收处理时,仅处理该区域的对象;

关于收集的补充说明
部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,分为:

  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

5.1.1 记忆集与卡表

  1. 记忆集的目的:解决跨带引用带来的可能要扫描整个老年代的问题
  2. 它是一个存储在非收集区的指针集合的数据结构,元素指向收集区
  3. 卡表:记忆集精度到内存区域,称为卡表;抽象为一个字节数组
  4. 卡页:卡表的一个元素:内存块:存储一个内存地址,根据指定页码的大小(2的N次幂字节数),构成地址范围;
  5. 一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代 指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃 圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它 们加入GC Roots中一并扫描。

5.2 标记-清除算法

  1. 标记出所有待回收对象
  2. 对被标记对象进行清除
  3. 缺点:1、如果有大量待清除对象,则出现多次的标记-清除操作(我理解是既然被清除,则无需进行标记);2、产生内存碎片

5.3 标志-复制算法

  1. 将内存同等划分2个区域,新对象都被放在其中一个区域A;
  2. 当该区域内存用完后,将活着的对象复制到另外一块区域中B;
  3. 将已使用过半区进行回收清除;同时新对象只会出现在区域B中;
  4. 优点:不会产生内存碎片(存活对象被放在一起,待回收对象被整块清除)
  5. 缺点:1、复制会产生开销;2、内存浪费:可用内存只剩一半

5.4 优化后的标志-复制算法

  1. 将新生代跨分成三个区域:一大:Eden,两小Survivor。分别占位10:8 10:1 10:1,新产生的对象随机进入使用Eden和其中一块正在使用的Survivor
  2. 发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor
  3. 当Survivor不足以存放存活对象时,转入到入老年代。如果对象经过经过18GC后,还存活,那么也会转入到老年代

过程如图所示:
3 垃圾收集算法

5.5 标记-整理算法

  1. 其中的标记过程仍然与“标记-清除”算法一样;
  2. 不对可回收对象直接回收,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
  3. 缺点:对象被移动,需要更新其引用,需要停止所有运行线程

过程如图所示:
3 垃圾收集算法

6 根节点枚举

6.1 关于根节点及其枚举

  1. 可以作为根节点的数据集中在:方法区【量池、静态变量】、虚拟机栈:【本地变量表】
  2. 根节点枚举,需要暂停用户线程
  3. 另外,查找引用链过程已经实现了跟用户线程并发

6.2 oopMap数据结构

oopMap是什么?

  1. oopMap是一个数据结构,它存储的内容可以作为根节点。
  2. jvm执行某些字节码指令时,会创建该数据结构

为什么需要oopMap?

  1. 优点:通过oopMap,jvm可以直接找到对象的引用, jvm不需要一个不漏地检查完所有 执行上下文和全局的引用位置,从而快速地完成根节点枚举
  2. 缺点:引起OopMap内容变化的指令非常多,如果为每一条指令都生成对应的oopMap,那将会需要大量的额外存储空间

oopMap如何实现

  1. 类加载完成后,保存对象的属性的偏移地址。【备注一】
  2. 即时编译时,保存栈里对象的引用地址【备注二】

6.3 安全点

安全点是什么

  1. 编译生成字节码指令时,只针对特定的指令,才创建oopMap,我们把这些特定指令的地址称为安全点。

为什么需要安全点?

  1. 如果对所有的指令都生成oopMap,会耗费大量的空间;选择在安全点位置创建oopMap,节省内存空间
  2. 有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。

安全点如何实现?

  1. 安全点位置的选取在具备让程序长时间执行的复用型指令,例如方法调用、循环跳转、异常跳转 ,只有具有这些功能的指令才会产生安全点;
  2. JVM使用主动式中断方案,让线程暂停执行,来响应GC事件。

关于主动式中断

  1. 当垃圾收集需要中断线程的时候,不直接对线程暂停,仅设置一个标志位,各个线程执行时轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起
另一种方案是抢先式中断:【主流虚拟机不使用该方案】
抢先式中断不需要线程的执行代码 主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地 方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上

6.4 安全区域

安全区域是什么?

  1. 在该代码片中,对象引用关系不会发生变化,那么这块代码(或者字节码指令片段)就是安全区域

为什么需要安全区域?

  1. 线程没有分配cpu时间时(处于Sleep状或Blocked状态)无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,安全点的设置就不起效,因此需要安全区域。
  2. 在安全区域进行垃圾收集是安全的。

安全区域对线程和回收器的影响

  1. 当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域。
  2. 虚拟机要发起垃圾收集时,将不会给处于安全区域的线程打上暂停标志【对应主动式中断的打标识】
  3. 当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举:
    1. 完成:线程就当作没事发生过,继续执行
    2. 未完成:线程一直等待,直到收到可以离开安全区域的信号为止。

用一张图来描述:
3 垃圾收集算法

6.5 oopMap、安全点、安全区域对比总结

用一张图来总结:
3 垃圾收集算法

7 可达性遍历的并发分析

未完成待续