Java中的五种引用方式底层详解

       在GC算法的可达性算法中描述的对象引用,一般指的是强引用,即是GCRoot对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收,而在Java中一共有五种引用关系。

目录

 

强引用 (Strong Reference)

软引用 (Soft Reference)

使用软引用实现简单缓存

 一个实际应用示例:图片缓存

带有引用队列的增强版缓存

使用场景和注意事项:

弱引用 (Weak Reference)

虚引用 (Phantom Reference)

终结器引用 (Final Reference)

总结:

软引用和虚引用可以单独使用,也可以使用引用队列

 对比使用和不使用引用队列的区别

虚引用必须使用引用队列

关键细节

引用对象的生命周期:


强引用 (Strong Reference)

最常见的引用类型,只要强引用存在,垃圾收集器就永远不会回收被引用的对象。

public class StrongReferenceDemo {
    public static void main(String[] args) {
        // 创建强引用
        Object strongRef = new Object();
        
        // 即使手动触发垃圾回收,strongRef指向的对象也不会被回收
        System.gc();
        
        // 只有当strongRef = null 后,对象才可能被回收
        strongRef = null;
        System.gc();
    }
}

软引用 (Soft Reference)

内存充足时不会被回收,内存不足时会被回收。常用于实现内存敏感的缓存,要先通过参数给jvm设置一定的内存空间,内存空间如果设置太大,即使是如图中分配了1000M以后并不会造成内存不足,也就不会回收软引用引用的对象。

public class SoftReferenceDetail {
    public static void main(String[] args) {
        // 创建一个大对象
        byte[] data = new byte[1024 * 1024 * 100]; // 100MB
        // 创建软引用
        SoftReference<byte[]> softRef = new SoftReference<>(data);
        
        // 清除强引用
        data = null;

        // 检查软引用是否被回收
        System.out.println("软引用对象是否存在:" + (softRef.get() != null));
        
        try {
            // 分配大量内存,触发GC
            byte[] newData = new byte[1024 * 1024 * 1000]; // 1000MB
        } catch (OutOfMemoryError e) {
            // 检查软引用是否被回收
            System.out.println("软引用对象是否存在:" + (softRef.get() != null));
        }
    }
}
使用软引用实现简单缓存
public class SoftReferenceCache<K, V> {
    // 使用ConcurrentHashMap存储缓存项
    private final ConcurrentHashMap<K, SoftReference<V>> cache = new ConcurrentHashMap<>();
    
    /**
     * 放入缓存
     */
    public void put(K key, V value) {
        cache.put(key, new SoftReference<>(value));
    }
    
    /**
     * 获取缓存项
     */
    public V get(K key) {
        SoftReference<V> softRef = cache.get(key);
        if (softRef != null) {
            V value = softRef.get();
            if (value == null) {
                // 如果软引用已经被回收,则移除该项
                // 也可以去数据库查询出相应的数据再放到map中,可以看实际情况选择
                cache.remove(key);
            }
            return value;
        }
        return null;
    }
    
    /**
     * 移除缓存项
     */
    public void remove(K key) {
        cache.remove(key);
    }
    
    /**
     * 清理已经被回收的软引用
     */
    public void cleanNullReferences() {
        cache.entrySet().removeIf(entry -> entry.getValue().get() == null);
    }
}

 一个实际应用示例:图片缓存
public class ImageCache {
    private static class ImageWrapper {
        private final byte[] imageData;
        private final String imageName;
        
        public ImageWrapper(byte[] imageData, String imageName) {
            this.imageData = imageData;
            this.imageName = imageName;
        }
        
        // 模拟图片大小
        public int getSize() {
            return imageData.length;
        }
    }
    
    private static class SoftImageCache {
        private final Map<String, SoftReference<ImageWrapper>> imageCache = new ConcurrentHashMap<>();
        
        /**
         * 加载图片到缓存
         */
        public void loadImage(String imageName, byte[] imageData) {
            imageCache.put(imageName, 
                new SoftReference<>(new ImageWrapper(imageData, imageName)));
        }
        
        /**
         * 获取图片
         */
        public ImageWrapper getImage(String imageName) {
            SoftReference<ImageWrapper> softRef = imageCache.get(imageName);
            if (softRef != null) {
                ImageWrapper image = softRef.get();
                if (image != null) {
                    return image;
                } else {
                    // 软引用已被回收,清除缓存条目
                    imageCache.remove(imageName);
                }
            }
            return null;
        }
        
        /**
         * 获取缓存大小
         */
        public int getCacheSize() {
            return imageCache.size();
        }
    }
    
    // 使用示例
    public static void main(String[] args) {
        SoftImageCache imageCache = new SoftImageCache();
        
        // 模拟加载大量图片
        for (int i = 0; i < 100; i++) {
            // 每张图片1MB
            byte[] imageData = new byte[1024 * 1024];
            imageCache.loadImage("image" + i, imageData);
            System.out.println("加载图片: image" + i);
        }
        
        // 模拟访问图片
        ImageWrapper image50 = imageCache.getImage("image50");
        System.out.println("访问图片50: " + (image50 != null));
        
        // 触发GC
        System.gc();
        
        // 再次访问图片
        image50 = imageCache.getImage("image50");
        System.out.println("GC后访问图片50: " + (image50 != null));
    }
}
带有引用队列的增强版缓存
public class EnhancedSoftCache<K, V> {
    private final Map<K, SoftReference<V>> cache = new ConcurrentHashMap<>();
    private final ReferenceQueue<V> queue = new ReferenceQueue<>();
    
    /**
     * 清理已被GC回收的对象
     */
    private void processQueue() {
        Reference<? extends V> ref;
        while ((ref = queue.poll()) != null) {
            // 遍历缓存找到并删除已经被回收的引用
            cache.entrySet().removeIf(entry -> entry.getValue() == ref);
        }
    }
    
    /**
     * 存入缓存
     */
    public void put(K key, V value) {
        processQueue(); // 清理已回收的对象
        cache.put(key, new SoftReference<>(value, queue));
    }
    
    /**
     * 获取缓存值
     */
    public V get(K key) {
        processQueue(); // 清理已回收的对象
        SoftReference<V> ref = cache.get(key);
        return ref != null ? ref.get() : null;
    }
    
    /**
     * 获取缓存大小
     */
    public int size() {
        processQueue(); // 清理已回收的对象
        return cache.size();
    }
}

        如上代码中定义了方法processQueue()来进行清理map中已经被回收的引用,然后在每个方法中都调用processQueue()方法来清理key和引用对象Reference,这两个对象都是被map强引用的。

使用场景和注意事项:

适用场景:

1.内存敏感的缓存实现

 2.图片、文件等大对象的缓存

 3.需要自动释放内存的场景

优点:

1.内存不足时自动释放 

2.无需手动管理内存 

3.比弱引用更持久

注意事项:

1.软引用对象的回收时机不确定   

2.需要定期清理无效引用 

3.不适合存储必须保持的数据

最佳实践:

1.配合引用队列使用

2.及时清理无效引用

3.合理设置缓存容量

4.考虑使用 WeakHashMap 作为替代方案

软引用是实现内存敏感型缓存的理想选择,它能在内存充足时保留缓存数据,在内存紧张时自动释放,从而在性能和内存使用之间取得平衡。

弱引用 (Weak Reference)

比软引用更弱,只要发生垃圾回收,弱引用对象就会被回收。常用于避免内存泄漏。

public class WeakReferenceDemo {
    public static void main(String[] args) {
        // 创建弱引用
        WeakReference<String> weakRef = new WeakReference<>(new String("Hello"));
        
        // 获取弱引用对象
        System.out.println("回收前:" + weakRef.get());
        
        // 触发垃圾回收
        System.gc();
        
        // 弱引用对象可能被回收
        System.out.println("回收后:" + weakRef.get());
    }
}

虚引用 (Phantom Reference)

最弱的引用关系,随时可能被回收。必须和引用队列 (ReferenceQueue) 一起使用,主要用于跟踪对象被回收的状态。

public class PhantomReferenceDemo {
    public static void main(String[] args) {
        // 创建引用队列
        ReferenceQueue<String> queue = new ReferenceQueue<>();
        
        // 创建虚引用
        PhantomReference<String> phantomRef = new PhantomReference<>(new String("Hello"), queue);
        
        // 虚引用对象永远无法通过get()方法获取
        System.out.println("虚引用对象:" + phantomRef.get()); // 永远返回null
        
        // 触发垃圾回收
        System.gc();
        
        // 检查引用队列是否收到通知
        Reference<?> ref = queue.poll();
        System.out.println("对象是否被回收:" + (ref != null));
    }
}

终结器引用 (Final Reference)

当对象被垃圾回收器检测到不可达时,终结器引用会被加入引用队列,等待执行它的 finalize() 方法。

public class FinalReferenceDemo {
    public static class ResourceClass {
        private static List<ResourceClass> list = new ArrayList<>();
        
        @Override
        protected void finalize() throws Throwable {
            // finalize方法会在对象被回收前调用
            System.out.println("执行finalize方法");
            // 可以在这里进行资源清理
            list.add(this); // 可以在finalize中复活对象
            super.finalize();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        ResourceClass ref = new ResourceClass();
        ref = null;
        
        // 触发垃圾回收
        System.gc();
        
        // 等待finalize执行
        Thread.sleep(1000);
    }
}

总结:

1. 强引用:最常用,不会被回收

2.软引用:内存不足时回收

3.弱引用:垃圾回收时回收

4. 虚引用:随时可能被回收,必须配合引用队列使用

5.终结器引用:用于实现对象的finalize方法

这五种引用强度依次减弱:强引用 > 软引用 > 弱引用 > 虚引用/终结器引用

在实际应用中:

  • 缓存场景常用软引用
  • 防止内存泄漏常用弱引用(如WeakHashMap)
  • 跟踪对象回收状态用虚引用
  • 资源清理场景可能用到终结器引用

        我在第一次看代码的时候曾有个疑惑就是,为什么要用引用队列呢?不用引用队列不是一样可以回收吗?不知道各位读者是否有和我一样的疑惑。

软引用和虚引用可以单独使用,也可以使用引用队列

        因为个人感觉软引用和虚引用其实差别不大,只是回收的时机不同,所以下面从虚引用的角度叙述。

        弱引用对象可以单独使用,也可以配合引用队列使用。当对象只有弱引用时,在下一次 GC 时一定会被回收,再由上面的map缓存案例也是可以得到,如果你用了引用队列,已经回收的对象都会放在引用队列里面,只虚要遍历引用队列就可以清理掉已经回收的数据,时间复杂度其实可以达到O(1),但是如果不用引用队列的话就根本不知道那些数据是被回收的,清理数据就会变得更加困难,就需要遍历整个 map。

public class WeakReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        // 创建引用队列
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        
        // 创建弱引用,关联引用队列
        WeakReference<Object> weakRef = new WeakReference<>(new Object(), referenceQueue);
        
        System.out.println("初始对象: " + weakRef.get());
        
        // 触发GC
        System.gc();
        Thread.sleep(1000);
        
        // 检查对象是否被回收
        System.out.println("GC后对象: " + weakRef.get());
        
        // 检查引用队列
        Reference<?> ref = referenceQueue.poll();
        System.out.println("引用队列中的对象: " + (ref == weakRef)); // true表示弱引用对象已经进入队列
    }
}
 对比使用和不使用引用队列的区别
public class WeakReferenceComparisonDemo {
    private static class Resource {
        private String name;
        private byte[] data = new byte[1024 * 1024]; // 1MB
        
        public Resource(String name) {
            this.name = name;
        }
        
        @Override
        protected void finalize() {
            System.out.println("Resource被回收: " + name);
        }
        
        @Override
        public String toString() {
            return "Resource{name='" + name + "'}";
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        // 不使用引用队列的弱引用
        WeakReference<Resource> weakRefWithoutQueue = 
            new WeakReference<>(new Resource("资源1"));
            
        // 使用引用队列的弱引用
        ReferenceQueue<Resource> queue = new ReferenceQueue<>();
        WeakReference<Resource> weakRefWithQueue = 
            new WeakReference<>(new Resource("资源2"), queue);
            
        System.out.println("GC前:");
        System.out.println("无队列弱引用对象: " + weakRefWithoutQueue.get());
        System.out.println("有队列弱引用对象: " + weakRefWithQueue.get());
        
        // 触发GC
        System.gc();
        Thread.sleep(1000);
        
        System.out.println("\nGC后:");
        System.out.println("无队列弱引用对象: " + weakRefWithoutQueue.get());
        System.out.println("有队列弱引用对象: " + weakRefWithQueue.get());
        
        // 检查引用队列
        Reference<?> ref = queue.poll();
        System.out.println("引用队列中的对象: " + (ref == weakRefWithQueue));
    }
}

虚引用必须使用引用队列

       虚引用必须配合引用队列使用,否则毫无意义。虚引用的主要作用是跟踪对象的回收状态,因为不管有没有gc,phantomRef.get()都无法得到数据,始终返回的是null。

public class PhantomReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        // 创建引用队列(必需)
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        
        // 创建一个大对象
        byte[] data = new byte[1024 * 1024]; // 1MB
        
        // 创建虚引用
        PhantomReference<byte[]> phantomRef = new PhantomReference<>(data, referenceQueue);
        
        // 清除强引用
        data = null;
        
        // 第一次检查
        System.out.println("原始对象: " + phantomRef.get()); // 始终返回null
        
        // 触发GC
        System.gc();
        Thread.sleep(1000);
        
        // 检查引用队列
        Reference<?> ref;
        while ((ref = referenceQueue.poll()) != null) {
            System.out.println("对象被回收,引用进入队列");
            // 可以在这里进行额外的清理工作
        }
    }
}

关键细节

引用对象的生命周期:
  • SoftReference/WeakReference 对象本身是普通对象,受强引用规则控制
  • 只有当没有强引用指向这些Reference对象时,它们才会被回收
  • 被引用的对象可能先于Reference对象被回收
  • 当不再需要Reference对象时,应该将其设为null
  • 使用引用队列来清理已经无用的Reference对象
  • 在缓存实现中要注意及时清理失效的引用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值