[转载] Java的四种引用关系
版权声明: 本文为转载文章, 转载时有适量修改. 再次转载时请附上原文出处链接和本声明.
原文链接: https://blog.csdn.net/u013256816/article/details/50907595
Java中提供了4个级别的引用: 强引用、软引用、弱引用和虚引用, 这四个引用定义在包java.lang.ref
下:
![Java四种引用关系的源码位置](https://img2018.cnblogs.com/blog/1438655/201909/1438655-20190909140621698-377795273.jpg)
1 强引用 (Final Reference)
强引用就是指在程序代码中普遍存在的、类似于Object obj = new Object()
的引用, 只要强引用还存在, 垃圾收集器就不会去回收这些被引用的对象.
强引用有以下三个特点:
- 强引用可以直接访问目标对象;
- 强引用锁指向的对象在任何时候都不会被系统回收 ——JVM宁愿抛出OOM异常也不回收强引用所指向的对象;
- 强引用可能导致内存泄露, 比如List中添加了new出来的对象, List失去被回收之后, 其内部的对象不能被访问、却又不会被回收的现象.
FinalReference类的全部定义如下:
package java.lang.ref;
/**
* Final references, used to implement finalization
*/
class FinalReference<T> extends Reference<T> {
public FinalReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
FinalReference只有一个构造函数: 根据给定对象的引用和引用队列构造一个强引用.
2 软引用 (Soft Reference)
软引用用来描述一些还有用但并非必须的对象: 被软引用关联着的对象, 如果内存充足, 则垃圾回收器不会回收该对象;
如果内存不够用, 就会回收这些对象.
在系统将要发生
OutOfMemoryError
之前, JVM会把被软引用关联着的对象列进回收范围, 并进行第二次回收. 如果这次回收之后内存仍然不够用, 系统才会抛出OutOfMemoryError
.
从JDK 1.2开始提供了SoftReference类来实现软引用: 与一个引用队列 (ReferenceQueue) 联合使用实现内存敏感的高速缓存 —— 如果软引用所引用的对象被垃圾回收器回收, JVM就会把这个软引用加入到与之关联的引用队列中.
2.1 案例1: 软引用的垃圾回收
package com.healchow.java.detail;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
public class SoftRefTest {
private static ReferenceQueue<MyObject> softQueue = new ReferenceQueue<>();
private static 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";
}
}
private static class CheckRefQueue implements Runnable {
Reference<MyObject> obj = null;
@Override
public void run() {
try {
obj = (Reference<MyObject>) softQueue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (obj != null) {
System.out.println("Object for SoftReference is " + obj.get());
}
}
}
public static void main(String[] args) {
MyObject obj = new MyObject();
SoftReference<MyObject> softRef = new SoftReference<>(obj, softQueue);
new Thread(new CheckRefQueue()).start();
// 删除强引用, 否则obj不会被回收
obj = null;
System.gc();
System.out.println("After GC: Soft Get = " + softRef.get());
System.out.println("尝试分配大块内存...");
byte[] b = new byte[5 * 1024 * 725];
System.out.println("After new byte[]: Soft Get = " + softRef.get());
System.gc();
}
}
(1) 测试方法1:
在运行测试主方法时设置VM参数: -Xmx5M
, 也就是指定该程序的Java Heap最大为5MB, 运行结果为:
After GC: Soft Get = I am MyObject
尝试分配大块内存...
After new byte[]: Soft Get = I am MyObject
MyObject's finalize called
Object for SoftReference is null
案例代码解释:
① 首先构造MyObject对象, 并将其赋值给object变量, 构成强引用.
② 然后使用SoftReference构造这个MyObject对象的软引用softRef, 并注册到softQueue引用队列 —— 当softRef被回收时, 会被加入softQueue队列.
③ 设置
obj = null
, 删除这个强引用, 这时系统内对MyObject对象的引用只剩下软引用.④ 显示调用GC, 通过软引用的get()方法获取MyObject对象的引用, 发现对象并未被回收, 这说明GC在内存充足的情况下, 不会回收软引用对象.
⑤ 接着请求一块大的堆空间
5*1024*725
(要多次调整使得垃圾回收工作能顺利进行、线程能顺利退出), 这个操作会使系统堆内存使用紧张, 从而产生新一轮的GC. 在这次GC后,softRef.get()
不再返回MyObject对象, 而是返回null —— 说明在系统内存紧张的情况下, 软引用被回收. 软引用被回收时, 会被加入注册的引用队列, 此时引用队列中有了元素, 开辟的多线程中softQueue.remove()
不再阻塞, 因此程序得以成功退出.
如果将上面案例中的数组再改大点, 比如5*1024*1024
, 就会抛出OOM异常:
After GC: Soft Get = I am MyObject
尝试分配大块内存...
MyObject's finalize called
Exception in thread "main" Object for SoftReference is null
java.lang.OutOfMemoryError: Java heap space
at com.healchow.java.detail.SoftRefTest.main(SoftRefTest.java:56)
(2) 测试方法2:
在运行测试主方法时设置VM参数: -Xmx5M -XX:PrintGCDetails
, 打印出GC的日志信息(关于GC日志可以查看《Java堆内存http://blog.csdn.net/u013256816/article/details/50764532》), 运行结果为:
[GC (Allocation Failure) [PSYoungGen: 1024K->480K(1536K)] 1024K->480K(5632K), 0.0013139 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (System.gc()) [PSYoungGen: 1383K->480K(1536K)] 1383K->544K(5632K), 0.0011186 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 480K->0K(1536K)] [ParOldGen: 64K->504K(4096K)] 544K->504K(5632K), [Metaspace: 3316K->3316K(1056768K)], 0.0044642 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
After GC: Soft Get = I am MyObject
尝试分配大块内存...
[GC (Allocation Failure) [PSYoungGen: 38K->64K(1536K)] 542K->568K(5632K), 0.0009263 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 64K->32K(1536K)] 568K->536K(5632K), 0.0011345 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [PSYoungGen: 32K->0K(1536K)] [ParOldGen: 504K->504K(4096K)] 536K->504K(5632K), [Metaspace: 3317K->3317K(1056768K)], 0.0038031 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 504K->504K(5632K), 0.0007999 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 504K->486K(4096K)] 504K->486K(5632K), [Metaspace: 3317K->3317K(1056768K)], 0.0037241 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
MyObject's finalize called
Object for SoftReference is null
After new byte[]: Soft Get = null
[Full GC (System.gc()) [PSYoungGen: 59K->0K(1536K)] [ParOldGen: 4086K->4083K(4096K)] 4145K->4083K(5632K), [Metaspace: 3317K->3317K(1056768K)], 0.0036086 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 1536K, used 10K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 1024K, 1% used [0x00000007bfe00000,0x00000007bfe02a68,0x00000007bff00000)
from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
ParOldGen total 4096K, used 4083K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
object space 4096K, 99% used [0x00000007bfa00000,0x00000007bfdfcfc0,0x00000007bfe00000)
Metaspace used 3324K, capacity 4564K, committed 4864K, reserved 1056768K
class space used 366K, capacity 388K, committed 512K, reserved 1048576K
2.2 案例2: 软引用缓存的使用
public class BitMapManager {
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<>();
// 保存Bitmap的软引用到HashMap
public void saveBitmapToCache(String path) {
// 强引用的Bitmap对象
Bitmap bitmap = BitmapFactory.decodeFile(path);
// 软引用的Bitmap对象
SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
// 添加该对象到Map中使其缓存
imageCache.put(path, softBitmap);
// 使用完后手动将位图对象置null
bitmap = null;
}
public Bitmap getBitmapByPath(String path) {
// 从缓存中取软引用的Bitmap对象
SoftReference<Bitmap> softBitmap = imageCache.get(path);
// 判断是否存在软引用
if (softBitmap == null) {
return null;
}
// 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空
Bitmap bitmap = softBitmap.get();
return bitmap;
}
}
2.3 软引用的应用场景
软引用主要用于内存敏感的高速缓存, 在Android系统中经常用到. 大多数 Android应用都会用到大量的图片. 由于读取文件需要硬件操作, 速度较慢, 所以考虑将图片缓存起来, 需要的时候直接从内存中读取.
但由于图片占用内存空间比较大, 缓存很多图片就可能容易发生OutOfMemoryError, 这时我们可以考虑使用软引用技术来避免这个问题发生.
SoftReference可以解决OOM的问题: 每一个对象通过软引用进行实例化, 这个对象就以cache的形式保存起来, 再次调用这个对象时, 就可以直接通过软引用中的get()
方法得到对象中的资源数据. 当内存将要发生OOM的时候, GC会迅速把所有的软引用清除, 防止OOM发生.
3 弱引用 (Weak Reference)
弱引用用来描述非必须的对象, 它的强度比软引用更弱, 被弱引用关联的对象只能生存到下一次垃圾收集发生之前. 当垃圾收集器工作时, 无论当前内存是否足够, 都会回收掉只被弱引用关联的对象. 一旦一个弱引用对象被垃圾回收器回收, 便会加入到一个注册引用队列中.
我们略微修改一下上一节案例1的代码:
package com.healchow.java.detail;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
public class WeakRefTest {
private static ReferenceQueue<MyObject> weakQueue = new ReferenceQueue<>();
private static 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";
}
}
private static class CheckRefQueue implements Runnable {
Reference<MyObject> obj = null;
@Override
public void run() {
try {
obj = (Reference<MyObject>)weakQueue.remove();
}
catch (InterruptedException e) {
e.printStackTrace();
}
if(obj != null) {
System.out.println("删除的弱引用为: " + obj);
System.out.println("获取弱引用的对象 obj.get() 为: " + obj.get());
}
}
}
public static void main(String[] args) {
MyObject object = new MyObject();
Reference<MyObject> weakRef = new WeakReference<>(object,weakQueue);
System.out.println("创建的弱引用为: " + weakRef);
new Thread(new CheckRefQueue()).start();
object = null;
System.out.println("Before GC: Weak Get = " + weakRef.get());
System.gc();
System.out.println("After GC: Weak Get = " + weakRef.get());
}
}
(1) 演示方法 —— 不修改JVM参数:
运行结果为:
创建的弱引用为: java.lang.ref.WeakReference@6f94fa3e
Before GC: Weak Get = I am MyObject
After GC: Weak Get = null
删除的弱引用 obj 为: java.lang.ref.WeakReference@6f94fa3e
但是获取弱引用的对象 obj.get() 为: null
MyObject's finalize called
可以看到:
在GC之前, 弱引用对象并未被垃圾回收器发现, 因此通过
weakRef.get()
可以获取对应的对象引用.但是只要进行垃圾回收, 弱引用就会被发现, 并立即被回收, 并加入注册引用队列中. 此时再试图通过
weakRef.get()
获取对象的引用就会失败.
(2) 弱引用的使用场景:
弱引用的使用场景可参考
java.util.WeakHashMap
.软引用、弱引用都非常适合来保存那些可有可无的缓存数据. 系统内存不足时, 这些缓存数据会被回收, 就不会导致内存溢出. 而当内存资源充足时, 这些缓存数据又可以存在相当长的时间, 从而提高系统的响应速度用.
4 虚引用 (Phantom Reference)
虚引用也称为幽灵引用或者幻影引用, 它是最弱的一种引用关系. 一个持有虚引用的对象, 和没有引用几乎是一样的 —— 随时都有可能被垃圾回收器回收.
当试图通过虚引用的
get()
方法取得强引用时, 总是会失败.并且虚引用必须和引用队列一起使用, 它的作用在于跟踪垃圾回收过程.
虚引用中get()
方法永远返回null, 其实现如下:
public T get() {
return null;
}
我们再来修改第2节案例1的代码:
package com.healchow.java.detail;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.concurrent.TimeUnit;
public class PhantomRefTest {
private static ReferenceQueue<MyObject> phantomQueue = new ReferenceQueue<>();
private static 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";
}
}
private static class CheckRefQueue implements Runnable {
Reference<MyObject> obj = null;
@Override
public void run() {
try {
obj = (Reference<MyObject>) phantomQueue.remove();
System.out.println("删除的虚引用 obj 为: " + obj);
System.out.println("但是获取虚引用的对象 obj.get() 为: " + obj.get());
System.exit(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyObject object = new MyObject();
Reference<MyObject> phantomRef = new PhantomReference<>(object, phantomQueue);
System.out.println("创建的虚引用为:" + phantomRef);
new Thread(new CheckRefQueue()).start();
object = null;
TimeUnit.SECONDS.sleep(1);
int i = 1;
while (true) {
System.out.println("第" + i++ + "次gc");
System.gc();
TimeUnit.SECONDS.sleep(1);
}
}
}
(1) 演示方法 —— 不修改JVM参数:
运行结果为:
创建的虚引用为:java.lang.ref.PhantomReference@6f94fa3e
第1次gc
MyObject's finalize called
第2次gc
删除的虚引用 obj 为: java.lang.ref.PhantomReference@6f94fa3e
但是获取虚引用的对象 obj.get() 为: null
可以看到:
在经过一次GC之后, 系统找到了垃圾对象, 并调用finalize()方法回收内存, 但没有立即将虚引用对象加入回收队列.
第二次GC时, 该对象真正被GC清除, 此时虚引用对象被加入虚引用队列.
(2) 虚引用的使用场景:
虚引用的最大作用在于跟踪对象回收, 清理被销毁对象的相关资源.
通常当对象不被使用时, 重载该对象的类的finalize()
方法可以回收对象的资源. 但是如果使用不慎, 会使得对象复活. 比如这样重写finalize()
方法:
public class Test {
private static Test obj;
@Override
protected void finalize() throws Throwable {
super.finalize();
obj = this;
}
}
创建Test对象: obj = new Test();
, 然后令obj = null;
, 之后调用System.gc()
企图销毁该对象.
但是很抱歉, 不管你调用多少次System.gc()
都没有用, 除非再次obj = null;
才能回收对象.
原因: JVM对每一个对象最多只执行一次被重写的finalize()
方法, 示例代码中, 在super.finalize()
之后又对obj进行了赋值, 使得obj又复活了, 它重写的finalize()方法不会被调用第二次.
(3) 通过虚引用清理对象
上面的小片段说明重写finalize()
方法并不是很靠谱, 我们可以使用虚引用来清理对象所占用的资源. 修改代码如下:
package com.healchow.java.detail;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class PhantomRefTest2 {
private static ReferenceQueue<MyObject> phantomQueue = new ReferenceQueue<>();
private static Map<Reference<MyObject>, String> resourceMap = new HashMap<>();
private static 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";
}
}
private static class CheckRefQueue implements Runnable {
Reference<MyObject> refObj = null;
@Override
public void run() {
try {
refObj = (Reference<MyObject>) phantomQueue.remove();
// 从资源Map中移除弱引用对象, 即手动释放资源
Object value = resourceMap.get(refObj);
System.out.println("clean resource: " + value);
resourceMap.remove(refObj);
System.out.println("删除的虚引用为: " + refObj);
System.out.println("获取虚引用的对象 obj.get() 为: " + refObj.get());
System.exit(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyObject object = new MyObject();
Reference<MyObject> phantomRef = new PhantomReference<>(object, phantomQueue);
System.out.println("创建的虚引用为: " + phantomRef);
new Thread(new CheckRefQueue()).start();
// 将创建的虚引用对象存入资源Map
resourceMap.put(phantomRef, "Some Resources");
object = null;
TimeUnit.SECONDS.sleep(1);
int i = 1;
while (true) {
System.out.println("第" + i++ + "次gc");
System.gc();
TimeUnit.SECONDS.sleep(1);
}
}
}
运行结果:
创建的虚引用为: java.lang.ref.PhantomReference@6f94fa3e
第1次gc
MyObject's finalize called
第2次gc
clean resource: Some Resources
删除的虚引用 obj 为: java.lang.ref.PhantomReference@6f94fa3e
但是获取虚引用的对象 obj.get() 为: null
参考资料
《Java程序性能优化——让你的Java程序更快、更稳定》葛一鸣 等编著。
《Java堆内存http://blog.csdn.net/u013256816/article/details/50764532》
版权声明
本文版权归原作者所有,如有侵权,请联系博主,定当立即删除。
感谢阅读,公众号 「瘦风的南墙」 ,手机端阅读更佳,还有其他福利和心得输出,欢迎扫码关注🤝