Java的内存回收——Java引用的种类

1、Java引用的种类

       Java是面向对象的编程语言,一个Java程序往往需要创建大量的Java类,然后对各Java类创建大量的Java对象,再调用这些Java对象的属性和方法来操作它们。
       程序员需要通过关键字new创建Java对象,既可以视作为Java对象申请内存空间,JVM会在堆内存中为每个对象分配空间;当一个Java对象失去引用时,JVM的垃圾回收机制会自动清除它们,并回收它们所占用的空间。
       Java内存管理包括内存分配(创建Java对象时)和内存回收两个方面(回收Java对象时)。这两方面的工作都是由JVM自动完成的,因此降低了Java程序员的学习难度,以至于使容易忽视内存分配的问题。但这两方面的工作也加重了JVM的工作,从而使Java程序运行较慢。

1、1 对象在内存中的状态

       对于JVM的垃圾回收机制来说,是否回收一个对象的标准在于:是否还有引用变量引用该对象?只要有引用变量引用该变量,垃圾回收机制就不会回收它。也就是说,当Java对象被创建出来之后,垃圾回收机制会实时地监控每个对象的运行状态,包括对象的申请、引用、被引用、赋值等。当垃圾回收机制实时地监控到某个对象不再被引用变量所引用时,垃圾回收机制就会回收它所占用的空间。
       基本上,可以把JVM内存中的对象引用理解成一种有向图,把引用变量、对象都当成有向图的顶点,将引用关系当成图的有向边,有向边总是从引用端指向被引用的Java对象。因为Java的所有对象都是由一条条线程创建出来的,因此可以把线程对象当成有向图的起始顶点。
       对於单线程程序而言,整个程序只有一条main线程,那么该图就是以main进程为顶点的有向图。在这个有向图中,main顶点可达的对象都处于可达状态,垃圾回收机制不会回收它们;如果某个对象在这个有向图中处于不可达状态,那么就认为这个对象不再被引用,接下来垃圾回收机制就会主动回收它了。
class Node {
	Node next;
	String name;
	
	public Node(String name) {
		this.name = name;
	}
}

public class NodeTest {
	public static void main(String[] args) {
		Node n1 = new Node("一号节点");
		Node n2 = new Node("二号节点");
		Node n3 = new Node("三号节点");
		n1.next = n2;
		n3 = n2;
		n2 = null;
	}
}
       上面程序中定义了三个Node对象,并通过合适的引用关系把这三个Node对象组织在一起,下图为JVM中对应的有向图。

       从main顶点开始,有一条路径到达“第一个Node对象”,因此该对象处于可达状态,垃圾回收机制不会回收它;从main顶点开始,有两条路径到达“第二个Node对象”,因此该对象也处于可达状态,垃圾回收机制不会回收它;从main顶点开始,没有路径可以到达“第三个Node对象”,因此这个Java对象就变成了垃圾,接下来垃圾回收机制就会回收它。

       当一个对象在堆内存中运行时,根据它在对应有向图中的状态,可以把它所处的状态分成如下三种。
  • 可达状态:当一个对象被创建后,有一个以上的引用变量引用它。在有向图可以从起始顶点导航到该对象,那么它就处于可达状态,程序可以通过引用变量来调用该对象的属性和方法。
  • 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它将先进入可恢复状态,此时从有向图的起始顶点不能导航到该对象。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存。如果系统调用finalize方法重新让一个以上的引用变量引用该对象,则这个对象会再次变为可达状态;否则,该对象将进入不可达状态。
  • 不可达状态:当对象的所有关联都被切断,且系统调用所有对象的finalize方法依然没有使该对象变成可达状态后,这个对象将永久性地失去引用,最后变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。
public class StatusTranfer {
	public static void test() {
		String a = new String("Java对象1");  //①
		a = new String("Java对象2");  //②
	}
	public static void main(String[] args) {
		test();
	}
}
       当程序执行test方法的①行代码时,代码定义了a变量,并让该变量指向“Java对象1”字符串。该代码执行结束后,“Java对象1”字符串对象处于可达状态。
       当程序执行test方法的②行代码后,代码再次定义了“Java对象2”字符串对象,并让a变量指向该对象。此时,“Java对象1”字符串对象处于可恢复状态,而“Java对象2”字符串对象处于可达状态。
       一个对象可以被一个方法的局部变量引用,也可以被其他类的类变量引用,或者被其他对象的实例变量引用。当某个对象被其他类的类变量引用时,只有该类被销毁后,该对象才会进入可恢复状态;当某个对象被其他对象的实例变量引用时,只有当引用该对象的对象被销毁或变成不可达状态后,该对象才会进入不可达状态。
       对垃圾回收机制来说,判断一个对象是否可回收的标准在于该对象是否被引用。因此引用也是JVM进行内存管理的一个重要概念。为了更好地管理对象的引用,从JDK1.2开始,Java在java.lang.ref包下提供了三个类:SoftReference、PhantomReference和WeakReference,分别代表了对对象的三种引用方法:软引用、虚引用和弱引用。其实,在Java中还有强引用,总共四种对对象的引用。

1、2 强引用

       这是Java程序中最常见的引用方式,程序创建一个对象,并把这个对象赋给一个引用变量,这个引用变量就是强变量。当一个对象被一个或一个以上的强引用变量所引用时,它处于可达状态,它不可能被系统垃圾回收机制回收。强引用时Java编程中广泛使用的引用类型,被强引用所引用的Java对象绝不会被垃圾回收机制回收,即使内存非常紧张;即使有些Java对象以后永远也不会被用到,JVM也不会回收强引用所引用的Java对象。

1、3 软引用

        软引用需要通过SoftReference来实现,当一个对象只具有软引用时,它可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象;当系统内存空间不足时,系统将会回收它。
       软引用通常用于内存敏感的程序中,软引用是强引用很好的替代。当系统内存空间充足时,软引用和强引用没有太大的区别;当系统内存空间不足时,被软引用所引用的Java对象可以被垃圾回收机制回收,从而避免系统内存不足的异常。

1、4 弱引用

       弱引用和软引用有点类似,区别在于弱引用所引用对象的生存期更短。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。但是并不是当一个对象只有弱引用时,它就会立即被回收,而是等到系统垃圾回收机制运行时才会被回收。
import java.lang.ref.WeakReference;

public class WeakReferenceTest {
	public static void main(String[] args) {
		String str = new String("Java对象");
		WeakReference<String> wr = new WeakReference<String>(str);
		str = null;  //切断str引用和"Java对象"字符之间的引用
		System.out.println(wr.get());   //①
		System.gc();  //强制垃圾回收
		System.runFinalization();
		System.out.println(wr.get());   //②
	}
}
输出结果为:
Java对象
null
       当执行str = null;之后,切断了str和“Java对象”字符串对象之间的引用关系,此时只有一个弱引用对象引用字符串对象,程序中①行代码依然可以输出“Java对象”。如果系统垃圾回收机制启动,只有弱引用的对象就会被清理掉。程序中②行代码输出null,这就表明对象已经被清理了。
注意:上面创建“Java对象”字符串对象时,不要使用String str = "Java对象";代码,因为系统不会缓存这个字符串常量(会使用强引用来引用它),系统则不会回收被缓存的字符串常量。
       弱引用具有很大的不确定性,因此每次垃圾回收机制执行时都会回收弱引用所引用的对象,而垃圾回收机制的运行又不受程序员的控制,因此程序获取弱引用所引用的Java对象时必须小心空指针异常。

1、5 虚引用

        软引用和弱引用可以单独使用,但虚引用不能单独使用,必须和引用队列联合使用。单独使用虚引用没有太大的意义。虚引用的主要作用就是跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否已经包含指定的虚引用,从而了解虚引用所引用的对象是否即将被回收
       引用队列由java.lang.ref.ReferenceQueue类表示,它用于保存被回收后对象的引用。当把软引用、弱引用和引用队列联合使用时,系统回收被引用的对象之后,将会把被回收对象对应的引用添加到关联的引用队列中。与软引用和弱引用不同的是,虚引用在对象被释放之前,将把它对应的虚引用添加到它关联的引用队列中,这使得可以在对象被回首之前采取行动。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class PhantomReferenceTest {
	public static void main(String[] args) throws Exception {
		String str = new String("Java对象");
		ReferenceQueue<String> rq = new ReferenceQueue<>();  //创建引用队列
		PhantomReference<String> pr = new PhantomReference<String>(str, rq);  //创建虚引用
		str = null;
		System.out.println(pr.get());   //①
		System.gc();  //强制垃圾回收
		System.runFinalization();
		System.out.println(rq.poll() == pr);  //取出引用队列中最先进入队列中的引用于pr进行比较 ②
	}
}
输出结果为:
null
true
       因此系统无法通过虚引用来获得被引用的对象,所以执行①出输出null。当程序强制垃圾回收后,只有虚引用引用的字符串对象会被垃圾回收,当被引用的对象被回收后,对应的引用会被添加到关联的引用队列中,因此②执行后会输出true。

总结:使用这些引用类可以避免在程序执行期间将对象留在内存中。如果以软引用、弱引用或虚引用的方法引用对象,垃圾回收器就能够随意地释放对象。如果希望尽可能减小程序在其生命周期中所占用的内存大小,这些引用类就很有好处。最后需要指出的是,要使用这些特殊的引用类,就不能保留对对象的强引用。如果保留了对对象的强引用,就会浪费这些类所提供的任何好处。




发布了46 篇原创文章 · 获赞 44 · 访问量 2万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章