GC垃圾回收
概述
垃圾回收是一块大内容,我们用三个问题来了解一下垃圾回收的过程
- 什么是垃圾
- 在什么时候回收垃圾
- 怎么回收
什么是垃圾
判断堆内存中的对象是否是垃圾,我们有两种方法
引用计数算法
原理
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时。计数器值就减一;任何时候计算器为零的对象就是不可能在被使用的。
优点
原理简单,判断效率高。
缺点
需要占用一些额外的内存空间,还有很难解决对象之间循环引用(两个对象相互引用)的问题。
解决办法
当循环引用的时候,可以手动去除掉互相引用
可达性分析算法
原理
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
可作为GC Roots 的对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
- 方法区中常量引用的对象,譬如字符串常量池里的引用。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
- java虚拟机内部引用,如基本数据类型对应的Class对象,一些常驻的异常对象等
- 所有被同步锁持有的对象。
在什么时候回收
判断步骤
- 如果对象到GCRoots没有引用链,则进行第一次标记。
- 进行筛选判断对象是否有必要执行finalize方法。
- 如果对象没有重写finalize方法或者finalize方法已经被调用过则没有必要在调finalize方法。
例子
/**
* 标记为垃圾之后,会尝试一次自救finalize,如果重新复制则不会被回收,如果第二次在出现GC,则不会在执行finalize
*/
public class FinalizeTestGC {
private static FinalizeTestGC testGC = new FinalizeTestGC();
public static void main(String[] args) throws InterruptedException {
//复制为null,已经没有引用了
testGC = null;
System.gc();
Thread.sleep(2000);
testGC.test();
testGC = null;
System.gc();
testGC.test();
}
private void test(){
System.out.println("成功跳脱GC");
}
@Override
protected void finalize(){
System.out.println("开始自救");
testGC = this;
System.out.println("自求结束");
}
}
执行结果
从结果可以看出,第一次赋值为空,进行垃圾回收,程序运行了finalize方法,重新赋上值;第二次为空时,进行垃圾回收就不会在走finalize方法了,这也说明了finalize方法只能调一次,最后就没垃圾回收了。
怎么回收
各种 GC 算法在删除不可达对象时略有不同, 但总体可分为三类: 清除( sweeping)、整理( compacting)和复制( copying)
经典算法
标记-清除算法
首先标记出所有需要回收的对象,在标记完成后,统一的回收掉被标记的对象,也可以反着来。
缺点:执行效率不稳定,内存空间碎片化问题
标记-整理算法
其中的标记阶段是跟标记清除算法一样的,但是后续步骤不是对对象进行直接回收,而是让所有存活的对象都向内存空间一端移动,然后直接清除掉边界以外的对象。
优点:吞吐量提高了
缺点:移动存活对象并更新对象引用会暂停用户线程(stop the world)
标记-复制算法
它将可用的内存分成两块,每次只使用一块内存,当这一块内存用完后就将还存活的对象复制到另一块内存中,然后把已使用的内存空间一次性清除掉。
例如:我们新生代就分成Eden和Survivor比例为8:1。
优点:执行效率高,没有碎片化问题
缺点:需要浪费内存,当存活对象太多时不太适合
衍生算法
分代算法
将堆分成不同的区域,一般至少分成新生代和老年代,然后先将对象放在新生代,进行一次回收,新生代使用标记复制算法,将存活的对象复制到Survivor区,然后进行第二次回收,将Eden和Survivor区的存活对象复制到空闲的Survivor区,以此类推,当对象达到一定年龄(回收次数)的时候,就将对象复制到老年代,老年代早期进行标记清除算法,当老年代内存空间出现碎片化过多时进行标记整理算法回收。
优点:回收效率比较高,空间利用率比较好,由于将堆分成了几个部分,回收时不需要扫描整个堆空间,提高了吞吐量
问题:跨代引用问题?新生代可能被老年代引用,但是我们又不能扫描整个老年代
解决办法:只需要在新生代上建立一个全局的数据结构(记忆集),这个结构把老年代分成若干小块,表示出老年代的那一块内存会存在跨代引用。此后当发生MinorGC时,只有包含了跨代引用的小块内存里的对象才会被加入到GCRoots进行扫描。
增量算法
用户线程与垃圾收集线程并发处理, 它并不会等GC执行完, 才将控制权交回程序, 而是一步一步执行, 跑一点, 再跑一点, 逐步完成垃圾回收, 在程序运行中穿插进行. 极大地降低了GC的最大暂停时间.
优点:提高了响应速度