垃圾收集
垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有
的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。
判断一个对象是否可被回收
1. 引用计数算法
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为
0 的对象可被回收。
在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为
循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
优缺点:
优点:执行效率高,程序执行受影响较小;
缺点:无法检测出循环引用的情况,导致内存泄漏;
2. 可达性分析算法
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:
虚拟机栈中局部变量表中引用的对象
本地方法栈中 JNI(Native方法) 中引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象
新生代用广度搜索,老生代用深度搜索
深度优先DFS一般采用递归方式实现,处理tracing的时候,可能会导致栈空间溢出,所以一般采用广度
优先来实现tracing(递归情况下容易爆栈)。
广度优先的拷贝顺序使得GC后对象的空间局部性(memory locality)变差(相关变量散开了)。
广度优先搜索法一般无回溯操作,即入栈和出栈的操作,所以运行速度比深度优先搜索算法法要快些。
深度优先搜索法占内存少但速度较慢,广度优先搜索算法占内存多但速度较快。
public class Test {
public Object instance = null;
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
a.instance = b;
b.instance = a;
a = null;
b = null;
doSomething();
}
} 方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性
价比不高。
主要是对常量池的回收和对类的卸载。
为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。
类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:
该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
finalize()
类似 C++ 的析构函数,用于关闭外部资源。但是 try-fifinally 等方式可以做得更好,并且该方法运行代价
很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
当一个对象可被回收时,如果需要执行该对象的 fifinalize() 方法,那么就有可能在该方法中让对象重新
被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 fifinalize() 方法自救,后面回
收时不会再调用该方法。
彻底死亡条件
条件1:通过GC Roots作为起点的向下搜索形成引用链,没有搜到该对象,这是第一次标记。
条件2:在fifinalize方法中没有逃脱回收(将自身被其他对象引用),这是第一次标记的清理。