垃圾收集(Garbage Collection)机制
前言
垃圾收集(Garbage Collection)简称GC。
1.GC需要回收哪些内存?
主要回收堆,其次是方法区,栈中不需要回收,线程结束,栈上的内存就会被释放。
2.回收的基本单位?
内存的单位是“字节”,但回收是按“对象”为单位进行的。
1.标记:判断对象生死
垃圾收集器在对堆进行回收之前,第一件事就是要确定堆中的对象是否“存活”,因此我们需要用标记的方法来给对象打上“生死标签”。
判断对象生死有下面两种方法:
(1)引用计数法(Python中使用,这里做简单介绍)
众所周知,创建一个对象,它必须被引用才会有意义,不然如何寻找这个对象呢?
所谓的引用计数法,就是在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加一;当引用失效时,计数器的值就减一;任何时刻计数器为零的对象就不可能再被使用了,此时该对象就会被标记。
优点:
原理简单,判定效率高效。
缺点:
致命缺点,它无法解决对象之间相互引用的问题。
举一个例子:
public class Test1 {
public Object o1 = null;
public static void testGC(){
//创建完成时两个对象的引用计数器都为1
Test1 A = new Test1(); //假设A指向对象1
Test1 B = new Test1(); //假设B指向对象2
//两个对象相互循环引用(此时两个对象的引用计数器都为2)
A.o1 = B;
B.o1 = A;
//让两个引用为空(此时计数器减一,两个对象的引用计数器都为1)
A = null;
B = null;
}
}
此时我们再想使用对象1和对象2时已经不可能了:当我想找对象1的引用时,发现该引用在对象2中;当我想找对象2的引用时,发现该引用在对象1中。所以说这两个对象已经废了,但如果用引用计数法来标记的话,它们却还是活着的对象,这就是引用计数法的最大缺陷。
(2)可达性分析(Java中使用)
Java中是通过可达性分析(Reachability Analysis)算法来判定对象是否存活,这个算法的基本思路是通过一系列被称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走的过程称为“引用链”,如果某个对象到“GC Roots”间没有任何引用链相连,也就是不可达时,则证明此对象不可能再被使用。
图中对象object5、object6、object7都是不可达的,因此它们都将被判定为可回收的对象。
从可达性分析的定义中可以看到“GC Roots”这个根对象是一个节点集,因此可以作为根对象并不是只有一个节点,而是有一个集合。
在Java中固定可以作为GC Roots的对象包括但不限于以下几种:
- 在方法区中类静态属性引用的变量,比如:Java类的引用类型静态变量
- 在方法区中常量引用的对象,比如:字符串常量池中的引用
- Native方法引用的对象
2.回收:把死的对象回收回去
知道垃圾是谁了,那么接下来就是清理垃圾的过程,Java中的垃圾收集算法有三种:标记-清除算法、标记-复制算法、标记-整理算法。从名字我们就可以看出来标记的必要性了。
(1)标记-清除算法
如它的名字一样,该算法分为两个阶段:“标记”和“清除”。首先标记处所有需要回收的对象,再统一回收被标记的对象;或者反过来,标记存活的对象,回收未被标记的对象。该算法是最基础的收集算法。
优点:
效率高(但是缺点也和效率有关)
缺点:1.效率不稳定。试想一下如果我们在标记过程中,正好标记的是那些“大多数”,再往极限想,全部都要标记,那标记的过程就是一个极其耗时的过程。
2.内存空间的碎片化。当完成标记和清除操作后,内存空间会产生大量的不连续内存碎片(就像删除数组中的元素却不移动其他元素一样),这就可能导致今后我们在给一个大对象分配空间时找不到一个足够大的连续内存而不得不再出发另外一次垃圾收集。
(2)标记-复制算法
也是“人”如其名,常被简称为复制算法。是为了解决标记-清除算法的缺点被提出的。它将可用的内存分为大小相等的两份,每次只使用其中一块,这一块的内存用完了,就将存活的对象复制到另外一块上,然后再把已使用的那一块内存一次性清理掉。这样就解决了标记-清除算法的效率不稳定和内存空间碎片化的问题。
优点:
实现简单,运行高效
缺点:严重浪费内存空间
(3)标记-整理算法
所谓标记-整理算法,就是标记-清除算法的另外一个改进版本,顾名思义:标记完成后先将所有存活的对象往内存空间的一端移动,然后直接清除掉边界以外的内存。
优点:
解决了内存空间碎片化的问题
缺点:移动存活的对象是一种极其负重的操作,而且想移动存活的对象必须先全程暂停用户应用程序才能进行。像这种停顿被描述为"Stop The World"。