掌握Java中的引用类型,有助于我们理解对象的生命周期,以及如何改变或者介入对象生命周期中的各个阶段。
每种编程语言都有自己操作内存中元素的方式,例如在 C 和 C++ 里是通过指针,而在 Java 中则是通过“引用”。在 Java 中一切都被视为了对象,但是我们操作的标识符实际上是对象的一个引用(reference)。
//创建一个引用,引用可以独立存在,并不一定需要与一个对象关联
String s;
通过将这个叫“引用”的标识符指向某个对象,之后便可以通过这个引用来实现操作对象了。
String str = new String("abc");
System.out.println(str.toString());
在 JDK1.2 之前,Java中的定义很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表着一个引用。
Java 中的垃圾回收机制在判断是否回收某个对象的时候,都需要依据“引用”这个概念。
在不同垃圾回收算法中,对引用的判断方式有所不同:
- 引用计数法:为每个对象添加一个引用计数器,每当有一个引用指向它时,计数器就加1,当引用失效时,计数器就减1,当计数器为0时,则认为该对象可以被回收(目前在Java中已经弃用这种方式了)。
- 可达性分析算法:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。
JDK1.2 之前,一个对象只有“已被引用”和"未被引用"两种状态,这将无法描述某些特殊情况下的对象,比如,当内存充足时需要保留,而内存紧张时才需要被抛弃的一类对象。
所以在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。
强引用
Java中默认声明的就是强引用,比如:
Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null; //手动置null
只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了
软引用
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。
下面以一个例子来验证下:
private static void testSoftReference() {
MyObject instance = new MyObject();
SoftReference<Object> reference = new SoftReference<>(instance);
System.out.println("[Origin]reference = " + reference.get());
instance = null; //移除强引用
gcAndWaitFinalized();//尝试触发GC
System.out.println("[After GC]reference = " + reference.get());
//在这一行,我们尝试耗尽所有可用内存,直到抛出OutOfMemoryError为止。
tryToAllocateAllAvailableMemory();
System.out.println("[After OutOfMemory]reference = " + reference.get());
}
private static void gcAndWaitFinalized() {
CountDownLatch finalizerLatch = new CountDownLatch(1);
ReferenceQueue<Object> queue = new ReferenceQueue<>();
new WeakReference<>(
new Object() {
@Override
protected void finalize() {
finalizerLatch.countDown();
}
},
queue);
System.gc();
try {
finalizerLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void tryToAllocateAllAvailableMemory() {
try {
final List<Object[]> allocations = new ArrayList<>();
int size;
while ((size = (int) Runtime.getRuntime().freeMemory()) > 0) {
Object[] part = new Object[Math.min(size, Integer.MAX_VALUE)];
allocations.add(part);
}
} catch (OutOfMemoryError e) {
System.out.println("OutOfMemoryError:" + e.getMessage());
}
}
private static class MyObject {
private byte[] data = new byte[1 * 1024 * 1024];
@Override
protected void finalize() throws Throwable {
System.out.println("==Trigger MyObject finalize==" + this.toString());
}
}
打印结果:
[Origin]reference = com.reference.ReferenceTest$MyObject@1b6d3586
[After GC]reference = com.reference.ReferenceTest$MyObject@1b6d3586
==Trigger MyObject finalize==
OutOfMemoryError:Java heap space
[After OutOfMemory]reference = null
结果表明,在可用内存不足的时候,软引用指向的对象被回收掉了。这种特性可以被用来实现缓存技术,比如网页缓存,图片缓存等。但更好的做法是基于LRU算法来实现缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。
private static void testSoftReferenceWithQueue() {
MyObject instance = new MyObject();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
SoftReference<Object> reference = new SoftReference<>(instance, queue);
System.out.println("[Origin]reference = " + reference.get());
System.out.println("[Origin]queue poll = " + queue.poll());
instance = null; //移除强引用
gcAndWaitFinalized();//尝试触发GC
System.out.println("[After GC]reference = " + reference.get());
System.out.println("[After GC]queue poll = " + queue.poll());
//在这一行,我们尝试耗尽所有可用内存,直到抛出OutOfMemoryError为止。
tryToAllocateAllAvailableMemory();
System.out.println("[After OutOfMemory]reference = " + reference.get());
System.out.println("[After OutOfMemory]queue poll = " + queue.poll());
}
打印结果:
[Origin]reference = com.reference.ReferenceTest$MyObject@1b6d3586
[Origin]queue poll = null
[After GC]reference = com.reference.ReferenceTest$MyObject@1b6d3586
[After GC]queue poll = null
OutOfMemoryError:Java heap space
==Trigger MyObject finalize==
[After OutOfMemory]reference = null
[After OutOfMemory]queue poll = java.lang.ref.SoftReference@677327b6
弱引用
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。
在GC决定回收弱引用关联的对象前,你都可以通过弱引用来获取该对象。但是一旦GC标记该对象可回收(注意:不是在调用对象的finalize()方法并释放内存之后),你就只能从中获取到NULL。
与软引用一样,弱引用也可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
下面以一个例子来验证下:
private static void testWeakReferenceWithQueue() {
MyObject instance = new MyObject();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> reference = new WeakReference<>(instance, queue);
System.out.println("[Origin]reference = " + reference.get());
System.out.println("[Origin]queue poll = " + queue.poll());
instance = null; //移除强引用
gcAndWaitFinalized();//尝试触发GC
System.out.println("[After GC]reference = " + reference.get());
System.out.println("[After GC]queue poll = " + queue.poll());
}
打印结果:
[Origin]reference = com.reference.ReferenceTest$MyObject@1b6d3586
[Origin]queue poll = null
==Trigger MyObject finalize==
[After GC]reference = null
[After GC]queue poll = java.lang.ref.WeakReference@677327b6
利用WeakReference和ReferenceQueue这个特性,Java为我们提供了WeakHashMap这个数据结构。类似于HashMap,它使用WeakReference用作Map的键。如果这个键成为垃圾,那它对应的值也会相应的被删除掉。
下面以一个例子来验证下:
private static void testWeakMap() throws InterruptedException {
WeakHashMap<String, Boolean> map = new WeakHashMap<>();
String instance = new String("hello world");
map.put(instance, true);
System.out.println("[before] weak map size=" + map.size());
instance = null;//移除强引用
gcAndWaitFinalized();//触发GC,hold住主线程直到被回收的对象finalize()方法被执行
System.out.println("[after] weak map size=" + map.size());
}
打印结果:
[before] weak map size=1
[after] weak map size=0
虚引用
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,这点和弱引用保持一致。在 JDK1.2 之后,用 PhantomReference 类来表示。
与软应用和弱引用不同点:
- 你永远无法从虚引用来获取对象,因为它的get()方法永远会返回NULL
- 虚引用必须和引用队列一起使用,软引用和弱引用是可选的
- 只有对象的内存即将被物理清理的时候,该虚引用才会进入引用队列。软引用和弱引用都是在GC开始标识不可达对象阶段就已经进入了引用队列
- 与软引用和弱引用不同,当虚引用进入引用队列后,该虚引用指向的对象不会被自动垃圾回收掉。除非该虚引用本身变得不可达或者清除掉虚引用和对象之间的关系(PhantomReference#clear())。
利用虚引用的这些特性,我们可以有机会在对象从内存中被彻底清除的时候得到一个系统通知。常用来完成资源的清理工作,它比Finalize机制更灵活可靠。
让我们看一下代码:
VM options: -Xms50m -Xmx50m -verbose:gc
private static void testPhantomReferenceWithQueue() {
MyObject instance = new MyObject();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> reference = new PhantomReference<>(instance, queue);
System.out.println("[Origin]queue poll = " + queue.poll());
instance = null; //移除强引用
gcAndWaitFinalized();//尝试触发GC - 因为MyObject重写了Finalize方法,本次无法GC,要等Finalizer线程异步执行后才能继续GC
System.out.println("[After GC]queue poll = " + queue.poll());
while(true) {
Reference ref = queue.poll();
if (ref == null) {
System.gc();//尝试再次触发GC - 从Finalized阶段尝试进入Deallocated阶段,因为存在引用队列,需要先通知。
} else {
System.out.println("[After GC Again]queue poll = " + ref);
ref.clear();//清除虚引用关系
System.gc();//再次触发GC - 真正的释放内存
break;
}
}
}
打印结果:
[Origin]queue poll = null
[GC (System.gc()) 2632K->1744K(49152K), 0.0015372 secs]
[Full GC (System.gc()) 1744K->1621K(49152K), 0.0046187 secs]
[After GC]queue poll = null
[GC (System.gc()) 2389K->1717K(49152K), 0.0004784 secs]
[Full GC (System.gc()) 1717K->1621K(49152K), 0.0051003 secs]
==Trigger MyObject finalize==com.reference.ReferenceTest$MyObject@3a18f192
[GC (System.gc()) 1877K->1717K(49152K), 0.0003184 secs]
[Full GC (System.gc()) 1717K->1618K(49152K), 0.0063282 secs]
[GC (System.gc()) 1618K->1650K(49152K), 0.0004734 secs]
[Full GC (System.gc()) 1650K->1618K(49152K), 0.0022322 secs]
[GC (System.gc()) 1874K->1682K(49152K), 0.0003491 secs]
[Full GC (System.gc()) 1682K->1619K(49152K), 0.0046835 secs]
[After GC Again]queue poll = java.lang.ref.PhantomReference@74a14482
[GC (System.gc()) 2387K->1683K(49152K), 0.0007709 secs]
[Full GC (System.gc()) 1683K->595K(49152K), 0.0053436 secs]
JDK很多地方都用到了虚引用,比如 DirectByteBuffer就是通过虚引用来实现堆外内存的释放的。
原文地址:
https://www.cnblogs.com/liyutian/p/9690974.html
http://antkorwin.com/concurrency/weakreference.html