在Java中提供了4个级别的引用:强引用,软引用,弱引用,虚引用。在这4个引用级别中,只有强引用FinalReference类是包内可见,其他3中引用类型均为
public
,可以在应用程序中直接使用。
强引用
Java中的引用,有点像C++的指针,通过引用,可以对堆中的对象进行操作。
在我们的代码生涯中,大部分使用的都是强引用,所谓强引入,都是形如Object o = new Object()
的操作。
强引用具备一下特点:
- 强引用可以直接访问目标对象
- 强引用所指向的对象在任何时候不会被系统回收,JVM宁愿抛出OOM异常,也不回收强引用所指向的对象
- 强引用可能导致内存泄漏
所以当我们在使用强引用创建对象时,如果下面不使用这个对象了,一定要显式地使用o = null
操作来辅助垃圾回收器进行gc操作。一旦我们没有进行置null
操作,就会造成内存泄漏,再使用MAT等工具进行复杂操作,浪费了大量的时间。
在jdk代码中也有许多显式置null
的操作。对于ArrayList
,相信了解过java的都知道,ArrayList
底层使用的数组实现的,在我们进行clear
操作时,就会对数组进行置null操作。
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
其实如果我们的对象Object o = new Object()
是在方法内创建的,那么局部变量o
将被分配在栈上,而对象Object
实例被分配在堆上,局部变量o
指向Object
实例所在的对空间,通过o
可以操作该实例,那么o
就是Object
的引用。这时候显式置null
的作用不大,只要在我们的方法退出,即该栈桢从Java虚拟机栈弹出时,o
指向Object
的引用就断开了,此时Object
在堆上分配的内存在GC时就能被回收。
软引用
软引用是除强引用外,最强的引用类型。可以通过java.lang.ref.SoftReference
使用软引用,一个持有软引用的对象,不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收,当堆使用率临近阈值时,才会去回收软引用对象。只要有足够的内存,软引用便可能在内存中存活相当长一段时间。因此,软引用可以用于实现对内存敏感的Cache。
在java doc中,软引用是这样描述的
虚拟机在抛出 OutOfMemoryError 之前会保证所有的软引用对象已被清除。此外,没有任何约束保证软引用将在某个特定的时间点被清除,或者确定一组不同的软引用对象被清除的顺序。不过,虚拟机的具体实现会倾向于不清除最近创建或最近使用过的软引用。
举个例子
/**
*
* @author xiaosuda
* @date 2018/10/23
*/
public class ReferenceTest {
public static void main(String[] args) {
//强引用
MyObject object = new MyObject();
//创建引用队列
ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
//创建软引用
SoftReference<MyObject> softRef = new SoftReference<>(object, queue);
//检查引用队列,监控对象回收情况
new Thread(new CheckRefQueue(queue)).start();
//删除强引用
object = null;
//手动GC
System.gc();
System.out.println("After GC:Soft Get= " + softRef.get());
System.out.println("分配大内存:" + Runtime.getRuntime().maxMemory());
try {
//分配大内容
byte[] maxObject = new byte[(int) Runtime.getRuntime().maxMemory()];
} catch (Throwable e) {
System.out.println(e.getMessage());
}
System.out.println("After new byte[]:Soft Get= " + softRef.get());
}
}
class CheckRefQueue implements Runnable {
ReferenceQueue queue;
public CheckRefQueue(ReferenceQueue queue) {
this.queue = queue;
}
@Override
public void run() {
Reference myObj = null;
try {
myObj = queue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (myObj != null) {
System.out.println("Object for SoftReference is " + myObj.get());
}
}
}
class MyObject {
@Override
protected void finalize() throws Throwable {
super.finalize();
//被回收时输出
System.out.println("MyObject's finalize called");
}
@Override
public String toString() {
return "I'am MyObject";
}
}
JVM参数为:-Xmx5m -XX:+PrintGCDetails -Xmn2m
执行结果如下:
[GC (System.gc()) [PSYoungGen: 891K->496K(1536K)] 891K->520K(5632K), 0.0015474 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 496K->0K(1536K)] [ParOldGen: 24K->425K(4096K)] 520K->425K(5632K), [Metaspace: 2687K->2687K(1056768K)], 0.0050401 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
After GC:Soft Get= I'am MyObject
分配大内存:5767168
[GC (Allocation Failure) [PSYoungGen: 20K->64K(1536K)] 445K->489K(5632K), 0.0002611 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 64K->96K(1536K)] 489K->521K(5632K), 0.0002294 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 96K->0K(1536K)] [ParOldGen: 425K->414K(4096K)] 521K->414K(5632K), [Metaspace: 2688K->2688K(1056768K)], 0.0037219 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 414K->414K(5632K), 0.0007248 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 414K->402K(4096K)] 414K->402K(5632K), [Metaspace: 2688K->2688K(1056768K)], 0.0047190 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
异常:Java heap space
MyObject's finalize called
Object for SoftReference is null
After new byte[]:Soft Get= null
Heap
PSYoungGen total 1536K, used 81K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 1024K, 7% used [0x00000007bfe00000,0x00000007bfe14760,0x00000007bff00000)
from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
to space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
ParOldGen total 4096K, used 402K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
object space 4096K, 9% used [0x00000007bfa00000,0x00000007bfa649d8,0x00000007bfe00000)
Metaspace used 2694K, capacity 4490K, committed 4864K, reserved 1056768K
class space used 290K, capacity 386K, committed 512K, reserved 1048576K
Process finished with exit code 0
在这个例子中,首先构造
MyObject
对象,并将其赋值给obj
变量,构成强引用。然后使用弱引用构造这个MyObject
对象的软引用,并注册导softQueue
队列里面。当softRef
被回收时,会被softQueue
队列,设置obj=null,删除这个强引用。因此,系统内对MyObject
对象的引用只剩下软引用。此时显示调用GC
,通过软引用的get方法,取得myObject
对象实例的强引用。法相对象未被回收。说明在GC
充足情况下不会回收软引用对象。
接着申请一块堆大小的的堆空间,并catch
异常,从执行结果发现,在这次GC
后,softRef.get()
不再返回MyObject
对象,而是返回null
。说明,在系统内存紧张的情况下,软引用被回收并且加入注册的引用队列
软引用在我们的日常开发中使用的场景很多,比如商城中商品的信息。某个商品可能会被多人访问,此时我们可以把该商品的信息使用软引用保存。当系统内存足够时,可以实现高速查找,当系统内存不足又会被回收,避免OOM
的风险。
TIPS: 尽管软引用会在OOM之前被清理,但是,这并不表示full gc 会清理软引用对象。在经过full gc后我们的软引用对象都放入了old区,由于full gc的存在,程序大多数强框下并不会OOM。由于软引用对象占据了老年代的空间,full gc将执行的更为频繁。所以还是建议使用弱引用
当然了,上面的例子是OOM
之前回收软引用。怎么才能full gc
就回收软引用对象呢?
-XX:SoftRefLRUPolicyMSPerMB // FullGC 保留的 SoftReference 数量,参数值越大,GC 后保留的软引用对象就越多。
当我们设置这个参数值为0时,full gc
就会回收我们的软引用对象了
修改main方法内容
public static void main(String[] args) {
//强引用
MyObject object = new MyObject();
//创建引用队列
ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
//创建软引用
SoftReference<MyObject> softRef = new SoftReference<>(object, queue);
//检查引用队列,监控对象回收情况
new Thread(new CheckRefQueue(queue)).start();
//删除强引用
object = null;
//手动GC
System.gc();
System.out.println("After new byte[]:Soft Get= " + softRef.get());
}
JVM参数-Xmx5m -XX:+PrintGCDetails -Xmn2m -XX:SoftRefLRUPolicyMSPerMB=0
执行结果
[GC (System.gc()) [PSYoungGen: 890K->496K(1536K)] 890K->520K(5632K), 0.0021185 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 496K->0K(1536K)] [ParOldGen: 24K->413K(4096K)] 520K->413K(5632K), [Metaspace: 2685K->2685K(1056768K)], 0.0071067 secs] [Times: user=0.01 sys=0.01, real=0.01 secs]
After new byte[]:Soft Get= null
Object for SoftReference is null
MyObject's finalize called
Heap
PSYoungGen total 1536K, used 71K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 1024K, 6% used [0x00000007bfe00000,0x00000007bfe11e68,0x00000007bff00000)
from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
ParOldGen total 4096K, used 413K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
object space 4096K, 10% used [0x00000007bfa00000,0x00000007bfa67608,0x00000007bfe00000)
Metaspace used 2693K, capacity 4490K, committed 4864K, reserved 1056768K
class space used 290K, capacity 386K, committed 512K, reserved 1048576K
Process finished with exit code 0
可以发现在手动GC后,软引用对象已经被回收。此时的软引用已经与弱引用效果一样了。下面看弱引用
弱引用
弱引用时一种比软引用较弱的引用类型。在系统GC时,只要发现弱引用,不管系统对空间是否足够,都会对对象进行回收。但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长时间。一旦一个弱引用对象被垃圾收集器回收,便会加入导一个注册引用队列中
修改软引用例子中的main方法内容为
public static void main(String[] args) {
//强引用
MyObject object = new MyObject();
//创建引用队列
ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
WeakReference<MyObject> weakRef = new WeakReference<>(object, queue);
new Thread(new CheckRefQueue(queue)).start();
object = null;
System.out.println("After new byte[]:Soft Get= " + weakRef.get());
System.gc();
System.out.println("After new byte[]:Soft Get= " + weakRef.get());
}
JVM
参数:-Xmx5m -XX:+PrintGCDetails -Xmn2m
执行结果如下
After new byte[]:Soft Get= I'am MyObject
[GC (System.gc()) [PSYoungGen: 891K->496K(1536K)] 891K->520K(5632K), 0.0016576 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 496K->0K(1536K)] [ParOldGen: 24K->426K(4096K)] 520K->426K(5632K), [Metaspace: 2685K->2685K(1056768K)], 0.0124398 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
MyObject's finalize called
Object for SoftReference is null
After new byte[]:Soft Get= null
Heap
PSYoungGen total 1536K, used 71K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 1024K, 6% used [0x00000007bfe00000,0x00000007bfe11e60,0x00000007bff00000)
from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
ParOldGen total 4096K, used 426K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
object space 4096K, 10% used [0x00000007bfa00000,0x00000007bfa6a988,0x00000007bfe00000)
Metaspace used 2692K, capacity 4490K, committed 4864K, reserved 1056768K
class space used 290K, capacity 386K, committed 512K, reserved 1048576K
Process finished with exit code 0
通过结果可以看到,在GC之前,弱引用对象并未被垃圾回收器发现,因此通过
weakRef.get()
方法可以获得对应的强引用,但是只要进行垃圾回收,弱引用对象一旦被发现,便会立刻被回收,并加入注册的引用队列中。此时,再通过weakRef.get()
方法取得强引用就会失败
虚引用在我们的日常代码中也经常用到,比如ThreadLocal
的内部实现就是一个ThreadLocalMap
,该map
的Entry
的key
为ThreadLocal
本身,value
为我们向ThreadLocal
对象set
的值,其中的key
就是弱引用对象。又比如
集合WeakHashMap
,都是使用了弱引用实现的,想更加深了解的可以区看看相关源码的实现。
虚引用
虚引用时所有引用类型中最弱的一个,一个持有弱引用的对象,和没有引用几乎是一样的,随时都可能被垃圾回收器回收。当试图通过虚引用的get()
方法取得强引用时,总会失败。并且,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。当垃圾回收器准备回收一个对象时,如果发现他还有虚引用,就会在垃圾回收后销毁这个对象,将这个对象加入引用队列。虚引用主要用于检测对象是否已经从内存中删除。