前言
对于垃圾收集器回收内存应该有以下几点思考:
- 哪些内存需要回收?
- 什么时候被回收?
- 如何回收?
哪些内存需要被回收?
在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”常量,则表示这是一个废弃的常量。
对于无用的类的判定则需要通过以下条件判断。
- 该类的所有实例都已经被回收。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用,即无法通过反射访问该类的方法。
如何回收内存?
对于垃圾收集算法的实现有标记-清除算法、复制算法、标记-整理算法等,这里主要介绍这三种算法。
标记-清除算法
标记-清除算法分为两个阶段,分别是“标记”和“清除”阶段。首先标记出所有需要被回收的对象,在标记完成后统一回收所有被标记的对象。是否被标记就看对象是否能到达GC Roots。
缺点:
- 效率问题。标记以及清除两个过程的效率都不高。
- 内存碎片。在标记清除后内存中会存在大量的内存碎片。
复制算法
复制算法通过将内存区分为两个大小相等的区域。每次只使用其中的一块,当这块内存即将耗尽时,JVM将程序暂停,开始GC线程执行复制算法。将这块即将耗尽内存中存活的对象复制到另一块内存中,并且严格的按照内存地址排列。最后将已使用过的内存一次性清理掉,这样又留出一半的内存空间等待下次复制使用。
以上是执行复制算法之前的内存,可以看出只使用了左边的一块内存,右边的内存用于下一次内存复制。当执行复制之后的结果如下所示
复制完成之后,左边内存中的所有对象被一次性清理,并且存活的对象被复制到右边的内存中按照内存空间整齐的排列着。
缺点:
- 一次性只能使用一半的内存,真奢侈。
- 如果当前内存中对象的存活率十分高,那么意味着对象的复制操作也比较多,效率将会变低。
标记-整理算法
标记-整理算法跟标记-清除算法类型,只不过标记对象后不是直接对可回收的对象进行清理,而是让所有存活的对象向一端移动,然后清除掉死去的对象。
算法的使用
如今商业虚拟机的垃圾收集器都采用“分代收集”算法。这种算法就是针对不同情况的对象使用不同的算法。例如针对新生代的对象,由于其总是会有大量的对象死去,只有少部分存活,那么就使用复制算法。而对于那些老年代的对象因为其存活率高,就使用标记-清除算法或标记-整理算法。
更多了解,还请关注我的个人博客:www.zhyocean.cn