Java垃圾收集机制(一)

一、内存回收对象

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 6object 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异常。
  • 强引用可能导致内存泄漏
3、软引用
软引用是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。也就是说JVM会根据当前堆内存的使用情况来判断何时回收,当堆使用率接近阀值时,就会回收软引用对象,而只要有足够的内存,软引用对象就可以在内存中存活下来。SoftReference类可以用来实现软引用。软引用对象可以用来实现对内存敏感的cache。 如下代码所示:运行该程序时使用虚拟机参数"-Xmx5M"来指定堆内存大小
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所指的对象就被真正的回收了,说明了在内存紧张的情况下,软引用会被回收,而当软引用被回收时,如果注册了引用队列,还会进入注册的引用队列。

如果在执行"new byte[3 * 1024 * 978]"时,分配的是一块比这更大的堆内存空间,那么即使回收了instance1对象,也无法满足内存的分配,此时尽管回收了instance1,但是也会出现OOM异常,如下图:new byte[4 * 1024 * 978]

4、弱引用
弱引用也是用来描述非必须对象的,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。但是由于垃圾收集器的线程通常优先级很低,所以并不一定能很快发现持有弱引用的对象;在这种情况下,弱引用对象可以存在较长的时间;而且与软引用一样,如果注册了引用队列,则在被回收时便会加入到该队列中。同样,弱引用对象也可以用来实现对内存敏感的cache。WeakReference类可以用来实现弱引用。
把软引用中那段代码中的SoftReference改成WeakReference,则可以发现在第一次进行垃圾收集时,只被弱引用关联的对象也被回收了。
5、虚引用
虚引用也称为幽灵引用或幻影引用。一个持有虚引用的对象,和没有引用几乎是一样的,随时都可能被回收,也就说一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是能在这个对象被收集器回收时收到一个系统通知,所以虚引用必须和引用队列一起使用,以便跟踪垃圾回收过程。可以用PhantomReference类来实现虚引用。
当垃圾收集器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,销毁这个对象时,将这个虚引用加入引用队列。如下代码所示:
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清除,此时,将其加入虚引用队列;也就是说在虚引用队列中,一旦取得了这个虚引用对象,则表示该对象正式被回收。
四、垃圾回收过程
1、在可达性分析算法中不可达的对象也并非是“非死不可的”,它们暂时处于“缓刑”阶段。真正宣告一个对象死亡,至少需要经历两次标记过程:
  • 第一次:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,则它会被第一次标记且进行一次筛选,筛选的条件就是该对象是否有必要执行finalize()方法,当对象没有覆写finalize()方法,或者finalize()方法已经执行过,则都视为“没有必要执行”。如果该对象有必要执行finalize()方法,那么该对象会被放置在一个叫做F-Queue的队列中,并在稍候由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它,但是并不会等待它运行结束,因为如果在一个对象的finalize()方法中发生了死循环等情况则会导致F-Queue队列中其他对象永久处于等待,导致整个内存回收系统崩溃。
  • 第二次:稍候GC将会对F-Queue队列中的对象进行第二次小规模标记,在此之前的第一次标记中,如果对象有必要执行finalize()方法,那么finalize()方法可以是对象逃脱被回收的最后一次机会,在finalize()方法只要重新与引用链上的任何一个对象建立关联即可,那么在第二次标记时将会把该对象移除“即将回收”的集合;如果此时对象还没有逃脱,那么基本上就会被真的回收了。
2、如下代码所示:
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");
		}
	}

}
运行程序结果如下图所示,执行第一次GC时,MyObject对象的finalize()方法被执行了,但是由于它与GC Roots重新建立了关联,所以并没有被回收;但是第二次执行GC时,由于该对象的finalize()方法已经被执行过了,所以并不会再次执行,所以在这次的回收过程中,由于并没有再次执行finalize()方法,所以不能再次重新与GC Roots建立关联,所以被回收了。

3、finalize方法与虚引用
在实际使用过程中应该尽量避免覆写finalize()方法,不要使用这种方法来拯救对象,因为它的运行代价高、不确定性大且无法保证各个对象的调用顺序。更不应该使用它来“关闭外部资源”等工作,稍微使用不慎就会造成内存泄漏等问题,如果确实需要关闭外部资源等工作,那么可以使用虚引用来关闭外部资源,因为在虚引用队列中的对象,事实上已经完成了对象的回收工作,是不可能再度复活该对象的,而且虚引用本身最大的作用就是用来跟踪对象回收,清理被销毁对象的相关资源,所以它不会造成内存泄漏等问题。
如下代码所示:
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对象被真正清除时,其关联的虚引用立马进入引用队列,然后释放相应的资源。
五、方法区回收
1、Java虚拟机规范可以不要求虚拟机在方法区实现垃圾收集。如果Java虚拟机实现了方法区的回收,则该区域的回收主要回收两部分的内容:废弃常量和无用的类。
2、废弃常量的回收与堆中对象的回收类似,但是对于无用的类的回收,需要同时满足以下三个条件才能算是"无用的类":
  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
虚拟机可以对满足以上三个条件的无用的类进行回收,但是对于无用的类,并不是一定非要回收。如果是HotSpot虚拟机,可以使用-Xnoclassgc参数来决定是否对无用的类进行回收。
3、在频繁自定义ClassLoader的情况下,需要虚拟机具备类的卸载功能,以保证永久代不会溢出。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值