众所周知,Java中存在4中引用类型。
强引用(Strong reference) > 软引用(Soft reference) > 弱引用(Weak reference) > 虚引用(Phontom reference)
那么他们的含义是什么呢?应用场景有哪些?他们之间有什么区别?这篇文章将说明这些。
4种引用类型的含义
1、强引用
强引用不会被GC回收,并且在java.lang.ref里也没有实际的对应类型,平时工作接触的最多的就是强引用。Object obj = new Object();
这里的obj引用便是一个强引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
2、软引用
如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只 要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
3、弱引用
如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回 收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
4、虚引用
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。由于Object.finalize()方法的不安全性、低效性,常常使用虚引用完成对象回收前的资源释放工作。
当JVM将虚引用插入到引用队列的时候,虚引用执行的对象内存还是存在的。但是PhantomReference并没有暴露API返回对象。如NIO直接内存的自动回收,就使用到了sun.misc.Cleaner
4种引用类型的使用
1、强引用
package ref;
/**
* Created by benjamin吴海旭 on 16/9/9.
*/
public class ClassStrong {
public static class Referred {
@Override
protected void finalize() throws Throwable {
System.out.println("Referred对象被垃圾收集");
}
}
public static void collect() throws InterruptedException {
System.out.println("开始垃圾收集...");
System.gc();
System.out.println("结束垃圾收集...");
Thread.sleep(2000);
}
/**
* 执行结果
* 创建一个强引用--->
* 开始垃圾收集...
* 结束垃圾收集...
* 删除引用--->
* 开始垃圾收集...
* 结束垃圾收集...
* Referred对象被垃圾收集
* Done
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
System.out.println("创建一个强引用--->");
// 这是一个强引用
// 如果没有引用这个对象就会被垃圾收集
Referred strong = new Referred();
// 开始垃圾收集
ClassStrong.collect();
System.out.println("删除引用--->");
// 这个对象将要被垃圾收集
strong = null;
ClassStrong.collect();
System.out.println("Done");
}
}
这个例子说明了强引用时候GC不会回收对象,只有这个对象没有强引用了,GC才会进行回收并调用finalize方法。
2、软引用
package ref;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
/**
* Created by benjamin吴海旭 on 16/9/9.
*/
public class ClassSoft {
public static class Referred {
@Override
protected void finalize() throws Throwable {
System.out.println("Referred对象被垃圾收集");
}
}
public static void collect() throws InterruptedException {
System.out.println("开始垃圾收集...");
System.gc();
System.out.println("结束垃圾收集...");
Thread.sleep(2000);
}
/**
* 执行结果
* 创建一个软引用--->
* 开始垃圾收集...
* 结束垃圾收集...
* 删除引用--->
* 开始垃圾收集...
* 结束垃圾收集...
* 开始堆占用
* Referred对象被垃圾收集
* 内存溢出...
* Done
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
System.out.println("创建一个软引用--->");
Referred strong = new Referred();
SoftReference soft = new SoftReference(strong);
ClassSoft.collect();
System.out.println("删除引用--->");
strong = null;
ClassSoft.collect();
System.out.println("开始堆占用");
try {
List heap = new ArrayList<>(100000);
while (true) {
heap.add(new ClassSoft());
}
} catch (OutOfMemoryError e) {
// 软引用对象应该在这个之前被收集
System.out.println("内存溢出...");
}
System.out.println("Done");
}
}
这个例子说明了当内存足够的时候,软引用所指向的对象没有其他强引用指向的话,GC的时候并不会被回收,当且只当内存不够时(即将要OOM的时候)才会被GC回收(调用finalize方法)。强度仅次于强引用。
3、弱引用
package ref;
import java.lang.ref.WeakReference;
/**
* Created by benjamin吴海旭 on 16/9/9.
*/
public class ClassWeak {
public static class Referred {
@Override
protected void finalize() throws Throwable {
System.out.println("Referred对象被垃圾收集");
}
}
public static void collect() throws InterruptedException {
System.out.println("开始垃圾收集...");
System.gc();
System.out.println("结束垃圾收集...");
Thread.sleep(2000);
}
/**
* 执行结果
* 创建一个弱引用--->
* 开始垃圾收集...
* 结束垃圾收集...
* 删除引用--->
* 开始垃圾收集...
* 结束垃圾收集...
* Referred对象被垃圾收集
* Done
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
System.out.println("创建一个弱引用--->");
Referred strong = new Referred();
WeakReference weak = new WeakReference<>(strong);
ClassWeak.collect();
System.out.println("删除引用--->");
strong = null;
ClassWeak.collect();
System.out.println("Done");
}
}
这个例子说明了弱引用指向的对象没有任何强引用指向的话,GC的时候会进行回收。
4、虚引用
package ref;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.HashMap;
import java.util.Map;
/**
* Created by benjamin吴海旭 on 16/9/9.
*/
public class ClassPhantom {
public static class Referred {
// Note! 如果这里重写了finalize方法,那么PhantomReference不会追加到ReferenceQueue中
// @Override
// protected void finalize() throws Throwable {
// System.out.println("Referred对象被垃圾收集");
// }
}
public static void collect() throws InterruptedException {
System.out.println("开始垃圾收集...");
System.gc();
System.out.println("结束垃圾收集...");
Thread.sleep(2000);
}
/**
* 执行结果
* 创建一个虚引用--->
* 开始垃圾收集...
* 结束垃圾收集...
* 你需要清理一些东西了
* Done
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
System.out.println("创建一个虚引用--->");
ReferenceQueue dead = new ReferenceQueue();
Map cleanUpMap = new HashMap<>();
Referred strong = new Referred();
PhantomReference phantom = new PhantomReference<>(strong, dead);
cleanUpMap.put(phantom, "你需要清理一些东西了");
strong = null;
ClassPhantom.collect();
Reference reference = dead.poll();
if (reference != null) {
System.out.println(cleanUpMap.remove(reference));
} else {
System.out.println("reference为空");
}
System.out.println("Done");
}
}
虚引用不像其他引用可以只传一个对象,它还需要一个referenceQueue,来进行GC回收时把引用放入队列中。因为它的get()方法始终返回null。大家可能发现这个例子的finalize方法被注掉了,因为如果重写了这个方法后,GC的时候不会把虚引用放入referenceQueue中,具体的原因详见这篇博文:Java避免使用finalizer方法,在strong=null后面使用两次GC回收就可以放入ReferenceQueue了。
小结:软引用和弱引用差别不大,JVM都是先把SoftReference和WeakReference中的referent字段值设置成null,之后加入到引用队列;而虚引用则不同,如果某个堆中的对象,只有虚引用,那么JVM会将PhantomReference加入到引用队列中,JVM不会自动将referent字段值设置成null
Phantom和Weak Soft引用的差别
上面的小结说的不够清晰,下面给出实际的例子来进行说明。
软引用和弱引用差别不大,JVM都是先将其referent字段设置成null,之后将软引用或弱引用,加入到关联的引用队列中。我们可以认为JVM先回收堆对象占用的内存,然后才将软引用或弱引用加入到引用队列。
而虚引用则不同,JVM不会自动将虚引用的referent字段设置成null,而是先保留堆对象的内存空间,直接将PhantomReference加入到关联的引用队列,也就是说如果我们不手动调用PhantomReference.clear(),虚引用指向的堆对象内存是不会被释放的。
referent是java.lang.ref.Reference类的私有字段,虽然没有暴露出共有API来访问这个字段,但是我们可以通过反射拿到这个字段的值,这样就能知道引用被加入到引用队列的时候,referent到底是不是null。SoftReference和WeakReference是一样的,这里我们以WeakReference为例。
package other;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
/**
* Created by benjamin吴海旭 on 16/9/8.
*/
public class TestWeakReference {
public static volatile boolean isRun = true;
public static void main(String[] args) throws InterruptedException {
String abc = new String("abc");
System.out.println(abc.getClass() + "@" + abc.hashCode());
final ReferenceQueue referenceQueue = new ReferenceQueue<>();
new Thread(){
@Override
public void run() {
while (isRun) {
Reference extends String> o = referenceQueue.poll();
if (o != null) {
System.out.println("进来了");
try {
Field referent = Reference.class.getDeclaredField("referent");
referent.setAccessible(true);
Object result = referent.get(o);
System.out.println("gc will collect: " + result.getClass() + "@" + result.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}.start();
WeakReference abcWeakRef = new WeakReference(abc, referenceQueue);
abc = null;
Thread.currentThread().sleep(3000);
System.out.println("开始gc");
System.gc();
System.out.println("结束gc");
Thread.currentThread().sleep(3000);
isRun = false;
}
}
运行结果为:
class java.lang.String@96354
开始gc
结束gc
进来了
java.lang.NullPointerException
at other.TestWeakReference$1.run(TestWeakReference.java:29)
运行这段代码会发现,我们创建的Thread中报空指针异常。当我们清除强引用,触发GC的时候,JVM检测到new String(“abc”)这个堆中的对象只有WeakReference,那么JVM会释放堆对象的内存,并自动将WeakReference的referent字段设置成null,所以result.getClass()会报空指针异常。
代码与上面类似, 我们将WeakReference替换成PhantomReference:
package other;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.reflect.Field;
/**
* Created by benjamin吴海旭 on 16/9/1.
*/
public class TestPhantomReference {
public static volatile boolean isRun = true;
public static void main(String[] args) throws InterruptedException {
String abc = new String("abc");
System.out.println(abc.getClass() + "@" + abc.hashCode());
final ReferenceQueue referenceQueue = new ReferenceQueue<>();
new Thread(){
@Override
public void run() {
while (isRun) {
Reference extends String> o = referenceQueue.poll();
if (o != null) {
System.out.println("进来了");
try {
Field referent = Reference.class.getDeclaredField("referent");
referent.setAccessible(true);
Object result = referent.get(o);
System.out.println("gc will collect: " + result.getClass() + "@" + result.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}.start();
PhantomReference abcWeakRef = new PhantomReference<>(abc, referenceQueue);
abc = null;
Thread.currentThread().sleep(3000);
System.out.println("开始gc");
System.gc();
System.out.println("结束gc");
Thread.currentThread().sleep(3000);
isRun = false;
}
}
运行结果为:
class java.lang.String@96354
开始gc
结束gc
进来了
gc will collect: class java.lang.String@96354
很明显,当PhantomReference加入到引用队列的时候,referent字段的值并不是null,而且堆对象占用的内存空间仍然存在。也就是说对于虚引用,JVM是先将其加入引用队列,当我们从引用队列删除PhantomReference对象之后(此时堆中的对象是unreachable的),那么JVM才会释放堆对象占用的内存空间。由此可见,使用虚引用有潜在的内存泄露风险,因为JVM不会自动帮助我们释放,我们必须要保证它指向的堆对象是不可达的。从这点来看,虚引用其实就是强引用,当内存不足的时候,JVM不会自动释放堆对象占用的内存。
小结: 上面的测试代码,只是为了帮助我们看清楚虚引用与软引用/弱引用的不同表现。在实际的开发中,我们是不会通过反射获取referent字段的值,这样做毫无意义,也不值得提倡。
弱引用使用之WeakHashMap
由于WeakHashMap的键对象为弱引用,因此当发生GC时键对象所指向的内存空间将被回收,被回收后再调用size、clear或put等直接或间接调用私有expungeStaleEntries方法的实例方法时,则这些键对象已被回收的项目(Entry)将被移除出键值对集合中。
如果需要用一张很大的HashMap作为缓存表,那么可以考虑使用WeakHashMap,当键值不存在的时候添加到表中,存在即取出其值。
WeakHashMap weakMap = new WeakHashMap();
for(int i = 0; i < 10000; i++){
Integer ii = new Integer(i);
weakMap.put(ii, new byte[i]);
}
HashMap map = new HashMap();
for (int i = 0; i < 10000; i++) {
Integer ii = new Integer(i);
map.put(ii, new byte[i]);
}
这2段代码分别用-Xmx5M的参数运行,运行的结果是第一段代码可以很好的运行,第二段代码会出现“Java Heap Space”的错误,这说明用WeakHashMap存储,在系统内存不够用的时候会自动回收内存。
【Effective Java】第6节中写到:
内存泄露的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间内仍然留在缓存中。对于这个问题,有几种可能的解决方案。如果你正好要实现这样的缓存,只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存,当缓存中的项过期之后,它们就会被自动删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。
FinalReference以及Finalizer
FinalReference 作为 java.lang.ref 里的一个不能被公开访问的类,又起到了一个什么样的作用呢?作为他的子类, Finalizer 又在垃圾回收机制里扮演了怎么样的角色呢?
实际上,FinalReference 代表的正是 Java 中的强引用,如这样的代码:
Bean bean = new Bean();
在虚拟机的实现过程中,实际采用了 FinalReference 类对其进行引用。而 Finalizer,除了作为一个实现类外,更是在虚拟机中实现一个 FinalizerThread,以使虚拟机能够在所有的强引用被解除后实现内存清理。
让我们来看看 Finalizer 是如何工作的。首先,通过声明 FinalizerThread,并将该线程实例化,设置为守护线程后,加入系统线程中去。
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2);
finalizer.setDaemon(true);
finalizer.start();
}
在 GC 的过程中,当一个强引用被释放,由系统垃圾收集器标记后的对象,会被加入 Finalizer 对象中的 ReferenceQueue 中去,并调用 Finalizer.runFinalizer() 来执行对象的 finalize 方法。
可以看到,通过这样的方法,Java 将四种引用对象类型:软引用 (SoftReference),弱引用 (WeakReference),强引用 (FinalReference),虚引用 (PhantomReference) 平等地对待,并在垃圾回收器中进行统一调度和管理。
其他的一些Finalizer的用法请参考我的另一篇文章:Java避免使用finalizer方法
参考文章
http://www.cnblogs.com/fsjohnhuang/p/4268411.html
http://blog.csdn.net/aitangyong/article/details/39450341
http://blog.csdn.net/redcreen/article/details/6118410