Java中的四种引用与垃圾回收

     java中垃圾回收的基本思想是考察每一个对象的可达性,即从根节点出发是否可以被访问到这个对象,如果这个对象被访问到说明这个对象正在被使用,如果所有的节点都无法获取到整个对象,那么说明这个对象不再被使用了,一般说来这种对象是需要被回收的,但是,一个这样的对象在某些条件下复活了自己,如果这样的话那么对他的回收是不合理的,那么我们可以定义如下的状态来确保这个对象可以被回收,而且不会引起其他的异常。

1.首先,这个对象是不可触及的,也就是说这个对象不能够通过根节点被访问到,这是一个首要的条件。

2.其次这个对象不能够复活自己,也就是说假如这个对象的所有的引用都被释放了,但是对象可能在finalize方法中复活,那么这个对象就不能够进行回收,也就是说,对象也同时应该满足不能够复活自己的条件。

能够达到这两个条件那么这个对象便进入了不可达到的状态,如果复活了自己,再次进行回收的时候也能够进行,这是因为finalize方法只调用一次。

体会一个对象复活自己的例子:

public class CanReliveObj {

	public static CanReliveObj obj;
	@Override
	protected void finalize() throws Throwable{
		super.finalize();
		System.out.println("CanReliveObj finalize called");
		obj = this;
	}
	
	@Override
	public String toString(){
		return "I am a CanReliveObj";
	}
	
	public static void main(String[] args) throws InterruptedException{
		obj = new CanReliveObj();
		obj = null;
		System.gc();
		Thread.sleep(1000);
		if(obj == null){
			System.out.println("Obj 是 null");
		}else{
			System.out.println("Obj 可用");
		}
		/**
		 * finalize()方法每次只能在释放销毁对象的时候被调用一次,
		 * 所以第一次会调用重写的方法让对象复活 obj=this.
		 * 但是,当第二次调用的时候就是原来的finalize方法
		 * 也就将对象释放
		 */
		System.out.println("第二次 gc");
		obj = null;
		System.gc();
		Thread.sleep(1000);
		if(obj == null){
			System.out.println("Obj 是 null");
		}else{
			System.out.println("Obj 可用");
		}
	}
}
运行结果如下:


可以看到在代码中将obj设置为null之后进行GC结果发现obj对象复活了,然而在下面的代码中再次释放GC,此时obj才真正的被回收。这是因为第一次GC时,在finalize方法调用之前虽然将obj设置为null,其在系统中已经被清除,但是finalize方法中对象的this引用依然会被传入到方法的内部,如果引用外泄那么对象就会复活,此时变为可触及的状态,但是finalize方法只会调用一次,因此在我们的第二次进行GC的时候,对象就不会再复活了,对象就被回收了,obj也变为了null。其实finalize方法是一个不太好的模式,因为它不是很适合释放资源,其方法内如果有复活对象自己的代码,那么就会在无意中复活自己,其次,这个方法是被系统调用的,调用的时间并不是明确的,所以不推荐使用这个方法进行资源回收,如果使用的话,尽量在try···catch···finally语句块中使用。

java中有四个级别的引用,其与对象的垃圾回收有着紧密的关系,认识一下这4中引用:

1.强引用

除了我们要说的强引用之外,其他的三种均可以在java.lang.ref中找到其他三种级别的引用,那么什么是强引用?强引用就是程序中一般使用的引用类型,强引用的对象想是可达到的,不会被回收,相对的软引用,弱引用和徐引用的对象是软可达,若可达和虚可达的,在一定条件下是可以被回收的.
强引用的例子我们经常见,如下:
StringBuffer str = new StringBuffer("Hello world");
    假设我们写的代码实在函数体内运行的,那么局部变量str就会分布在栈上,而对象StringBuffer实例就会被分布在堆上,局部变量str指向StringBuffer实例所在的堆空间,通过str来操作这个实例,那么str就是StringBuffer实例的强引用。
此时如果我们这样处理的话:
StringBuffer str1 = str;
   那么str所指的对象也将会被str1所指向,同时在局部变量表上会分配空间存放str1变量,此时StringBuffer实例就有两个同时指向自己的引用,对引用使用“==”操作,可以看到结果相等,这说明两个指向了同一个内存地址,但并不表示两个操作数所指向的对象相等。
总结一下,强引用有如下的特点:
1.强引用可以直接访问目标对象。
2.强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿跑出OOM错误也不会回收强引用所指向的对象。
3.强引用可能会导致内存泄漏。

2.软引用

软引用是比强引用稍微弱一点的引用类型,一个对象 仅仅持有软引用,那么当内存空间不足的时候,就会释放这些内容,回收空间,软引用使用java.lang.ref.SoftReference来实现,下面的例子将会说明在空间不足的时候就会回收软引用。
public class SoftRef {
	public static class User{
		public User(int id,String name){
			this.id = id;
			this.name = name;
		}
		public int id;
		public String name;
		
		@Override
		public String toString(){
			return "id=" +String.valueOf(id) +", name=["+name+"]";
		}
	}
	
	public static void main(String[] args){
		User u = new User(1,"gym");
		SoftReference<User> userSoftRef = new SoftReference<User>(u);
		u=null;
		System.out.println(userSoftRef.get());
		System.gc();
		System.out.println("After GC:");
		System.out.println(userSoftRef.get());
		
		byte[] b = new byte[1024*925*7];
		System.gc();
		System.out.println(userSoftRef.get());
		
	}

}
   代码中建立了User类的实例,同时将u变量为强引用,通过强引用建立了弱引用,userSoftRef;在代码中将u设置为null,取消所有的强引用,将其设置为一个仅有软引用的对象,在下面的代码中我们重新获得强引用对象,之后进行GC,在此时再次使用软引用获取到了强引用的对象,然而在我们再次开辟一个大的内存的时候,系统资源紧张,我们通过一次GC模拟系统进行GC,其实这个是多余的,因为在分配较大的数据时,系统会自动的进行GC,这里仅是为了说明问题,我们再次从软引用获取软引用时,已经失效,说明软引用被回收,所以也就是说软引用不会引起系统的内存溢出现象,每一个软引用都会附带一个引用队列,当对象的可达性状态发生改变时,软引用对象就会进入引用队列,通过这个引用队列可以跟踪对象的回收情况。代码如下:
public class SoftRefQue {
	public static class User{
		public User(int id,String name){
			this.id = id;
			this.name = name;
		}
		public int id;
		public String name;
		
		@Override
		public String toString(){
			return "id=" +String.valueOf(id) +", name=["+name+"]";
		}
	}
	
	static ReferenceQueue<User> softRefQue =null;
	public static class CheckRefQueue extends Thread{
		@Override
		public void run(){
			while(true){
				if(softRefQue != null){
					UserSoftReference obj = null;
					try{
						obj = (UserSoftReference) softRefQue.remove();
					}catch(InterruptedException e){
						e.printStackTrace();
					}
					if(obj != null){
						System.out.println("user id " + obj.uid+"is delete");
					}
				}
			}
		}
	}
	
	public static class UserSoftReference extends SoftReference<User>{
		int uid;
		public UserSoftReference(User referent, ReferenceQueue<? super User> q){
			super(referent,q);
			uid = referent.id;
		}
	}
	
	public static void main(String[] args) throws InterruptedException{
		Thread t = new CheckRefQueue();
		t.setDaemon(true);
		t.start();
		User u = new User(1,"geym");
		softRefQue = new ReferenceQueue<User>();
		UserSoftReference userSoftRef = new UserSoftReference(u, softRefQue);
		u = null;
		System.out.println(userSoftRef.get());
		System.gc();
		//内存足够,不会被回收
		System.out.println("After GC");
		System.out.println(userSoftRef.get());
		
		
		System.out.println("try to create byte array and GC");
		byte[] b = new byte[1024*925*7];
		System.gc();
		System.out.println(userSoftRef.get());
		
		Thread.sleep(1000);
		
	}
	
}
   值得注意的地方有两个:我们自己定义了一个自定义的软引用类,它继承了SoftReference类,为的是能够了解到使用记录User.uid知道哪个实例被回收了;下一个就是我们在构造这个软引用时使用了ReferenceQueue软引用队列实例化对象,这样,当软引用对象被回收时,这个对象就会加入到这个引用队列,也就是说我们追踪是要基于ReferenceQueue来实现的,我们指定堆的大小-Xmx10m
执行结果如下:


   这说明再开辟一个较大的内存时,我们的user的软引用进行了回收,同时以由于开辟的内存较大,已经出超出了我们定义的堆的大小10m,所以随后跑出了一个OOM错误,但是,这个说明了当我们预留的内存不够为新的内容开辟时,就会回收软引用对象,不过,加入一个对象有很多的软引用,但是其有一个强引用,那么GC就不会回收这些软引用对象。

3.弱引用

     弱引用是一种比软引用较弱的引用类型,在系统进行GC时,发现如引用不管系统的堆空间使用如何,都会将其进行回收,但是由于垃圾回收器的线程的优先级较低,因此并不一定会很快的发现持有弱引用的对象,这种情况下弱引用可能会较长时间存在堆中,一旦一个弱引用对象被垃圾回收器回收,便会加入到一个注册的引用队列中,这一点和软引用很像,但是,弱引用是通过java.lang.WeakReference类实现:

public class WeakRef {

	public static class User{
		public User(int id,String name){
			this.id = id;
			this.name = name;
		}
		public int id;
		public String name;
		
		@Override
		public String toString(){
			return "id=" +String.valueOf(id) +", name=["+name+"]";
		}
	}
	
	public static void main(String[] args){
		User u = new User(1, "geym");
		//去除强引用
		WeakReference<User> userWeakRef =new WeakReference<User>(u);
		u=null;
		
		System.out.println(userWeakRef.get());
		System.gc();
		//不管当前的内存是否够用,都会回收
		System.out.println("After GC");
		System.out.println(userWeakRef.get());
		
	}
}
代码中,我们将u的强引用去掉,并且在这个操作之前通过强引用构建了弱引用,之后开始进行GC,执行结果如下:


可以看到GC后我们无法再次获取弱引用,也就是说弱引用对象在进行GC时是直接被清除的,其实,弱引用和软引用一样,都可以通过利用ReferenceQueue来初始化弱引用对象,构造一个队列,能够有效的跟踪哪个弱引用被回收。软引用和弱引用都非常适合来保存那些可有可无的缓存数据,如果这么做当系统的内存不足时,这些缓存会被回收,不会导致内存溢出,而内存资源充足时,又可以存在相当长的时间,从而起到系统加速的作用。

4.虚引用

虚引用是所有引用类型中最弱的一个,一个持有虚引用的对象和没有引用几乎是一样的,随时都可能被垃圾回收器回收,当试图通过get方法获取强引用时,总是会失败,并且虚引用必须和引用队列一起使用,他的作用在于跟踪垃圾回收过程,当垃圾回收器准备回收一个对象时,如果发现它有虚引用,就会在回收对象之后将这个虚引用加入到引用对列,通知应用程序对象的回收情况。先给个例子,我们体会一下:
public class TraceCanReliveObj {
	public static TraceCanReliveObj obj;
	static ReferenceQueue <TraceCanReliveObj> phantomQueue = null;
	public static class CheckRefQueue extends Thread{
		@Override
		public void run(){
			while(true){
				if(phantomQueue != null){
					PhantomReference <TraceCanReliveObj> objt = null;
					try{
						objt = (PhantomReference<TraceCanReliveObj>) phantomQueue.remove();
					}catch(InterruptedException e){
						e.printStackTrace();
					}
					if (objt != null){
						System.out.println("TraceCanReliveObj is delete by GC");
					}
				}
			}
		}
	}
	@Override
	protected void finalize() throws Throwable{
		super.finalize();
		obj = this;
	}
	
	@Override
	public String toString(){
		return "I am a CanReliveObj";
	}
	
	public static void main(String[] args) throws InterruptedException{
		Thread t =new CheckRefQueue();
		t.setDaemon(true);
		t.start();
		
		
		phantomQueue = new ReferenceQueue<TraceCanReliveObj>();
		obj = new TraceCanReliveObj();
		PhantomReference<TraceCanReliveObj> phantomRef = new PhantomReference<TraceCanReliveObj>(obj, phantomQueue);
		
		obj = null;
		System.gc();
		Thread.sleep(1000);
		if(obj == null){
			System.out.println("Obj 是 null");
		}else{
			System.out.println("obj 可用");
		}
		
		System.out.println("第二次GC");
		obj = null;
		System.gc();
		Thread.sleep(1000);
		if(obj == null){
			System.out.println("Obj 是 null");
		}else{
			System.out.println("obj 可用");
		}
	}
	
}
代码中TraceCanReliveObj对象是一个在finalize方法中可以复活的对象,代码中构造了这个对象的虚引用,并且指定了对应的引用队列,然后将强引用去除,第一次GC 和之前看的例子一样,这个对象会复活,所以GC无法回收这个对象,接着,代码中再次进行GC,由于finalize方法只会调用一次,第二次会被回收,同时,其引用队列应该也会捕获到对象的回收,输出结果如下:
     由于虚引用可以跟踪对象回收的时间,所以也可以将一些资源释放操作放置在虚引用中执行和记录。


猜你喜欢

转载自blog.csdn.net/qq_18870127/article/details/79902076
今日推荐