Java中的各种引用类型以及部分引用的相关例子

引用类型

在Java中,引用类型主要有四种,分别是:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。这些类型通常与垃圾回收机制有关,用来描述对象的生命周期和可达性。下面详细介绍每一种引用类型:

  1. 强引用(Strong Reference)
    强引用是最常见的引用类型,当在代码中创建一个对象并赋值给一个引用变量时,这个引用就是强引用。例如:

    String str = new String("Java");
    

    只要强引用还存在,垃圾回收器永远不会回收被引用的对象。强引用可能导致内存泄漏,因为即使对象已经不再需要了,只要强引用还在,对象就不会被回收。

  2. 软引用(Soft Reference)
    软引用是为了解决内存敏感的缓存问题而设计的。通过java.lang.ref.SoftReference类可以创建软引用。垃圾回收器在系统内存不足时会回收这些对象。软引用通常用于实现内存敏感的高速缓存,例如,图片缓存。软引用可以让缓存的对象在内存充足时被保留,而在内存不足时被回收。

    SoftReference<String> softReference = new SoftReference<>(new String("Java"));
    
  3. 弱引用(Weak Reference)
    弱引用通过java.lang.ref.WeakReference类实现。弱引用不阻止它的对象被垃圾回收器回收。垃圾回收器一旦发现只有弱引用指向的对象,不管当前内存空间足够与否,都会回收它。弱引用比软引用更弱,它主要用于实现没有阻止垃圾收集的引用链,例如,常见于元数据、查找大型结构的关键等。

    WeakReference<String> weakReference = new WeakReference<>(new String("Java"));
    
  4. 虚引用(Phantom Reference)
    虚引用是最弱的一种引用类型,通过java.lang.ref.PhantomReference类实现。一个具有虚引用的对象,跟没有引用一样,在任何时候都可能被垃圾回收器回收。设置虚引用的唯一目的是在这个对象被回收时收到一个系统通知。虚引用必须和引用队列(ReferenceQueue)联合使用。虚引用主要用于跟踪对象被垃圾回收的活动,例如,确保对象完全销毁后进行某些特定资源的清理。

    PhantomReference<String> phantomReference = new PhantomReference<>(new String("Java"), new ReferenceQueue<>());
    

弱引用

合理的使用弱引用可以解决部分内存泄漏的问题。

ThreadLocal

ThreadLocal是Java中多线程编程中一个重要的类。它能够将值存储在当前线程中,不与其他线程共享,同时让编码者能够跨越多个层级获取到该值。比如在Web服务中我们可以使用ThreadLocal存储请求的id,从而使得该次请求中所有的日志输出都携带上请求id,方便后续的异常排查;又或是如同Spring Transaction的实现一样,将Connection存储在ThreadLocal,让一个线程绑定一个Connection,实现事务机制。

ThreadLocal实现解析:
ThreadLocal的本身的实现就是弱引用使用的一个经典案例。

image
上面是ThreadLocal的get方法,可以看到它先是获取当前的线程之后再获取了线程中的ThreadLocalMap实例,接着从该Map中获取到具体的值。也就是说ThreadLocal实际上是通过每一个线程中存储一个单独的ThreadLocalMap实例来实现的。

接下来查看ThreadLocalMap的源码:
image
ThreadLocalMap是ThreadLocal的一个内部类,它的Entry继承自WeakReference,结合Entry的构造方法可以发现Entry是持有了一个指向ThreadLocal实例的弱引用并且使用该弱引用作为Key。如此实现,当外部没有指向该ThreadLocal的软引用、强引用之后,该ThreadLocal实例将会被直接回收。这样做带来的一个好处就是,在我们不需要该ThreadLocal对象之后,这个ThreadLocal能够及时的被GC回收,保证不会因此导致内存泄漏。

有一个经典问题:ThreadLocal是如何导致内存泄漏的?
首先ThreadLocal自身提供了remove方法,上面说了ThreadLocalMap的实现保证了程序不会因为ThreadLocal导致内存泄漏,那这个问题是不是与上面所说的冲突了?其实不然,从前面的代码可以看到ThreadLocalMap的Entry.key虽然是弱引用,但是Entry.value并不是弱引用。当ThreadLocal被GC回收之后,并且我们在回收之前没有显示调用remove方法,我们便无法访问到对应的Entry,从而将该Entry删除,那么始终便存在一条这样的引用链路Thread->ThreadLocalMap->Entry->value,在线程被结束之前该value以及该Entry无法被回收,若是该线程无法没及时结束,那么就有可能导致内存泄漏。

可能会有人问为何不将Entry以及Entry.value也实现为弱引用?答案也很简单,若是实现为弱引用,那么很多情况下通过ThreadLocal获取一开始存入的值都将为null,在非特殊情况下,
ThreadLocal的作用可以说是等于零。

综上所述,在编码的时候,只要合理的调用remove方法其实就能够避免因ThreadLocal导致的内存泄漏。

WeakHashMap

Java中还有其他地方也使用到了弱引用,比如WeakHashMap。

  • 相比ThreadLocal,WeakHashMap也是通过将对象的软引用作为entry.key,但是与ThreadLocal最大的不同就是它同时使用了ReferenceQueue,ReferenceQueue提供了在引用对象被GC回收时,通知给程序的功能,WeakHashMap借此实现了在get等方法调用时,自动删除被回收key所对应条目的功能。

使用案例:

import java.util.WeakHashMap;

public class WeakCache {
    private final WeakHashMap<Key, BigObject> weakMap = new WeakHashMap<>();

    public void put(Key key, BigObject object) {
        weakMap.put(key, object);
    }

    public BigObject get(Key key) {
        return weakMap.get(key);
    }

    // Key类用于映射的键
    class Key {
        // ...
    }

    // BigObject类是一个大对象,占用大量内存
    class BigObject {
        // ...
    }
}

WeakCache 类中,通过WeakHashMap 实现了一个弱引用键的缓存。当Key没有在其他地方被强引用时,这个Key-Value映射随时都可能被自动移除。

posted @ 2024-04-13 16:33  辻-  阅读(41)  评论(0编辑  收藏  举报