一、简介
二、如何确定垃圾
2.1 引用计数法
在java中,引用和对象是关联的。如果要操作对象则必须用引用进行。因此,一个简单的办法是通过引用计数来判断一个对象是否可以回收。在对象头中分配一个空间来保存该对象被引用的次数,如果该对象被引用了,则加1,如果该引用被取消了,则减1,当该对象的引用为0时,就被当成垃圾回收了。
垃圾收集的开销被分摊到整个应用程序的运行当中;当某个对象的引用减为0时,需要递归遍历它所指向的所有域,将它所有域所指向的对象的引用计数都减1,然后才回收当前对象,若指向的对象减1之后也变成0了,也将被一起回收。
该方法有一个循环引用问题,会导致内存泄露。
比如:
public class GcDemo {
public static void main(String[] args) {
//分为6个步骤
GcObject obj1 = new GcObject(); //Step 1
GcObject obj2 = new GcObject(); //Step 2
obj1.instance = obj2; //Step 3
obj2.instance = obj1; //Step 4
obj1 = null; //Step 5
obj2 = null; //Step 6
}
}
class GcObject{
public Object instance = null;
}
它们的互相引用如图所示:
如图,执行到step4时可以明显的看出两个对象都被引用了两次,引用计数都为2。
而执行step5和step6后如图所示:
Step5:栈帧中obj1不再指向Java堆,GcObject实例1的引用计数减1,结果为1;
Step6:栈帧中obj2不再指向Java堆,GcObject实例2的引用计数减1,结果为1。
两者都不为0,因此不会被当成垃圾回收,便产生了内存泄露问题。
2.2可达性分析法
目前主流的虚拟机都是采用GC Roots Tracing算法。该算法的核心是从GC Roots对象作为起始点,如果一个对象和GC Roots对象之间没有可达路径,则称该对象不可达。
如图,可以作为GC Roots的对象如下:
- 虚拟机栈的栈帧的局部变量表所引用的对象;
- 本地方法栈的JNI所引用的对象;
- 方法区的静态变量和常量所引用的对象;
可以看到实例3和5之间虽然连通,但是没有一个跟GC Roots关联,是不可达的。
不可达对象变成可回收对象至少要经过两次标记过程,两次标记后将面临回收。
finalize()方法是对象脱逃死亡命运的最后一次机会。
三、垃圾回收算法
3.1标记清除算法(Mark-Sweep)
最基础的垃圾回收算法,分为两个阶段,标记和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。如图:
从图中我们可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
3.2复制算法(copying)
为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图:
这种算法虽然实现简单,内存效率高,不易产生碎片;但是最大的问题是可用内存被压缩到了原本的一半,且存活对象增多的话,Copying算法的效率会大大降低。
3.3标记整理算法(Mark-Compact)
结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活的对象移动向内存的一端,然后清理端边界外的对象。如图:
该算法的缺点是效率低。
参考
https://www.zhihu.com/question/21539353中@Gityuan的回答
https://blog.csdn.net/luzhensmart/article/details/81431212