JVM垃圾回收算法

1:对象状态判断

在介绍垃圾回收算法前,有一件很重的事情就是,如何判断对象是否已经死了?也就是如何判断对象是不是可以被回收了,只有处于死亡状态的对象才有可能被垃圾回收。有下面两种方法判断对象是存活还是死亡?

1.1:引用计数法

算法描述;给对象添加一个引用计数器,每当有一个地方引用它时,该计数器+1,当引用失效时,该计数器-1,任何时刻当计数器为0的对象不可能在被使用了,也就是死亡了!

 

 但是如上图所示,若两个对象之间内部都有引用对象来指向另外一个对象,虽然每一个对象的原始引用已经指向null,外部没有办法通过引用指向该对象了,但是由于他们相互只因,所以引用计数器的值不可能为0,则就造成了永远无法通过GC回收器来回收它们。

1.2:可达性分析算法

在主流实现中,都是通过可达性分析来判断对象是否存活,这个算法的基本思想是铜鼓一列的的GC roots的对象作为起始节点,向下搜索,搜索走过的路径被称为引用链,Referende Chain,当一个对象到GC roots没有任何引用链相连,则证明这个对象是不可用的,如下图所示:

 

 

 在Java语言中,可作为GC roots的对象包括以下几种;

  • 虚拟机栈中引用的对象
  • 方法区中静态熟悉的对象
  • 方法区中常量引用的对象
  • 本地方法栈JNI引用的对象

Tips:内部非静态类,因为内部非静态类包含了一个外部类的引用,所以即便外部类没有的引用,也不能被GC。而内部静态类这不会包含外部类的一个引用。

1.3:life or death?

即使在可达性分析中,不可达的对象也不是非死不可!要标记一个对象是否死亡?至少经过两个标记过程,如果对象进行可行性分析后,发现不可达,则会被标记第一次并进行一次筛选,筛选的条件是是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize方法是否已经被虚拟机调用过,这两种情况都被是为没有必要执行情况。在GC的F_Queue队列中,调用该对象的finalize()方法,若在该方法中,成功拯救自己-只要重新与引用链上的任何一个对象引用建立关系即可!必须把自己this关键字赋值给某个类变量或者对象的成员变量。

1.4:回收方法区

  很多人都认为在方法区中没有垃圾回收的,Java虚拟机规范中输不要求在方法区(永久代)进行垃圾回收的,而且在方法区(永久代)的垃圾回收的性价比很低,在堆的新生代中一次GC可以回收70%-90%的空间,而方法区(永久代)的垃圾回收效率远低于此!

  正式由于方法区(永久代)中存放在常量,类模板信息等,所以回收的内容主要是废弃常量和无用的类。

  • 判断是否废弃常量:如常量池中常量没有引用指向,则为废弃常量。
  • 无用类:当同时满足下面3个条件的才可以被称为无用得类
    • 该类的所有实例都已经被回收,Java堆中不存在该来的任何实例对象。
    • 加载该类的ClassLoader已经被回收
    • 该类的对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射来方位该类的方法。

2:垃圾回收算法

2.1:标记清除法

  最基础的算法就是标记清除法,分为标记和清除两个阶段,首先标记所有需要回收的对象,在标记完成后,统一回收所有被标记的对象,后续方法都是对这种方法的改进,这种方法有两个不足,一个是效率问题,另一个是空间问题,标记清除后会产生大量的不连续的内存碎片,空间碎片太多会导致后续程序运行中,需要分配大内存对象时,无法找到足够连续的空间而不得不再触发一次垃圾回收操作。

2.2:复制法

  为了解决效率问题,将内存按照容量划分为大小相等的两块,每次使用其中一块,当这一块内存用完了,就将还存活的对象复制到另一个内存块上面,然后将已使用过过的内存块一次清理掉。这样是的每次都对整个半区进行内存回收,这种算法的代价就是为了提高效率,留下来原来的一半内存。

 

现在主流的虚拟机都采用这种方法来回收新生代,研究表明新生代的98%对象都是很多就死亡了的,所以不需要按照1:1的方式来分配内存,而是将一块较大的Eden空间和两块小的Survivor内存空间。每次使用Eden和一个Survivor,当回收时,将Eden和该Survivro空见的存活的对象一次性复制到另外一块Survivor空间中,然后再清理掉Eden和Survivor空间的。按照Eden:Survivor1:Survivor2 = 8:1:1。来划分内存,也就是每次新生代可以使用内存区的90%的内存。但是当真的10%的Survivor每次不够存储前面存活的对象时,也要依赖其他内存(老年代)进行担保。

2.3:标记整理法

 上面复制算法,在对象存活率较高的时候,就要进行很复杂的操作,效率低下。在老年代中对象存活时间久长,不适合使用复制算法。针对老年代的特点,与标记清除类似,先标记所有存活的对象,让对象向一端移动,然后直接清理掉端节边界以外的内存。

2.4:分代收集算法

  根据不同对象的存活周期的不同,将内存块划分为几块。一般将Java分为新生代和老年代,这样根据哥哥年代的对象存活周期特点采用最适用的收集算法;新生代中很多对象都是早生夕死的,每次GC时都会有大量对象死了,所以采用复制算法。而老年代因为对象存活率较高,没有额外的空间进行担保,就必须进行标记清理或者标记整理算法进行回收。

posted @ 2020-02-29 16:02  大朱123  阅读(138)  评论(0编辑  收藏  举报