Java中的四种引用类型比较
1.引用的概念
引用这个概念是与JAVA虚拟机的垃圾回收有关的,不同的引用类型对应不同的垃圾回收策略或时机。
垃圾收集可能是大家感到难于理解的较难的概念之一,因为它并不能总是毫无遗漏地解决Java运行时环境中堆管理的问题。
垃圾回收的大致思路是:当Java虚拟机觉得内存不够用的时候,会触发垃圾回收操作(GC),清除无用的对象,释放内存。可是如何判断一个对象是否是垃圾呢?其中的一个方法是计算指向该对象的引用数量,如果引用数量为0,那么该对象就为垃圾(Thread对象是例外),否则还有用处,不能被回收。但这种引用计数算法不能解决循环引用的问题,现在JAVA虚拟机使用的是可达性分析算法。常见的情况是:一个对象并不是从根部直接引用的,而是一个对象被其他对象引用,甚至同时被几个对象所引用,从而构成一个以根集为顶的树形结构,当某个对象不可到达(即这个对象不能通过引用获得这个对象时),这个对象及被这个对象所引用的其他对象都将视作垃圾,此内存接下来很可能被JVM所回收。
2.四种引用类型
Java中提供了4个级别的引用,即强引用(FinalReference)、软引用
(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)这四个级别。在这4个级别中只有强引用类是包内可见的,其他3种引用类型均为public,可以在应用程序中直接使用,垃圾回收器会尝试回收只有弱引用的对象。
■ 强引用(Strong Reference):在一个线程内,无须引用直接可以使用的对象,强引用不会被JVM清理。我们平时申明变量使用的就是强引用,普通系统99%以上都是强引 用,比如,String s="Hello World"。
■ 软引用(WeakReference):通过一个软引用申明,JVM抛出OOM之前,清理所有的软引用对象。垃圾回收器某个时刻决定回收软可达的对象的时候,会清理软引用,并可选的把引用存放到一个引用队列(ReferenceQueue)。
■ 弱引用(SoftReference):通过一个弱引用申明。类似弱引用,只不过 Java 虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。
■ 虚引用(PhantomReference):通过一个虚引用申明。仅用来处理资源的清理问题,比Object里面的finalize机制更灵活。get方法返回的永远是null,Java虚拟机不负责清理虚引用,但是它会把虚引用放到引用队列里面。
3.强引用(Strong Reference)
Java中的引用,有点像C++的指针。通过引用,可以对堆中的对象进行操作。在Java 程序中,最常见的引用类型是强引用,它也是默认的引用类型。
当在Java语言中使用new 操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。而判断一个对象是否存活的标准为是否存在指向这个对象的引用。
在某函数中,例如:StringBuffer str=new StringBuffer(“Hello World”);假设该代码是在函数体内运行的,那么局部变量str将被分配在栈中,而对象StringBuffer实例,被分配在堆上。局部变量str指向StringBuffer实例所在堆空间,通过str可以操作该实例,那么str就是StringBuffer的引用。此时,如果运行一个赋值语句:StringBuffer str1=str;那么,str所指向的对象也将被str1所指向,同时在局部栈空间上会分配空间存放str1变量。此时““Hello World”这个字符串对象就有了两引用,两个引用都同时指向这一个对象。而对引用的“==”操作用也只是表示两个操作数所指向的堆空间地址是否相同,即判断两个引用是否指向同一个对象。
强引用具备以下特点:
(1)强引用可以直接访问目标对象。
(2)强引用所指向的对象在任何时候都不会被系统回收。JVM宁愿抛出Out Of Memory异常也不会回收强引用所指向的对象。
(3)强引用可能导致内存泄漏。
强引用的存在会阻止一个对象被回收。在垃圾回收器遍历对象引用并进行标记之后,如果一个对象是强引用可达的,那么这个对象不会作为垃圾回收的候选。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。程序员编写代码的时候,有时为了防止内存泄漏,需要将引用显式地赋空(Object o=null)。例如:在ArrayList的remove(int index)中,将最后一个元素赋为空。
"public E remove(int index)"方法的代码片断
elementData[--size] = null; // clear to let GC do its work
在实现一个缓存系统的时候,如果全部使用强引用,那么你需要自己去手动的把某些引用Clear掉(引用置为Null),否则迟早会抛出Out Of Memory错误。缓存系统引入弱引用或者软引用的唯一原因是,把引用Clear的事情交由Java垃圾回收器来处理,Cache程序自己置身事外。
通常来说,应用程序内部的内存泄露有两种情况。一种是虚拟机中存在程序无法使用的内存区域,另一种情况是程序中存在大量存活时间过长的对象。
4. 软引用(WeakReference)
软引用是除了强引用外最强的引用类型,我们可以通过java.lang.ref.SoftReference使用软引用。一个持有软引用的对象,它不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收。当堆使用率临近阙值时,才会去回收软引用的对象。只要有足够的内 存,软引用便可能在内存中存活相当长一段时间。因此,就如上一节所说的,软引用可以用于实现对内存敏感的Cache。垃圾回收器会保证在抛出OOM错误之前,回收掉所有软引用可达的对象。通过软引用,垃圾回收器就可以在内存不足时释放软引用可达的对象所占的内存空间。程序所要做的是保证软引用可达的对象被垃圾回收器回收之后,程序也能正常工作。
软引用的特点是它的一个实例保存对一个Java对象的软引用,该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。也就是说,一旦软引用保存了对一个Java对象的软引用后,在垃圾线程对这个Java对象回收前,软引用类所提供的get()方法返回Java对象的强引用。另外,一旦垃圾线程回收该Java对象之后,get()方法将返回null。
示例代码:
import org.junit.Assert; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; public class MyObject { public static ReferenceQueue<MyObject> softQueue; @Override protected void finalize() throws Throwable { super.finalize(); Reference<MyObject> obj = null; Assert.assertNotNull(softQueue); obj = (Reference<MyObject>) softQueue.remove(); System.out.println(obj); if (obj != null) System.out.println("Object for SoftReference is" + obj.get()); System.out.println("MyObject finalize called ");// } @Override public String toString() { return "I am MyObject"; } public static void main(String[] args) { MyObject obj = new MyObject(); softQueue = new ReferenceQueue<>(); SoftReference<MyObject> softRef = new SoftReference<>(obj, softQueue); System.out.println(obj); obj = null; System.gc(); System.out.println(obj); System.out.println("After GC:Soft get=" + softRef.get()); System.out.println("分配大块内存"); byte[] bytes = new byte[1000 * 1024 * 925];//分配一块大内存,强制GC System.out.println("After new byte[]:soft Get=" + softRef.get()); System.out.println(obj); } }
作为一个Java对象,软引用对象除了具有保存软引用的特殊性之外,也具有Java对象的一般性。所以,当软引用对象被回收之后,虽然这个软引用对象的get()方法返回null,但这个对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量软引用对象带来的内存泄漏。在java.lang.ref包里还提供了ReferenceQueue。如果在创建软引用对象的时候,使用了一个ReferenceQueue对象作为参数提供给软引用的构造方法。
ReferenceQueue softQueue = new ReferenceQueue<>(); SoftReference<MyObject> softRef = new SoftReference<>(obj, softQueue);
那么当这个SoftReference所持有的软引用的obj被垃圾收集器回收的同时,softRef所强引用的软引用对象被列入ReferenceQueue。也就是说,ReferenceQueue中保存的对象是Reference对象,而且是已经失去了它所软引用的对象的Reference对象.
ReferenceQueue是一个队列,这具有一般队列的基本特性,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的非强可及对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。利用这个方法,我们可以检查哪个SoftReference所软引用的对象已经被回收。于是我们可以把这些失去所软引用的对象的SoftReference对象清除掉。常用的方式如SoftReference ref=null;while ((ref=(EmployeeRef) q.poll())!=null) {//清除ref}。
5.弱引用(SoftReference)
1)弱引用是一种比软引用较弱的引用类型(不管系统堆空间是否足够,都会将对象进行回收),使用弱引用后,可以维持对referent的引用,而不会阻止它被垃圾收集。当垃圾收集器跟踪堆的时候,如果对一个对象的引用只有弱引用,那么这个referent就会成为垃圾收集的候选对象,就像没有任何剩余的引用一样,而且所有剩余的弱引用都被清除。弱引用是在构造时设置的,在没有被清除之前,可以获取它的 值,如果弱引用被清除了,无论是被垃圾收集了,还是有人调用了clear()方法,它都会返回null。因此,在使用弱引用对象之前,都需要检查是否返回一个非null值。
2)由于垃圾回收器的线程通常优先级很低,因此并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。一旦一个弱引用对象被垃圾回收器回收,便会加入到一个注册引用队列中。上述例子只需要更改对象类型为weakReference即可,会看到一旦obj被设置为null,GC立即回收若类型实例。
弱引用、软引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。
3)弱引用的重要作用是解决对象的存活时间过长的问题。在程序中,一个对象的实际存活时间应该与它的逻辑存活时间一样。从逻辑上来说,一个对象应该在某个方法调用完成之后就不再被需要了,可以对其进行垃圾回收。但是,如果仍然有其他的强引用存在,该对象的实际存活时间会长于逻辑存活时间,直到其他的强引用不再存在。这样的对象在程序中过多出现会导致虚拟机的内存占用上升,最后产生OOM错误。要解决这样的问题, 需要小心注意管理对象上的强引用。当不再需要引用一个对象时,显式地清除这些强引 用。不过这会对开发人员提出更高的要求。更好的方法是使用弱引用替换强引用来引用这些对象。这样既可以引用对象,又可以避免强引用带来的问题。
4)Map的子类WeakHashMap就是应用弱引用的典型应用,它可以作为简单的缓存表解决方案。WeakHashMap在java.util包内, 它实现了Map接口,是HashMap的一种实现,它使用弱引用作为内部数据的存储方案。
如果在系统中需要一张很大的Map表,Map中的表项作为缓存之用,这也意味着即使没有能从该Map中取得相应的数据,系统也可以通过候选方案获取这些数据。虽然这样会消耗更多的时间,但是不影响系统的正常运行。在这种场景下,使用WeakHashMap是最为适合的。因为WeakHashMap会在系统内存范围内,保存所有表项,而一旦内存不够, 在GC时没有被引用的表项也会很快被清除掉,从而避免系统内存溢出。
@Test public void mapTest() { Map<Integer, byte[]> map1 = new WeakHashMap<>(); long startTime = System.currentTimeMillis(); List list = new ArrayList(); for (int i = 0; i < 100000; i++) { Integer iWrap = Integer.valueOf(i); map1.put(iWrap, new byte[i]); } System.out.println("HashMap放入元素所耗的时间" + (System.currentTimeMillis() - startTime) / 1000 + "秒"); }
上述代码会报出异常,堆空间错误,内存溢出了;
但如果将“ Map<Integer, byte[]> map1 = new HashMap<>();”改成“ Map<Integer, byte[]> map1 = new WeakHashMap<>();”,则不会出现内存溢出的问题。
另外如果在使用WeakHashMap的同时,还在循环体中加入list.add(iWrap),这仍然会出现内存溢出的问题。因为“list.add(iWrap);”这行代码对key进行了强引用,将导致WeakHashMap不会自动启动实例回收机制。所以,如果在系统中通过WeakHashMap自动清理数据,就尽量不要在系统的其他地方强引用WeakHashMap的key,否则这些key就不会被回收,WeakHashMap也就无法正常释放它们所占用的内存。
5)弱引用与软引用的区别/差异
总的来说,弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收。虽然,GC在运行时一定回收Weak对象,但是复杂关系的弱对象群常常需要好几次GC的运行才能完成。就像上面描述的场景,弱引用对象常常用于Map结构中,引用数据量较大的对象,一旦该对象的强引用为null时,GC能够快速地回收该对象空间。
6.虚引用(PhantomReference)
1)虚引用,又称幽灵引用,是强度最弱的一种引用类型,用java.lang.ref.PhantomReference类来表示。虚引用的主要目的是在一个对象所占的内存被实际回收之前得到通知,从而可以进行一些相关的清理工作。幽灵引用在使用方式上与之前介绍的两种引用类型有很大的不同:首先虚引用在创建时必须提供一个引用队列作为参数;其次虚引用对象的get方法总是返回null,因此无法通过虚引用来获取被引用的对象。
2)虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
3)虚引用与软引用/弱引用的区别
软引用和弱引用在其可达状态达到时就可能被添加到对应的引用队列 中。也就是说,当一个对象变成软引用可达或弱引用可达的时候,指向这个对象的引用对象就可能被添加到引用队列中。在添加到队列之前,垃圾回收器会清除掉这个引用对象的引用关系。当软引用和弱引用进入队列之后,对象的finalize方法可能还没有被调用。在finalize方法执行之后,该对象有可能重新回到可达状态。如果该对象回到了可达状态,而指向该对象的软引用或弱引用对象的引用关系已经被清除,那么就无法再通过引用对象来查找这个对象。而幽灵引用则不同,只有在对象的finalize方法被运行之后,幽灵引用才会被添加到队列中。与软引用和弱引用不同的是,幽灵引用在被添加到队列之前,垃圾回收器不会自动清除其引用关系,需要调用clear方法来显式地清除。当幽灵引用被清除之后, 对象就进入了不可达状态,垃圾回收器可以回收其内存。当幽灵引用被添加到队列之后,由于PhantomReference类的get方法总是返回null,程序也不能对幽灵引用所指向的对象进行任何操作。
import java.lang.ref.PhantomReference; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; public class RefQueue { static class RefObj { @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize方法被调用"); } } public static void phantomReferenceQueue() { ReferenceQueue<RefObj> queue = new ReferenceQueue<>(); RefObj obj = new RefObj(); PhantomReference<RefObj> phantomRef = new PhantomReference<>(obj, queue); obj = null; Reference<? extends RefObj> ref = null; while ((ref = queue.poll()) == null) { System.gc(); } phantomRef.clear(); System.out.println(ref == phantomRef); System.out.println("幽灵引用被清除"); } public static void main(String[] args) { phantomReferenceQueue(); } }
控制台打印