转载

憨人笔记之JVM-垃圾回收算法

话不多说,干就完了。

在之前我们说到堆空间是对象实例存放的地方。程序会一致运行,对象也可能一直创建,但是堆的内存空间是有限的,那么如何保证在程序运行过程中,堆空间一直有足够的内存来创建新的对象呢? 垃圾回收 ,垃圾回收将已经不使用的对象进行回收,释放内存空间,以便在分配新的对象时有足够的内存空间来进行分配。

在对堆空间进行垃圾回收之前,首先就是要确定哪些对象还"存活",哪些对象已经"死去"(也就是不回再被任何途径所使用)。垃圾回收只会针对死去的对象。

如何判定对象已死(垃圾判断算法)

引用计数器算法(Reference Counting)

引用计数器算法就是在对象中添加一个引用计数器,每当有一个地方使用到该对象时,计数器值就加1,而当引用失效的时候,该计数器值就减1,只要当计数器的值为0的时候,就表示该对象已经不再被使用。

  • 优点

    引用计数器算法实现简单,而且效率高。

  • 缺点

    很难解决对象之间的相互循环引用问题。

由于其缺点,所以目前主流的虚拟机中都没有选用引用计数器算法来管理内存。那么什么是对象的相互循环引用呢?通过下面代码实例来进行说明

public class Dog {
  private Cat cat;
  // 省略get/set方法
}

public class Cat {
  private Dog dog;
  // 省略get/set方法
}

public static void main(String[] args){
  Dog dog = new Dog();
  Cat cat = new Cat();
  // 对象的相互循环引用
  cat.dog = dog;
  dog.cat = cat;
  
  dog = null;
  cat = null;
}
复制代码

在上述代码中,虽然dog,cat被置为null,也就是不再使用了,但是dog和cat之间存在相互引用,所以虚拟机并不会回收这两个对象。

可达性分析算法

可达性算法的基本思想就是通过一系列被称为"GC Roots"的对象作为起始点,由这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个GC Roots对象没有任何引用链相连时,则证明该对象是不可用的。

憨人笔记之JVM-垃圾回收算法

在Java中,可作为GC Roots的对象主要包括了以下几种:

  • 栈帧中的本地变量表中的引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象

需要说明的是,即使是在可达性算法中,不可达的对象,并非就会被标记为"非死不可"的对象。对于一个对象的死亡宣告,至少要经历两次标记的过程。

第一次标记

l对象经过可达性算法分析后,发现没有与GC Roots项链的引用链时,它会被标记一次,同时会进行筛选,筛选的条件就是此对象有没有必要执行finalize方法。如果该对象 没有覆盖finalize方法 或者说 finalize方法已经被虚拟机执行过 。那么虚拟机会认为这两种情况没有必要执行finalize方法。

第二次标记

如果对象被判定为有必要执行finalize方法,那么虚拟机会将对象防止在一个F-Queue队列中。同时虚拟机会自动建立一个低优先级的Finalizer线程去执行它。需要注意的是,执行仅仅表示虚拟机会触发这个对象的finalize方法,但并不会保证等待这个对象的运行结束。

finalize方法是对象逃脱死亡命运的最后一次机会,GC会对F-Queue队列中的对象做第二次标记。对象在finalize方法中,如果重新建立与引用链上的任何一个对象关联,例如将自己(this关键字)赋值给某个类变量或者对象的成员变量。那么在第二次标记时,会被移出"即将回收"的集合。

憨人笔记之JVM-垃圾回收算法

最终流程如上图所示。需要注意的是,对于任何 一个对象的finalize方法,都只会被系统自动调用一次 ,如果对象已经调用过finalize方法之后,那么它的finalize方法就不会被再次执行。在实际开发过程中,应当避免调用对象的finalize方法。

垃圾收集算法

常见的垃圾收集算法有四类: 标记-清除算法、标记-整理算法、复制算法、分代算法 。下面分别依次介绍每一种算法的思想。

标记-清除算法

标记-清除算法是垃圾收集算法中最基础的算法。在之前说如何判定一个对象是否存活的算法中,GC Roots会对对象进行标记,而标记清除算法,则是根据GC Roots的标记判断该对象可回收,如下图:

憨人笔记之JVM-垃圾回收算法

通过途中可以很明显的看到,标记清除算法会带来一个问题,那就是 内存碎片化 。在说到堆空间的新生代时,也提到过内存的碎片化,其后果就是会影响到程序的性能。

标记-整理算法

标记整理算法的标记过程同标记-清除算法一致,但是后续过程存在差异,标记-整理算法在标记后不是立马对可回收的对象进行回收,而是让存活的对象都向一端移动,然后清除可用边界以外的对象,释放内存空间。

憨人笔记之JVM-垃圾回收算法

在上图中可以明显的看到,与标记-清除算法不通的是,它并 不会产生内存碎片 ,而是通过整理,使当前可用对象都会保存在一段连续的内存上。

复制算法

在说到堆空间的新生代时,有说到新生对象从Eden区到Survivor区的流转过程,而这个过程正好就是复制算法的实际体现。复制算法会在内存中划分出两块大小相等的区域(假设为A、B),每次只使用其中一块,当A区域满了的时候,会将存活的对象复制一份到B区域,同时清空A区域。只需要移动堆顶的指针,按照顺序分配,也就不用考虑内存碎片的问题了。

分代算法

在目前主流的商业虚拟机中,基本上采用的都是分代算法,分代算法实质上就是 根据对象的存活周期不同划分不同的区域 。一般是把Java堆划分成新生代和老年代。而针对不同的代采取不同的收集算法。例如在新生代中,通常会采用复制算法,而在老年代中,因为对象的存活率较高,一般使用标记-整理或标记-清除算法。

总结

本章中主要讲解了垃圾回收的相关的算法。两个方面,一是判定对象是否存活的算法,二是垃圾回收算法,总结如下图:

憨人笔记之JVM-垃圾回收算法

不怕路歹行不怕大雨淋,心上一字敢 面对我的梦,甘愿来作憨人。 --<憨人>

原文  https://juejin.im/post/5eb01e7bf265da7bce2681c4
正文到此结束
Loading...