java村支书 · 2019年10月25日

解锁JVM成神之路(二)

之前发布过解锁JVM成神之路(一) ,里面介绍了运行时数据区,类加载子系统以及双亲委派机制。现在说学习gc算法。

如何判断对象是否可以被回收

在jvm的堆内存中,存放着许多new出来的对象,要对这些对象进行回收,第一步永远是判断这些对象是否可以被回收,那么如何判断呢?

引入计数法

这种方式是给对象增加一个计数器,每当有地方在引用这个对象时,那么计数器就加1,而引用失效后,计数器就会减1。任何时候,计数器为0时,那么这个对象就能被回收。
这种方式简单而且效率高,但是现在主流的jvm不再使用这种方式。因为,它无法解决循环引用的问题(对象之间循环引用)就。通过下面代码所示:

public class GCDemo1 {
Object object = null;
public static void main(String[] args) {
    GCDemo1 a = new GCDemo1();
    GCDemo1 b = new GCDemo1();
    a.object = b;
    b.object = a;
    a = null;
    b = null;
}

}

对象a和对象b相互引用着对方,这两个对象再无任何引用。但是他们因为相互引用着导致他们的引用计数器不为0,于是GC无法回收他们。

可达性分析算法

现在主流的jvm都是采用这个方式,它基本思想就是从一个叫做"GC Roots"的对象作为起点,从这个起点开始向下搜索,走过的路,我们称之为“引用链”。当一个对象到GC Roots没有任何的引用链时,则此对象不可用,我们称之为对象不可达,那么就可以被回收了。

file

但是这里要注意的是,一个对象在被回收之前,至少要经过2次标记不可达。如果一个对象和GC Roots没有任何的引用链时,则这个对象第一次被标记为可收回。然后在做一次筛选,这个过程是:如果这个对象没有覆盖finalize()或者finalize()方法已经被虚拟机调用过,那么这个对象就将被放到一个叫做F-Queue的队列中,队列中对象的finalize()方法将由一个虚拟机自动建立低优先级的Finalize线程去执行。

finalize()方法执行的过程是对象逃脱被回收的最后机会,如果对象在finalize()中与GC Roots有引用链,那么这个对象就会被移除队列。因此,我们在开发的过程中,不建议覆盖finalize()方法,或者在finalize()做一些清理资源的工作。因为它的不确定性,而且运行代价很高,完全可以使用try-finally去代替使用。

如何判断一个常量是废除的常量

运行时常量池主要是回收废弃的常量。假如常量池中有“abc”字符串,当没有任何的String对象引用它时,那么“abc”就是废弃的常量,在发生内存回收时,如果有必要,那么“abc”就会被清理出常量池。

如何判断一个类是无用的类

满足以下三个条件:

  • 该类的所有实例已经被回收。jvm堆中没有任何它的实例
  • 加载该类的ClassLoader已经被回收
  • 该类的Class对象没有任何的地方引用。也就是任何地方都无法通过发射来访问该类

满足以上三个条件,那么就可以对其进行回收。这里说的是可以,并不是和对象一样必然被回收。

垃圾回收算法

大致有四个算法:标记清除算法,复制算法,标记整理算法,分代回收算法

标记清除算法

分为两个步骤,先标记,后清除。这种最简单,但是产生内存碎片,会导致大对象无法找到可以利用的内存空间

file

复制算法

思路就是将内存容量分为两个部分,先使用一部分,当这部分没有空间使用时,将存活的对象复制移到另一部分上,然后再对第一部分进行清理。这种方式也简单,但是最大的缺点就是内存只能利用一半,效率有点低。

file

标记整理算法

结合以上两种算法而得出,标记阶段跟标记清理算法的一致,只是标记后不清理对象,而是将存活的对象向内存的另一端移动,然后回收这一端以外的内

file

分代回收算法

垃圾收集器基本都采用这种算法,思路就是根据对象的存活周期不同将内存划分为几块区域,就可以根据它们的特点分别对这些区域进行回收。

一般情况下,gc最活跃的地方就是在堆中。我们可以根据堆的特点将其分为新生代和老年代。新生代的特点就是每次垃圾回收都有大量的内存需要回收,而老年代则只有少部分需要回收。因此可以根据这些特点进行选择不同的算法。

新生代与复制算法

因为新生代每次都要回收大量的对象,所以可以选择复制算法,只要付出少量对象的复制成本就可以完成每次垃圾收集。所以在新生代中又分为三个区域,一个比较大的区域为Eden Space,以及两个比较小的区域Survivor 空间分别是:From Space,To Space。每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中

file

老年代与标记清理(整理)算法

老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保。因此选择标记清理算法或者标记整理算法:

  1. jvm的方法区我们称之为永生代(Permanet Generation),它用来存储 class 类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
  2. 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况会直接分配到老生代。
  3. 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,EdenSpace 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 From

Space 进行清理。

  1. 如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。
  2. 在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。
  3. 当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老生代中。

总结:

  • 在jvm中判断对象是否可以被收回使用的是可达性分析算法;
  • 一个对象要被垃圾收集器回收至少要标记2次;
  • 垃圾回收算法有:标记清理算法,复制算法,标记整理算法,分代回收算法;

本人水平有限,难免有错误或遗漏之处,望大家指正和谅解,提出宝贵意见,愿与之交流。

推荐阅读
关注数
0
文章数
16
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息