1. 前序
在了解引用的分类之前先了解一下对象和对象引用之间的区别。
1.1 对象
对象是类的实例。当使用new关键字创建类的实例时,jvm 会在堆内存中给该对象分配内存空间。
对象的特性:
① java对象普遍存储在堆内存中(其他情况,在经过【即时编译器】的【逃逸分析】技术分析之后,如果 能够确定一个对象不会逃逸到线程之外,那么就可以在 虚拟机栈 上为这个对象分配内存,这被称为【栈上分配】)。
② java对象 包含实例变量(非静态字段)和方法。
举例,假设有一个名为 Person的类,那么创建一个对象的行为可以是:
Person person = new Person();
在这行代码中,new Person() 实际上就是创建了一个 Person类型的对象。
1.2 对象引用
对象引用 就是一个变量,它存储的是 Java对象在堆内存中的地址。当你声明了一个变量用来存储对象时,你创建的就是一个引用,这个引用指向了堆内存中的对象,你可以通过这个引用来访问对象。
对象引用的特性:
① 对象引用可以存储在堆内存,也可以存储在虚拟机栈内存中(作为局部变量存储时)。
② 对象引用指向堆内存中的一个对象实例。
③ 如果改变指向,则就指向堆内存中的另一个不同的对象。
④ 如果引用被设置为 null,它就不再指向任何对象,这样做可以让原本引用的对象成为垃圾回收的候选对象。
继续上面的例子,变量 person 就是一个对象引用,它指向 new Person() 创建的 Person对象实例。概括来说,对象时具体的数据实体,而对象引用相当于指针或者句柄,指向这些实体数据。
2. 引用
2.1 引用的分类
Q:在Java中,对象的引用分为了四个类别:强引用、软引用、弱引用、虚引用。为什么要分这么多类型的引用?
A:这些引用类型提供了不同的生命周期和垃圾收集行为,Java虚拟机在进行垃圾回收时,可以根据应用场景的需要选择性的回收对象。
2.2 强引用
举例
Person person = new Person();
强引用在编程中最常见。只要 对象有强引用指向它,垃圾回收器就不会回收这个对象。当 person变量超出作用域或者被显示的设置为null时(代表着对象已经不可达),才会被gc回收。只要 person这个变量在作用域内,new Person()创建的对象就不会被GC回收,无论内存状况如何,因为它是可达的(垃圾标记算法:可达性分析算法)。
2.3 软引用
举例
SoftReference<Person> softRef = new SoftReference<>(new Person());
软引用是一种对内存敏感的引用类型。在内存充足时,软引用指向的对象 就不会被回收。在内存不足时,可能会被回收。
软引用通常用来实现内存敏感的缓存。
2.4 弱引用
举例
WeakReference<Person> weakRef = new WeakReference<>(new Person());
弱引用的生命周期比软引用更短。在JVM垃圾回收时,只要发现了软引用,不管当前内存空间是否足够,都会回收这个对象。
2.4.1 弱引用与强引用
结论:【Weak Reference 允许对象在没有强引用指向的情况下被垃圾收集器回收】。
解释:
这句话意味着,当一个对象只通过 WeakReference(弱引用)被引用时,它可以被 Java垃圾收集器(GC)回收,即便这个弱引用本身还存在。在Java中,一个对象是否可以被回收主要取决于它们是否可达。
举例
WeakReference<Person> weakRef = new WeakReference<>(new Person());
一旦上述代码执行过后,如果没有其他地方通过强引用指向 new Person()创建的对象,那么该对象就只剩下了一个弱引用weakRef。在这种情况下,即使 weakRef 还在作用域内,垃圾收集器 在下一次执行GC的时候也会认为这个对象是可以被回收的,因为它不是通过强引用可达的。
部分代码示例如下:
public static void weakReferenceTest() throws InterruptedException {
// 强引用。new Object() 会创建一个对象,变量 srObj 就是对象的强引用。
Object srObj = new Object();
// 弱引用。变量 weakReference 就是对象的弱引用。
WeakReference<Object> weakReference = new WeakReference<>(srObj);
// 输出 srObj 指向的 Object对象实例的地址
System.out.println("前:" + weakReference.get());
// 将强引用设置为null。此时,srObj 原本指向的 Object对象实例就可以被gc回收了。
// 因为当一个对象只有弱引用指向它时,这个对象就可以被回收了,即便弱引用weakReference还存在。
srObj = null;
// 执行垃圾收集,模拟jvm自动垃圾回收动作。实际情况是无法预知gc具体什么时间执行。
System.gc();
// 给 gc 一些时间来清理 弱引用
Thread.sleep(2000);
// 因为当一个对象只有弱引用指向它时,这个对象就可以被回收了。所以,这里只会输出null。
System.out.println("后:" + weakReference.get());
}
2.4.2 WeakHashMap
弱引用通常用来实现 “不阻碍对象被回收的关联映射”。
“关联映射” 通常指的是 键值对的存储结构。比如HashMap,在常规的HashMap中,一旦你添加了键值对,它们就会持续存在于映射中,直到你显式的移除了它们。这种情况下,即使外部没有其他的强引用指向这个键或者值,它们也不会被垃圾回收,因为HashMap自身维持着对它们的强引用。
但是,如果我们想要 允许垃圾收集器 根据需要自动移除不再使用的对象,来避免内存泄漏,这个时候就可以使用弱引用。比如,WeakHashMap,它内部就使用了弱引用。WeakHashMap的键是弱引用,这就意味着一旦外部对键的强引用不存在,这个键就可以被垃圾收集器回收了。如果某个键被回收了,那么这个键对应的键值对也会从WeakHashMap中自动移除。
部分代码示例如下:
public static void weakHashMapTest() throws InterruptedException {
// 1- 创建一个 weakHashMap
WeakHashMap<TbAddress, String> weakHashMap = new WeakHashMap<>();
// 2-1 创建一个键的强引用。
TbAddress sfTa = new TbAddress();
// 将 键和值 放入 weakHashMap中。
weakHashMap.put(sfTa, "123");
// 2-2 再创建两个键值对,没有强引用指向它们的键(只有WeakHashMap中的弱引用指向它们的键)
weakHashMap.put(new TbAddress(), "456");
weakHashMap.put(new TbAddress(), "789");
/**
* 3- 执行垃圾收集,垃圾收集器 可能会 回收没有 被强引用指向的键。
* 这个过程不确定,因为GC的确切行为取决于诸多因素。比如,GC的算法、系统的内存状态、JVM的配置等。这里只是为了促进gc的执行,只是为了演示,实际情况是无法预知gc具体什么时间执行。
*/
System.gc();
// 4- 给 gc 一些时间来清理弱引用
Thread.sleep(2000);
// 5- 打印 WeakHashMap 中仍然存在的键值对。如果发生了gc,那么这里就只会输出 "123"。
weakHashMap.forEach((tbAddress, s) -> System.out.println(s));
}
2.5 虚引用
举例
PhantomReference<Person> phantomRef = new PhantomReference<>(new Person(), someReferenceQueue);
虚引用是最弱的一种引用,【虚引用也不会影响其引用的对象生命周期】。
通过 PhantomReference的get()方法始终会返回null,这意味着,不能通过虚引用来获取对象实例。
虚引用主要用来 跟踪对象被垃圾回收器回收的活动。虚引用必须和引用队列(ReferenceQueue)结合起来使用。【当垃圾回收准备回收对象时,如果这个对象有与之关联的虚引用,并且这个 虚引用已经被注册到了某个 ReferenceQueue(引用队列)上,那么这个 虚引用 就会被添加到 与之关联的 ReferenceQueue中,这个过程由 gc 自行完成】。
虚引用 会用来做一些 对象被回收之前的清理工作,比如,释放资源、性能监控与分析等。
部分伪代码示例如下:
public static void phantomReferenceTest() throws InterruptedException {
// 强引用。new Object() 会创建一个对象,变量 srObj 就是 new Object()创建的对象实例 的强引用。
Object srObj = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
// 虚引用。变量 phantomReference 就是 srObj强引用所指向的 new Object()对象实例 的虚引用。
PhantomReference<Object> phantomReference = new PhantomReference<>(srObj, referenceQueue);
// 虚引用不能通过正常的方法来获取对象实例。所以,下面会输出null。
System.out.println("phantomReference Get: " + phantomReference.get());
// 下面输出 false,因为 上面的 srObj强引用所指向的 new Object()对象实例 还没有被gc回收,所以,虚引用也不会被加入到引用队列中。
System.out.println("phantomReference 是否已入队(引用队列)" + phantomReference.isEnqueued());
// 清除强引用,让对象符合GC回收的条件。
srObj = null;
// 强制执行垃圾收集,模拟jvm自动垃圾回收动作。实际情况是无法预知gc具体什么时间执行。
System.gc();
// 给 gc 一些时间来清理 弱引用
Thread.sleep(2000);
boolean enqueued = phantomReference.isEnqueued();
System.out.println("phantomReference 是否已入队(引用队列)" + enqueued);
if (enqueued) {
// do something like cleanup resources here
// 你可以使用 poll() 或者 remove()方法 从引用队列中获取 虚引用
PhantomReference<Object> reference = (PhantomReference) referenceQueue.poll();
if (null != reference) {
System.out.println(reference);
}
}
}