一、内存回收对象
1、在Java内存运行时区域的各个部分中,程序计数器、虚拟机栈和本地方法栈这三个区域随线程而产生和消亡;栈中的栈帧随着方法的进入和退出相应地执行入栈和出栈操作。每一个栈帧中分配的内存基本上是在类结构确定下来时就已经确定了(在运行时会由JIT编译器进行一些优化,但大体上可以认为是在编译期就可以确定的)。所以这几个区域的内存分配和回收都具备确定性,因为方法结束或线程结束时,内存就跟着回收了,所以这几个区域不需要过多地考虑内存回收。
2、一个接口的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存可能也不一样,而且只有在程序运行时才能知道会创建哪些对象,所以对于Java堆和方法区这两个区域的内存分配和回收都是动态的,所以这部分内存是垃圾收集器主要考虑的。
二、判断对象是否存活
1、引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为0的对象就是不可能再被使用的。这种算法缺点是很难解决对象之间相互循环引用的问题,如下代码所示:到最后a1和a2这两个对象就已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数器都不为0,于是就无法通知GC回收它们。
A a1 = new A();
A a2 = new A();
a1.instance = a2;
a2.instance = a1;
a1 = null;
a2 = null;
主流的Java虚拟机都没有采用引用计数法来管理内存。
2、可达性分析算法
该算法的原理是通过一系列称为"GC Roots"的对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称为引用链;当一个对象到GC Roots没有任何引用链相连时,则该对象是不可用的。如下图所示,对象object 5、object 6、object 6虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为可回收的对象。
在Java中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
主流的Java虚拟机都是采用可达性分析来判定对象是否是存活的。
三、引用
1、无论是引用计数算法还是可达性分析算法,判定对象是否存活都与"引用"有关。在JDK1.2之前,Java中的引用定义为:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,则称这块内存代表着一个引用。这种定义描述的对象只有被被引用和没有被引用两种状态。如果要实现这样的缓存功能:当内存空间还足够时,则保留在内存之中,如果内存空间在进行垃圾收集后还是很紧张,则可以回收这些对象。则该定义有些无能为力。在JDK1.2之后,将引用分为强引用、软引用、弱引用和虚引用四种,且四种引用强度依次逐渐减弱。其中只有强引用FinalReference类是包内可见的,其它三种引用类型均为public,可以在程序中直接使用。
2、强引用
强引用就是类似"Object obj = new Object()"、"Object obj2 = obj"这类的引用。具有的特点如下:
- 强引用可以直接访问目标对象
- 只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象,JVM宁愿抛出OOM异常。
- 强引用可能导致内存泄漏
class CheckReferenceQueue extends Thread{
private ReferenceQueue<MyObject> referenceQueue;
public CheckReferenceQueue(ReferenceQueue<MyObject> referenceQueue) {
this.referenceQueue = referenceQueue;
}
@Override
public void run() {
Reference<MyObject> reference = null;
try {
// 如果对象被回收则进入引用队列,该队列是一个阻塞队列
reference = (Reference<MyObject>) this.referenceQueue.remove();
} catch (Exception e) {
e.printStackTrace();
}
if (null != reference) {
System.out.println("被回收对象的软引用入队列,其所引用的对象为:" + reference.get());
}
}
}
public class MyObject {
private String name;
// 占点内存,以便观察回收情况
private byte[] bigSize = new byte[1 * 1024 * 1024];
public MyObject(String name) {
this.name = name;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println(this + " 被调用finalize方法进行回收");
}
@Override
public String toString() {
return this.name;
}
public static void main(String[] args) throws InterruptedException {
MyObject instance1 = new MyObject("对象A"); // 强引用
MyObject instance2 = new MyObject("对象B"); // 强引用
ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<MyObject>(); // 创建引用队列
// 构造instance1对象的软引用,所以此时的instance1既有强引用,又有软引用;而instance2只有一个强引用
SoftReference<MyObject> softReference = new SoftReference<MyObject>(instance1, referenceQueue);
new CheckReferenceQueue(referenceQueue).start(); // 开启新线程检查引用队列,监控对象回收情况
instance1 = null; // 删除强引用,还有一个软引用
instance2 = null; // 删除强引用,没有了其它任何引用
System.out.println("***** 第一次进行垃圾收集开始 *****");
System.gc();
// 因为finalize方法优先级很低,所以暂停1秒以等待它
Thread.sleep(1000);
System.out.println("***** 第一次进行垃圾收集结束 *****");
System.out.println("***** 分配一大块内存,强迫进行垃圾回收 *****");
byte[] b = new byte[3 * 1024 * 978]; // 分配一块较大的内存区,强迫虚拟机进行垃圾回收
System.out.println("强制进行垃圾回收后,软引用所引用的对象为:" + softReference.get());
}
}
以下是程序的运行结果截图,MyObject类中定义了一个成员变量bigSize,会占用2M的堆内存空间,所以程序在实例化该类的两个对象instance1和instance2之后,就至少会占用大于4M以上的堆内存空间,而由虚拟机参数"-Xmx5M"指定了堆内存总共大小才5M,所以程序执行"System.gc()"虽然不会保证一定会执行垃圾回收,但是在只有5M内存就占用了4M以上内存的情况下理论上是会执行垃圾回收的,如预料之中,确实在执行了"System.gc()"后启动了垃圾回收,由于instance2所指向的对象连仅有的一个强引用也失去了,它就是不可达对象,所以就被回收了,但是instance1所指的对象虽然失去了强引用,却还有一个软引用,所以在第一次垃圾收集时,由于内存还够用,所以instance1并没有被回收,说明GC的内存够用的情况下不会回收软引用对象。解下来由于需要在堆内存中分配一块大约3M的内存空间(new byte[3 * 1024 * 978]),该操作会使得系统堆内存变得使用紧张,所以迫使虚拟机执行新一轮的垃圾回收,这次回收后instance1所指的对象就被真正的回收了,说明了在内存紧张的情况下,软引用会被回收,而当软引用被回收时,如果注册了引用队列,还会进入注册的引用队列。
class CheckReferenceQueue extends Thread{
private ReferenceQueue<MyObject> referenceQueue;
public CheckReferenceQueue(ReferenceQueue<MyObject> referenceQueue) {
this.referenceQueue = referenceQueue;
}
@Override
public void run() {
Reference<MyObject> reference = null;
try {
// 如果对象被回收则进入引用队列,该队列是一个阻塞队列
reference = (Reference<MyObject>) this.referenceQueue.remove();
} catch (Exception e) {
e.printStackTrace();
}
if (null != reference) {
System.out.println("被回收对象的软引用入队列,其所引用的对象为:" + reference.get());
}
}
}
public class MyObject {
private String name;
public MyObject(String name) {
this.name = name;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println(this + " 被调用finalize方法进行回收");
}
@Override
public String toString() {
return this.name;
}
public static void main(String[] args) throws InterruptedException {
MyObject instance = new MyObject("对象A"); // 强引用
ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<MyObject>(); // 创建引用队列
// 构造instance对象的虚引用,所以此时的instance既有强引用,又有虚引用
PhantomReference<MyObject> phantomReference = new PhantomReference<MyObject>(instance, referenceQueue);
System.out.println("MyObject对象有强引用,还有虚引用时通过get方法取得强引用:" + phantomReference.get());
new CheckReferenceQueue(referenceQueue).start(); // 开启新线程检查引用队列,监控对象回收情况
instance = null; // 删除强引用,还有一个虚引用
Thread.sleep(1000);
int i = 1;
while (i <= 2) {
System.out.print("第" + i++ + "次调用System.gc\n\t");
System.gc();
Thread.sleep(1000);
}
}
}
运行该代码结果截图如下:从结果中可以发现,对虚引用的get()操作,总是返回null,即便强引用还存在时,也不例外,因为在虚引用的get()方法的实现中,总是返回null。另外,在第一次GC时,系统找到了垃圾对象,并调用其finalize()方法回收内存,但没有立即加入引用队列;第二次GC时,该对象真正被GC清除,此时,将其加入虚引用队列;也就是说在虚引用队列中,一旦取得了这个虚引用对象,则表示该对象正式被回收。
- 第一次:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,则它会被第一次标记且进行一次筛选,筛选的条件就是该对象是否有必要执行finalize()方法,当对象没有覆写finalize()方法,或者finalize()方法已经执行过,则都视为“没有必要执行”。如果该对象有必要执行finalize()方法,那么该对象会被放置在一个叫做F-Queue的队列中,并在稍候由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它,但是并不会等待它运行结束,因为如果在一个对象的finalize()方法中发生了死循环等情况则会导致F-Queue队列中其他对象永久处于等待,导致整个内存回收系统崩溃。
- 第二次:稍候GC将会对F-Queue队列中的对象进行第二次小规模标记,在此之前的第一次标记中,如果对象有必要执行finalize()方法,那么finalize()方法可以是对象逃脱被回收的最后一次机会,在finalize()方法只要重新与引用链上的任何一个对象建立关联即可,那么在第二次标记时将会把该对象移除“即将回收”的集合;如果此时对象还没有逃脱,那么基本上就会被真的回收了。
public class MyObject {
private static MyObject instance;
public void isAlive() {
System.out.println("I am alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize executed");
MyObject.instance = this; // 重新与GC Roots建立关联
}
public static void main(String[] args) throws InterruptedException {
instance = new MyObject();
instance = null; // 删除强引用,成为不可达对象
System.gc();
Thread.sleep(1000); // 因为finalize方法优先级低,暂停1秒以等待它
if (null != instance) {
instance.isAlive();
} else {
System.out.println("I am dead");
}
instance = null; // 再次删除强引用,成为不可达对象
System.gc();
Thread.sleep(1000);
if (null != instance) {
instance.isAlive();
} else {
System.out.println("I am dead");
}
}
}
class CheckReferenceQueue extends Thread {
private ReferenceQueue<MyObject> referenceQueue;
private Map<Reference<MyObject>, Object> resourceMap;
public CheckReferenceQueue(ReferenceQueue<MyObject> referenceQueue, Map<Reference<MyObject>, Object> resourceMap) {
this.referenceQueue = referenceQueue;
this.resourceMap = resourceMap;
}
@Override
public void run() {
Reference<MyObject> reference = null;
try {
// 如果对象被回收则进入引用队列,该队列是一个阻塞队列
reference = (Reference<MyObject>) this.referenceQueue.remove();
Object res = this.resourceMap.get(reference); // 取得对象所占用的资源
System.out.println("清理资源:" + res); // 模拟清理对象占用的资源
this.resourceMap.remove(reference); // 完全释放引用
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class MyObject {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("MyObject对象被调用finalize方法进行回收");
}
public static void main(String[] args) throws InterruptedException {
// 保存MyObject对象占用的资源,key是MyObject对象所关联的虚引用
Map<Reference<MyObject>, Object> resourceMap = new HashMap<Reference<MyObject>, Object>();
MyObject instance = new MyObject(); // 强引用
ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<MyObject>(); // 创建引用队列
// 构造instance对象的虚引用,所以此时的instance既有强引用,又有虚引用
PhantomReference<MyObject> phantomReference = new PhantomReference<MyObject>(instance, referenceQueue);
resourceMap.put(phantomReference, "MyObject对象占用的资源"); // 模拟MyObject对象占用的资源
new CheckReferenceQueue(referenceQueue, resourceMap).start(); // 开启新线程检查引用队列,监控对象回收情况
instance = null; // 删除强引用,还有一个虚引用
Thread.sleep(1000);
int i = 1;
while (i <= 2) {
System.out.print("第" + i++ + "次调用System.gc\n\t");
System.gc();
Thread.sleep(1000);
}
}
}
程序运行结果如下图所示,当MyObject对象被真正清除时,其关联的虚引用立马进入引用队列,然后释放相应的资源。
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法