在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