垃圾收集器的内存回收机制

前言

对于垃圾收集器回收内存应该有以下几点思考:

  1. 哪些内存需要回收?
  2. 什么时候被回收?
  3. 如何回收?

哪些内存需要被回收?

在Java内存运行时区域中,虚拟机栈、本地方法栈、程序计数器3个区域随线程而生,随线程而亡。所以它们不需要垃圾收集器来管理。

方法区则不一样,堆中存放的对象实例和方法区中的内存只有在运行期间才知道,这部分内存的分配和回收都是动态的,垃圾收集器关注的就是这部分内存。

内存什么时候被回收?

在堆中存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收之前,需要知道哪些对象还“存活着”,哪些已经“死去”。

而判断对象的存活还是死去有以下两种方式:引用计数算法可达性分析算法

引用计数算法

引用计数算法是通过在对象中添加一个引用计数器,每当有一个地方引用该对象时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就表示永远不会再被引用的对象,当GC到了回收内存时,就会回收这部分对象。

该算法实现简单,判定效率也很高,但是主流的Java虚拟机里都没有选用引用计数算法来管理内存,因为它很难解决对象之间相互引用的问题。

 
/**
* testGC()方法执行后,objA和objB会不会被GC呢?
*/
public class ReferenceCountingGC {

public Object instance = null;

private static final int _1MB = 1024 * 1024;

/**
* 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过
*/
private byte[] bigSize = new byte[2 * _1MB];

public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;

objA = null;
objB = null;

// 假设在这行发生GC,objA和objB是否能被回收?
System.gc();
}
}

在该示栗中,可以发现第一个实例化的ReferenceCountingGC对象同时被objA和objB.instance所引用,第二个实例化的对象同理,当将objA和objB设为null后,由于这两个对象还持有其他引用,因此无法被垃圾收集器回收。

可达性分析算法

在主流的商用程序语言(Java、C#)的主流实现中,都是通过可达性分析来判定对象是否存活。

该算法的思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

上图中可以发现,object1、object2、object3都可以到达GC Roots,因此它们是存活的对象,而object5、object6、object7虽然它们互联,但它们却无法到达GC Roots,因此它们三兄弟是可以被收回的对象。

在Java中,可以作为GC Roots的对象有全局性引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)。

当然,对于该算法中不可达的对象也不是“非死不可的”。在下面的这种情况下一个对象也可以做到起死回生。

在执行该算法时,真正要宣告一个对象的死亡,需要经历两次标记过程。如果发现一个对象没有直达GC Roots的引用链时,会对该对象进行第一次标记,并且会进行一次筛选操作,筛选的条件是此对象是否有必要执行finalize()方法。

如果一个对象没有覆写finalize()方法或是finalize()方法已经被虚拟机调用过,则表示没必要执行finalize()方法。

如果一个对象被判定为有必要执行finalize()方法。那么该对象将会被放在一个F-Queue的队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行该对象的finalize()方法。如果在finalize()方法中该对象成功的拯救了自己(重新与引用链上的一个对象建立关联),那么在该对象在进行第二次标记时则可以被移除出“即将回收”的集合。否则在进行第二次标记时它就真的完成了它的使命然后被收回了。

任何一个对象的finalize()方法只会被系统自动调用一次。

方法区中的垃圾回收

对于方法区中的垃圾收集,垃圾收集器主要回收废弃的常量无用的类

判断一个常量是否废弃只需要看常量是否还会被使用。拿常量池举例,如果一个字符串“abc”存在于常量池中,而当前Java程序中没有任何一个对象叫“abc”,即没有任何一个String对象引用常量池中的“abc”常量,则表示这是一个废弃的常量。

对于无用的类的判定则需要通过以下条件判断。

  1. 该类的所有实例都已经被回收。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,即无法通过反射访问该类的方法。

如何回收内存?

对于垃圾收集算法的实现有标记-清除算法复制算法标记-整理算法等,这里主要介绍这三种算法。

标记-清除算法

标记-清除算法分为两个阶段,分别是“标记”和“清除”阶段。首先标记出所有需要被回收的对象,在标记完成后统一回收所有被标记的对象。是否被标记就看对象是否能到达GC Roots。

缺点:

  1. 效率问题。标记以及清除两个过程的效率都不高。
  2. 内存碎片。在标记清除后内存中会存在大量的内存碎片。

复制算法

复制算法通过将内存区分为两个大小相等的区域。每次只使用其中的一块,当这块内存即将耗尽时,JVM将程序暂停,开始GC线程执行复制算法。将这块即将耗尽内存中存活的对象复制到另一块内存中,并且严格的按照内存地址排列。最后将已使用过的内存一次性清理掉,这样又留出一半的内存空间等待下次复制使用。

以上是执行复制算法之前的内存,可以看出只使用了左边的一块内存,右边的内存用于下一次内存复制。当执行复制之后的结果如下所示

复制完成之后,左边内存中的所有对象被一次性清理,并且存活的对象被复制到右边的内存中按照内存空间整齐的排列着。

缺点:

  1. 一次性只能使用一半的内存,比较奢侈。
  2. 如果当前内存中对象的存活率十分高,那么意味着对象的复制操作也比较多,效率将会变低。

标记-整理算法

标记-整理算法跟标记-清除算法类型,只不过标记对象后不是直接对可回收的对象进行清理,而是让所有存活的对象向一端移动,然后清除掉死去的对象。

算法的使用

如今商业虚拟机的垃圾收集器都采用“分代收集”算法。这种算法就是针对不同情况的对象使用不同的算法。例如针对新生代的对象,由于其总是会有大量的对象死去,只有少部分存活,那么就使用复制算法。而对于那些老年代的对象因为其存活率高,就使用标记-清除算法或标记-整理算法。

猜你喜欢

转载自blog.csdn.net/weixin_39654286/article/details/88579240