3 垃圾收集算法(垃圾收集机制)

  本篇文章为你整理了3 垃圾收集算法(垃圾收集机制)的详细内容,包含有三种常规垃圾收集算法效率比较 垃圾收集机制 垃圾收集方式及其优缺点 垃圾收集处理的一般流程 3 垃圾收集算法,希望能帮助你了解 3 垃圾收集算法。

  目录1 垃圾收集三件事2 对象存活判定算法2.1 引用计数算法2.2 可达性分析算法2.2.1 不可达对象的后置处理2.3 方法区回收判定5 垃圾收集算法介绍5.1 分代收集理论5.1.1 记忆集与卡表5.1.2 写屏障5.2 标记-清除算法5.3 标志-复制算法5.4 优化后的标志-复制算法5.5 标记-整理算法6 根节点枚举6.1 关于根节点及其枚举6.2 oopMap数据结构6.3 安全点6.4 安全区域6.5 oopMap、安全点、安全区域对比总结7 可达性遍历的并发分析7.1 三色标记法7.2 增量更新7.3 原始快照

  1 垃圾收集三件事

  哪些内存需要回收:死去的对象需要回收

  什么时候回收

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

  本章节分析的是Java堆和方法区的内存管理策略

  

1、虚拟机栈、本地方法栈,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。

 

   每一个栈帧中分配多少内存基 本上是在类结构确定下来时就已知的(尽管在运行期会由即时编译器进行一些优化,但在基于概念模 型的讨论里,大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收都具备确定性。

  2、堆和方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,

  只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。

  

 

  2 对象存活判定算法

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

  2.1 引用计数算法

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

  缺点:

  当对象存在相互引用时,该判断方法失效

  当前较少java虚拟机应用该算法

  关于引用的说明

  
目的只是为了能在这个对象被收集器回收时收到一个系统通知

  垃圾收集器启动,就会被回收 【设置虚引用的目的仅是为了在对象被回收时收到一个通知】

  
2.2 可达性分析算法

  通过GC Root节点,根据引用关系向下遍历,当存在对象不在引用连上,则该对象可能不在被引用。
 

  注意:当前下,根节点选举,还是需要暂停所有用户线程,以便保证快照一致性

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

  虚拟机栈中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等

  方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量

  方法区中常量引用的对象,譬如字符串常量池里的引用

  本地方法栈中Native方法引用的对象

  所有被同步锁(synchronized)持有的对象

  2.2.1 不可达对象的后置处理

  当对象被判断为不可达对象后,它仍有可能不被回收:调用了finalize()方法并且在方法里调用其它存活对象。
 

  因此,不可达对象在第一次标志后,还会有一个执行判断过程:

  当对象被判定为不可达对象后,进行第一次标记。

  对已经被标记的对象筛选出来,判断是否需要执行finalize()方法,需要就放到执行队列里面。

  在finalize(),如果产生对存活对象的引用,jvm会将该不可达对象移除待回收的集合。

  过程如下所示:
 

  关于finalize()方法

  它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此不推荐使用

  finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好

  2.3 方法区回收判定

  方法区的回收条件比较苛刻,成本高,《Java虚拟机规范》不要求实现方法区域的垃圾回收

  HotSpot虚拟机中的元空间或者永久代是没有垃圾收集行为

  方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型

  一、判断常量是否被废弃

  没有任何对象引用常量池中的这个常量

  虚拟机中也没有其他地方引用这个常量

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

  该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例

  加载该类的类加载器已经被回收

  该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

  

Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是 和对象一样,没有引用了就必然会回收。

 

  关于是否要对类型进行回收,HotSpot虚拟机提供了- Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX: +TraceClassUnLoading查看类加载和卸载信息

  

 

  5 垃圾收集算法介绍

  5.1 分代收集理论

  !!!重要重要重要

  
将堆内存按照区域划分,存储不同"年龄"对象。(即分为新生代和老年代);

  新生代对象可以转到老年代去;

  对于新生代,回收时只关注少量需要保留的对象;

  对于老年代,使用低频率来进行回收;

  由于分区的出现,促使回收可以针对特定区域进行,或者不同的区域使用不同的回收算法。

  对于跨代对象,在新生代建立记忆表,存储老年代哪些区域存在跨带引用,在回收处理时,仅处理该区域的对象;

  
关于收集的补充说明
 

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

  老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。

  混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集

  整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

  5.1.1 记忆集与卡表

  记忆集是什么?

  它是一种数据结构,存储在非收集区,元素是指针,指向收集区。

  它是一种抽象定义

  为什么需要记忆集?

  解决对象跨带引用带来的可能要扫描整个老年代的问题

  记忆集如何实现?

  实现为:对象数组:把涉及跨带引用的对象用数组存储起来。缺点:维度太大,不利于存储

  实现为:元素为1个字长的数组,该字包含跨代指针。

  实现为:卡表。

  卡表是什么?

  它是一种数组,其元素映射到一个内存区域。

  卡表是记忆集的具体实现。

  卡页是什么?

  卡页是卡表数组元素对应的内存块

  首先定义页码大小:HotSpot定义为2^9,即512个字节。

  卡表标识一个内存基地址,基地址+页码大小,构成一块内存区域,这块区域称为卡页,多块区域集合成卡表数组

  垃圾收集器如何利用卡表?

  一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。

  卡表和卡页的映射关系

  垃圾收集器将筛选出老年代中地址为:0x0200到0x03ff的内存区域,进行可达性分析。

  总结记忆集、卡表、卡页的关系:

  记忆集是一种数据集合的抽象

  卡表是记忆集的数组实现

  卡页是卡表的元素

  5.1.2 写屏障

  写屏障是什么?

  写屏障是Write Barrier的中文翻译,抽象且无厘头。

  它是一种AOP环绕操作:当发生引用类型字段赋值时,虚拟机更新卡表元素使其“变脏”

  为什么需要写屏障?

  用写屏障实现卡表元素变脏。

  写屏障如何实现?

  在编译期间完成写屏障:时间点发生在引用类型字段赋值的那一刻。

  不能在运行期间执行,因为通过字节码指令是无法识别出是否发生“引用类型字段赋值”

  伪代共享的解决方案

  缓存一致性【《深入理解计算机系统》的后续章节会介绍】引发伪造共享

  不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元未被标记过时才将其标记为变脏

  5.2 标记-清除算法

  标记出所有待回收对象

  对被标记对象进行清除

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

  5.3 标志-复制算法

  将内存同等划分2个区域,新对象都被放在其中一个区域A;

  当该区域内存用完后,将活着的对象复制到另外一块区域中B;

  将已使用过半区进行回收清除;同时新对象只会出现在区域B中;

  优点:不会产生内存碎片(存活对象被放在一起,待回收对象被整块清除)

  缺点:1、复制会产生开销;2、内存浪费:可用内存只剩一半

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

  将新生代跨分成三个区域:一大:Eden,两小Survivor。分别占位10:8 10:1 10:1,新产生的对象随机进入使用Eden和其中一块正在使用的Survivor。

  发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor

  当Survivor不足以存放存活对象时,转入到入老年代。如果对象经过经过18次GC后,还存活,那么也会转入到老年代

  过程如图所示:
 

  5.5 标记-整理算法

  其中的标记过程仍然与“标记-清除”算法一样;

  不对可回收对象直接回收,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存

  缺点:对象被移动,需要更新其引用,需要停止所有运行线程

  过程如图所示:
 

  6 根节点枚举

  6.1 关于根节点及其枚举

  可以作为根节点的数据集中在:方法区【量池、静态变量】、虚拟机栈:【本地变量表】

  根节点枚举,需要暂停用户线程

  另外,查找引用链过程已经实现了跟用户线程并发

  6.2 oopMap数据结构

  oopMap是什么?

  oopMap是一个数据结构,它存储的内容可以作为根节点。

  jvm执行某些字节码指令时,会创建该数据结构

  为什么需要oopMap?

  优点:通过oopMap,jvm可以直接找到对象的引用, jvm不需要一个不漏地检查完所有 执行上下文和全局的引用位置,从而快速地完成根节点枚举

  缺点:引起OopMap内容变化的指令非常多,如果为每一条指令都生成对应的oopMap,那将会需要大量的额外存储空间

  oopMap如何实现:

  类加载完成后,保存对象的属性的偏移地址。【备注一】

  即时编译时,保存栈里对象的引用地址【备注二】

  
备注一:属性的偏移量应该是在对象被创建后才确定的,类加载后是无法确定对象信息的。待明确 todo

   备注二:同理,对象的创建发生在运行期间,编译时又如何得到引用的地址呢?后面关于实时编译的章节会解释 待明确 todo

  6.3 安全点

  安全点是什么?

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

  为什么需要安全点?

  如果对所有的指令都生成oopMap,会耗费大量的空间;选择在安全点位置创建oopMap,节省内存空间

  有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。

  安全点如何实现?

  安全点位置的选取在具备让程序长时间执行的复用型指令,例如方法调用、循环跳转、异常跳转 ,只有具有这些功能的指令才会产生安全点;

  JVM使用主动式中断方案,让线程暂停执行,来响应GC事件。

  关于主动式中断

  当垃圾收集需要中断线程的时候,不直接对线程暂停,仅设置一个标志位,各个线程执行时轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起;

  

另一种方案是抢先式中断:【主流虚拟机不使用该方案】

 

  抢先式中断不需要线程的执行代码 主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地 方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上

  

 

  6.4 安全区域

  安全区域是什么?

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

  为什么需要安全区域?

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

  在安全区域进行垃圾收集是安全的。

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

  当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域。

  虚拟机要发起垃圾收集时,将不会给处于安全区域的线程打上暂停标志【对应主动式中断的打标识】

  当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举:

  完成:线程就当作没事发生过,继续执行

  未完成:线程一直等待,直到收到可以离开安全区域的信号为止。

  
用一张图来描述:
 

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

  用一张图来总结:
 

  7 可达性遍历的并发分析

  遍历失效的情形:

  可达性分析的收集算法,垃圾收集器在对象遍历完成之前,已经遍历过的对象有可能被用户线程修改过引用关系。

  用三色标记法来演示

  7.1 三色标记法

  白色:表示对象尚未被垃圾收集器访问过,在分析结束的阶段,仍然是白色的对象,即代表不可达。

  黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过,黑色对象是可达的。

  灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过

  并发出现“对象消失”问题的示意:
 

  “对象消失”的问题的发生条件
 

  Wilson于1994年在理论上证明了,当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色:

  
赋值器插入了一条或多条从黑色对象到白色对象的新引用;【意味着这个对象现在是可达的】

  赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。【意味着这个对象将不会被扫描到】

  
综上:“对象消失”就是指对象实际是可达的,但又不会被扫描到。 最终被误以为是不可达对象而被回收处理了。
 

  因此,我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可。由此分别产生了两种解决方案:增量更新和原始快照。

  7.2 增量更新

  增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。

  7.3 原始快照

  原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。

  以上无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障(AOP环绕切入) 实现的。在 HotSpot虚拟机中,增量更新和原始快照这两种解决方案都有实际应用。

  以上就是3 垃圾收集算法(垃圾收集机制)的详细内容,想要了解更多 3 垃圾收集算法的内容,请持续关注盛行IT软件开发工作室。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: