在Java语言中,除了原始数据类型(int/float等基本数据类型)的变量,其他所有的引用类型,都是指向到内存中不同的对象(一般为new操作符创建的对象)。而为了更好的管理内存,java在1.2版本中增加了包java.lang.ref,这个包提供了三个引用对象的实现类:SoftReference、WeakReference、PhantomReference。即软引用、弱引用、虚引用。此外对于直接用new操作符创建的对象,如果该对象使用一个变量来引用,那么这种引用类型称为强引用。本篇文章将从对象的生命周期、GC以及Object#finalize()方法说起,认识一下这三种引用对象的作用和应用场景。
为了说明强引用、软引用、弱引用、虚引用,我们有必要来了解一下对象的生命周期和GC的基本原理。下面就以一个不十分详细的说明来描述一下。(对于想深入了解的可以参见书籍《深入理解Java虚拟书 》这本书)
假设我们有一段代码
public static void foo(String bar) { Integer baz = new Integer(bar); } // 调用函数 foo("123")
传入参数bar给方法foo(String bar),该方法内部创建了一个Integer的对象。
下图描述了在java内存空间中,栈和堆的关系。可以看出,bar作为方法foo的入参,其栈内存放了一个变量bar,指向堆中的地址,字符串”123”在堆内存中的地址。在foo函数的执行栈中,baz变量同样在栈中存放了指向堆内存,即Integer实例的内存地址。
重点:在foo函数执行完毕后,弹出执行栈,其内部的baz、bar变量立即销毁,此时堆中的”123”、Integer(“123”)对象将没有任何引用,当JVM内存不足,触发GC时,这些内存空间将被回收。
梳理一下,对于一般情况(也可以理解为强引用的情况下),堆内存中的对象,当没有变量引用时,在GC周期它们所属的内存将被回收。但在软引用、弱引用的作用下,就算堆内存中的对象被引用类型的实例(即SoftReference或WeakReference)引用,仍然可以触发回收动作。
引用官方的说法 ,在GC对实例进行可达性分析时,主要有四种可达性:
四种引用对象提供了一种有限的能力用于跟GC打交道,一个程序可以使用一个引用对象来维护某个其他对象的引用,使得后者对象仍然可以在某种情况下被收集器来回收。这样讲,可能有点不明确,请看图和描述。
在引用对象(reference-object)中使用了一个实例变量引用了一个对象(referents),在这种情况下就算引用对象(reference-object)仍然处于作用域中,被引用对象仍然有可能被GC回收。各自的作用如官网所说:
软引用,在JVM内存不足时,将回收软引用对象中被引用的对象。
应用场景:处理超大数据集时,避免OOM
public static List<List<Object>> processResults(ResultSet rslt) throws SQLException { try { // 创建一个软引用对象,其中引用了一个LinkedList实例,用于保存处理结果 SoftReference<List<List<Object>>> ref = new SoftReference<List<List<Object>>>(new LinkedList<List<Object>>()); ResultSetMetaData meta = rslt.getMetaData(); int colCount = meta.getColumnCount(); int rowCount=0; while (rslt.next()){ rowCount++; // store the row data List<List<Object>> results = ref.get(); if (results == null) throw new TooManyResultsException(rowCount); else results.add(row); // 取消强引用,这样才能保证在内存不足时可以进行软引用的收集动作 results = null; } return results; } finally { closeQuietly(rslt); } }
弱引用,当一个对象只存在弱引用时,在GC时将被回收
应用场景:
// 固定关系的一端 static class Person { // 当发生GC时,回收实例将调用该方法 @Override protected void finalize() throws Throwable { System.out.println("Finalize"); super.finalize(); } }
private static void test1() throws InterruptedException { // 使用WeakHashMap来绑定两个对象的关系 Map<Person, String> personIds = new WeakHashMap<>(); Person person = new Person(); // WeakHashMap会将key包装为一个WeakReference对象 personIds.put(person, "1"); String s = personIds.get(person); System.out.println(s); // 去掉对实例的强引用之后,当前实例只存在弱引用,即WeakHashMap中对应的key person = null; // 手动触发GC,将会回收person实例 // 如果使用HashMap来保存关系的话,为了避免内存泄漏,我们需要手动HashMap#remove(person) // 来删除引用,让GC回收person。使用弱引用后,将无需我们来管理这一部分内存管理 System.gc(); TimeUnit.SECONDS.sleep(10); }
// 模拟String类的intern(String string)方法,来保证不会有重复对象 private Map<String,WeakReference<String>> _map = new WeakHashMap<String,WeakReference<String>>(); public synchronized String intern(String str) { WeakReference<String> ref = _map.get(str); String s2 = (ref != null) ? ref.get() : null; if (s2 != null) return s2; _map.put(str, new WeakReference(str)); return str; }
虚引用(PhantomReference)不同于软引用、弱引用,其无法获得被引用对象,但是可以在引用对象被回收时,通过ReferenceQueue将自身(引用对象本身)来通知应用程序,进行一些回收资源的操作。
收到回收资源的操作,Java中有一个类似于C++的析构函数(Object#finalize)。如果在类中覆盖该方法,那么在GC回收该类的实例的时候,将触发调用该方法(GC通过一个线程来调用,时间复杂度为O(n)级别),在执行完这个方法之后,才会进行内存资源的回收。因此存在一个弊端,如果很多类都实现了finalize方法,那么会导致内存资源回收失败,发生OOM异常。
四种引用:强引用、软引用、弱引用、虚引用
2. 软引用、弱引用、虚引用在Java中各自的实现类,以及作用
熟悉这四种引用的含义和作用,将可以帮助我们更好的了解和使用JVM内存,避免内存泄漏导致的OOM异常
Java Reference Objects
官方文档
WeakReference