图书管理员
想象一下,你是一个图书馆管理员:
- 强引用:一本书被借走后,只要借书人没有归还(强引用存在),你就不能把书下架(回收)。
- 软引用:一本书被标记为"可下架",但只有在书架满了(内存不足)时,你才会考虑下架它。
- 弱引用:一本书被标记为"优先下架",只要有人来整理书架(垃圾回收),它就会被下架。
- 幻象引用:一本书已经被下架了,但你还需要记录它的信息(清理操作),以便后续处理。
考察知识点
这个问题主要涉及以下知识点:
- Java 引用类型:强引用、软引用、弱引用、幻象引用的定义及区别。
- 对象可达性:强可达、软可达、弱可达、幻象可达的概念及其在垃圾回收中的作用。
- 引用队列(ReferenceQueue):如何通过引用队列监控对象的回收状态。
- 实际应用场景:不同引用类型在缓存、资源管理等场景中的应用。
知识解析
1、强引用(Strong Reference):金卡 VIP 饭票
最常见的引用类型,只要强引用存在,垃圾回收器就不会回收该对象。
对象是强可达的。即使内存不足,JVM 抛出 OutOfMemoryError
也不会回收强引用对象。
这就是传说中的"金主爸爸",持有它的对象地位稳如老狗,只要你还有强引用,GC 绝对不敢动你 !除非你自己把这张金卡剪了(设为 null
),不然 JVM 再怎么缺内存,也不会动你一根寒毛。
- 使用场景:普通对象的引用。
java
/**
* 强引用示例
*/
public class StrongReferenceExample {
public static void main(String[] args) {
Object obj = new Object(); // 强引用
System.gc(); // 建议 JVM 执行垃圾回收
System.out.println("强引用对象仍然存在: " + obj);
}
}
2. 软引用(Soft Reference):"吃剩饭"的饭票
通过 SoftReference
类实现,内存不足时会被回收。
对象是软可达的。用于描述一些还有用但非必需的对象。适合用于实现内存敏感的缓存。
这是"资源紧张才考虑回收"的灰色会员卡。对象被软引用持有时,GC 会说:"你不是必须的 VIP,但也不想浪费你......等我实在没内存了再考虑让你走。"
使用场景:常用于实现内存敏感的缓存,比如图片、配置之类------内存够就留着,不够就清走。
java
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;
/**
* 使用软引用实现的图片缓存
*/
public class ImageCache {
// 使用 HashMap 存储软引用图片对象
private final Map<String, SoftReference<Image>> cache = new HashMap<>();
/**
* 从缓存中获取图片
*/
public Image getImage(String key) {
SoftReference<Image> softRef = cache.get(key);
if (softRef != null) {
Image image = softRef.get();
if (image != null) {
System.out.println("从缓存中获取图片: " + key);
return image;
} else {
System.out.println("图片已被回收: " + key);
cache.remove(key); // 清除无效的软引用
}
}
return null;
}
/**
* 将图片放入缓存
*/
public void putImage(String key, Image image) {
cache.put(key, new SoftReference<>(image));
System.out.println("图片已缓存: " + key);
}
/**
* 模拟图片类
*/
static class Image {
private final String name;
public Image(String name) {
this.name = name;
}
@Override
public String toString() {
return "Image{" + name + "}";
}
}
public static void main(String[] args) {
ImageCache imageCache = new ImageCache();
// 缓存图片
imageCache.putImage("image1", new Image("image1"));
imageCache.putImage("image2", new Image("image2"));
// 从缓存中获取图片
Image image1 = imageCache.getImage("image1");
System.out.println("获取到的图片: " + image1);
// 模拟内存不足
try {
System.out.println("模拟内存不足...");
byte[] bytes = new byte[1024 * 1024 * 100]; // 分配大量内存
} catch (OutOfMemoryError e) {
System.out.println("内存不足,部分图片可能已被回收");
}
// 再次从缓存中获取图片
Image image2 = imageCache.getImage("image2");
System.out.println("获取到的图片: " + image2);
}
}
3、弱引用(Weak Reference):"边吃边准备走"的临时饭票
通过 WeakReference
类实现,比软引用更弱,无论内存是否充足,只要发生垃圾回收,就会被回收弱引用指向的对象。
对象是弱可达的。适合用于非强制性的映射关系。非常适合用于实现非强制性的映射关系,例如缓存、监听器管理或对象关联等场景。
这张票一进 JVM 食堂,清洁工就虎视眈眈,GC 扫到你就直接回收,不看天气、不看库存,只要你不是强引用就马上说拜拜。
- 使用场景 :
WeakHashMap
是 Java 标准库中基于弱引用实现的典型例子。它的键是弱引用,当键对象不再被其他强引用指向时,垃圾回收器会自动回收该键,并移除对应的键值对。
java
import java.util.Map;
import java.util.WeakHashMap;
/**
* 使用 WeakHashMap 实现的非强制性缓存,假设我们有一个 `User` 类,表示系统中的用户。
* 我们希望缓存用户的元数据(如用户名、邮箱等),但当用户对象不再被其他部分引用时,缓存可以自动清理对应的元数据。
*/
public class WeakReferenceExample {
// 使用 WeakHashMap 缓存用户元数据
private static final Map<User, String> userMetadataCache = new WeakHashMap<>();
/**
* 用户类
*/
static class User {
private final String id;
private final String name;
public User(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "User{id='" + id + "', name='" + name + "'}";
}
}
/**
* 添加用户元数据到缓存
*/
public static void addUserMetadata(User user, String metadata) {
userMetadataCache.put(user, metadata);
System.out.println("添加用户元数据: " + user + " -> " + metadata);
}
/**
* 从缓存中获取用户元数据
*/
public static String getUserMetadata(User user) {
String metadata = userMetadataCache.get(user);
if (metadata != null) {
System.out.println("从缓存中获取用户元数据: " + user + " -> " + metadata);
} else {
System.out.println("用户元数据已被回收: " + user);
}
return metadata;
}
public static void main(String[] args) {
// 创建用户对象
User user1 = new User("1", "Alice");
User user2 = new User("2", "Bob");
// 添加用户元数据到缓存
addUserMetadata(user1, "Alice's metadata");
addUserMetadata(user2, "Bob's metadata");
// 从缓存中获取用户元数据
getUserMetadata(user1);
getUserMetadata(user2);
// 模拟用户对象不再被强引用
user1 = null; // user1 不再被强引用
System.gc(); // 建议 JVM 执行垃圾回收
// 再次从缓存中获取用户元数据
System.out.println("GC 后尝试获取 user1 的元数据...");
getUserMetadata(new User("1", "Alice")); // 新建一个相同 ID 的用户对象
getUserMetadata(user2);
}
}
4、幻象引用(Phantom Reference):饭都吃完了才给你发的"离场通知单"
你已经吃完走人(对象已经无法访问了),但系统还想 在你被回收前做点善后处理,比如清理 Native 内存、关闭文件。
注意:你根本 不能通过幻象引用拿到对象,它就是一张"对象快死"的告知单,等你收到了,说明该准备后事了(如资源释放)。
这时候用上 PhantomReference
,配合 ReferenceQueue
,无法通过 get()
方法获取对象。对象是幻象可达的。用于对象被回收后的清理操作。
使用场景 :资源清理,例如 Cleaner
机制。
java
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
/**
* 幻象引用示例
*/
public class PhantomReferenceExample {
public static void main(String[] args) {
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
System.gc(); // 建议 JVM 执行垃圾回收
System.out.println("幻象引用对象: " + phantomRef.get()); // 始终返回 null
System.out.println("引用队列中的对象: " + queue.poll()); // 对象已被回收并加入队列
}
}
知识拓展
1、引用队列(ReferenceQueue)
引用队列(ReferenceQueue
)是 Java 中与引用类型(如 SoftReference
、WeakReference
、PhantomReference
)配合使用的一种机制。它用于跟踪被垃圾回收器回收的引用对象,并在对象被回收时将引用对象加入到队列中。通过引用队列,我们可以感知到对象的回收事件,并执行一些后续操作(如资源清理)。
引用队列的作用
-
跟踪对象回收:引用队列允许我们监控哪些对象已经被垃圾回收器回收。
-
执行清理操作:当对象被回收时,可以通过引用队列触发一些清理操作(如释放资源、移除缓存等),通过引用队列清理缓存中的无效条目。
-
避免内存泄漏 :结合
WeakReference
或PhantomReference
,可以避免因未及时清理引用而导致的内存泄漏。
java
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
/**
* 使用 WeakReference 和 ReferenceQueue 的示例,展示如何监控对象回收并执行清理操作。
**/
public class ReferenceQueueExample {
public static void main(String[] args) throws InterruptedException {
// 创建引用队列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
// 创建对象
Object obj = new Object();
// 创建弱引用,并关联引用队列
WeakReference<Object> weakRef = new WeakReference<>(obj, referenceQueue);
// 启动一个线程监控引用队列
Thread cleanupThread = new Thread(() -> {
try {
while (true) {
// 从队列中获取被回收的引用
Reference<?> ref = referenceQueue.remove();
System.out.println("对象已被回收,执行清理操作: " + ref);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("监控线程被中断");
}
});
cleanupThread.setDaemon(true); // 设置为守护线程
cleanupThread.start();
// 解除强引用,使对象只有弱引用
obj = null;
// 触发垃圾回收
System.gc();
// 等待一段时间,确保垃圾回收完成
Thread.sleep(1000);
}
}
2、显式影响软引用回收策略
调整 -XX:SoftRefLRUPolicyMSPerMB
参数
- 增加该值:软引用对象的存活时间变长,减少被回收的概率。
- 减少该值:软引用对象的存活时间变短,增加被回收的概率。
bash
-XX:SoftRefLRUPolicyMSPerMB=500 # 将每 MB 堆内存中软引用对象的存活时间设置为 500 毫秒。
调整堆内存大小
- 增加堆内存大小(
-Xmx
)可以减少内存压力,从而降低软引用对象被回收的概率。 - 减少堆内存大小会增加内存压力,可能导致软引用对象更快被回收。
java
java -Xms1g -Xmx4g CalosApp // 将堆内存的初始大小设置为 1 GB,最大大小设置为 4 GB
3、Reachability Fence(可达性栅栏)
Java 9 引入的一种机制,用于防止对象在代码执行期间被提前回收。它的主要作用是确保在某个代码点之前,对象仍然是可达的(即不会被垃圾回收器回收)。这种机制解决了在某些复杂场景下,JVM 优化行为可能导致的对象提前回收问题。
在某些情况下,JVM 的优化行为可能会导致对象在代码执行期间被提前回收,从而引发不可预知的错误。例如:
-
逃逸分析优化:JVM 可能会通过逃逸分析优化代码,认为某些对象不会逃逸出当前方法或线程,从而提前回收这些对象。
-
局部变量生命周期判断错误:在某些复杂的代码逻辑中,JVM 可能会错误地判断局部变量的生命周期,导致对象被提前回收。如果一个对象只有弱引用或软引用,但它的某些字段或方法仍在被访问,垃圾回收器可能会错误地回收该对象。
-
多线程环境中的不确定性:在多线程环境中,对象的可达性可能会因线程调度的不确定性而受到影响。
java
import java.lang.ref.Reference;
import java.util.concurrent.CompletableFuture;
public class ReachabilityFenceExample {
static class Resource {
private final byte[] buffer = new byte[1024 * 1024]; // 1 MB 缓冲区
public byte[] getBuffer() {
return buffer;
}
@Override
protected void finalize() throws Throwable {
System.out.println("Resource 被回收");
super.finalize();
}
}
public static void main(String[] args) throws InterruptedException {
// 创建 Resource 对象
Resource resource = new Resource();
// 异步操作:访问 Resource 的缓冲区
CompletableFuture.runAsync(() -> {
// 在异步操作中访问 Resource 的缓冲区
byte[] buffer = resource.getBuffer();
System.out.println("缓冲区大小: " + buffer.length);
// 模拟耗时操作
try {
Thread.sleep(1000); // 模拟 1 秒的耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再次访问 Resource 的缓冲区
System.out.println("再次访问缓冲区大小: " + resource.getBuffer().length);
// 使用 Reachability Fence 确保 Resource 对象在异步操作中不被回收
Reference.reachabilityFence(resource);
});
// 解除 Resource 的强引用
resource = null;
// 触发垃圾回收
System.gc();
// 等待异步操作完成
Thread.sleep(2000);
}
}
1、没有 Reachability Fence
的情况
注释掉 Reference.reachabilityFence(resource)
,可能会输出:
text
缓冲区大小: 1048576
Resource 被回收
再次访问缓冲区大小: 1048576
或者(如果垃圾回收发生在第一次输出之前):
text
Resource 被回收
缓冲区大小: 1048576
Exception in thread "ForkJoinPool.commonPool-worker-1" java.lang.NullPointerException
或者(如果垃圾回收发生在第二次访问之前):
text
缓冲区大小: 1048576
Resource 被回收
Exception in thread "ForkJoinPool.commonPool-worker-1" java.lang.NullPointerException
当注释掉 Reference.reachabilityFence(resource)
时,代码的行为如下:
resource
对象在main
方法中被设置为null
,失去了强引用。- 垃圾回收器可能会在异步操作执行期间回收
resource
对象。 - 如果
resource
被回收,finalize
方法会被调用,输出Resource 被回收
。 - 尽管
resource
被回收,buffer
仍然是一个独立的数组对象,它的生命周期不受resource
影响。因此,buffer.length
仍然可以正确访问。
2、使用 Reachability Fence
的情况
启用 Reference.reachabilityFence(resource)
,输出:
text👎
缓冲区大小: 1048576
再次访问缓冲区大小: 1048576
Resource 被回收