• 一文讲透java弱引用以及使用场景


    概念

    大部分情况下我们看到是强引用,比如下面这一行:

    String str1 = new String("abc");
    

    变量str1被用来存放一个string对象的强引用上。强引用在你正在使用时这个对象时,一般是不会被垃圾回收器回收的。当出现内存空间不足时,虚拟机不会释放强引用的对象占用的空间,而是选择抛出异常(OOM)。

    什么时候会回收强引用的空间呢,就是没有引用的时候,比如你这样写:

    str1 = null
    

    GC在适当的时候就会回收str1指向的空间。

    而弱引用(Weak Reference)的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

    java中使用弱引用的语法是:

    1. String str1 = new String("abc");
    2. WeakReference<String> weakReference = new WeakReference<>(str1);

    深入原理

    我们先来通过一个案例,看下gc对于弱引用的回收策略。

    1. public class App {
    2. public static WeakReference<String> weakReference1;
    3. public static void main(String[] args) {
    4. test1();
    5. //test1外部,hello对象作用域结束,没有强引用指向"value"了。只有一个弱引用指向"value"
    6. System.out.println("未进行gc时,只有弱引用指向value内存区域:" + weakReference1.get());
    7. //此时gc时会回收弱引用
    8. System.gc();
    9. //此时输出都为nuill
    10. System.out.println("进行gc时,只有弱引用指向value内存区域:" + weakReference1.get());
    11. }
    12. public static void test1() {
    13. //hello对象强引用"value"
    14. String hello = new String("value");
    15. //weakReference1对象弱引用指向"value"
    16. weakReference1 = new WeakReference<>(hello);
    17. //test1内部调用gc,此时gc不会回收弱引用,因为hello对象强引用"value"
    18. System.gc();
    19. System.out.println("进行gc时,强引用与弱引用同时指向value内存区域:" + weakReference1.get());
    20. }
    21. }

    运行的结果:

    1. 进行gc时,强引用与弱引用同时指向value内存区域:value
    2. 未进行gc时,只有弱引用指向value内存区域:value
    3. 进行gc时,只有弱引用指向value内存区域:null

    这里有个前置知识说下,当要获得WeakReference的object时, 首先需要判断它是否已经被GC回收,若被收回,则下列返回值为空:

    weakReference1.get();
    

    根据这个结果,我们可以得出这样的结论:

    • 当有强引用指向value内存区域时,即使进行gc,弱引用也不会被释放,对象空间不回被回收。
    • 当无强引用指向value内存区域是,进行gc,弱引用会被释放,对象空间将会执行回收流程。

    我们接着从源码层面看下弱引用。首先引入一个概念叫gc的可达性。因为本篇文章并不是专门讲gc的,所以我这里并不打算展开太多这部分,知识一句话概括下gc可达性的概念。

    GC决定一个对象是否可被回收,其基本思路是从GC Root开始向下搜索,如果对象与GC Root之间存在引用链,则对象是可达的,GC会根据是否可到达与可到达性决定对象是否可以被回收。

    1. public class WeakReference extends Reference {
    2. /**
    3. * Creates a new weak reference that refers to the given object. The new
    4. * reference is not registered with any queue.
    5. *
    6. * @param referent object the new weak reference will refer to
    7. */
    8. public WeakReference(T referent) {
    9. super(referent);
    10. }
    11. /**
    12. * Creates a new weak reference that refers to the given object and is
    13. * registered with the given queue.
    14. *
    15. * @param referent object the new weak reference will refer to
    16. * @param q the queue with which the reference is to be registered,
    17. * or null if registration is not required
    18. */
    19. public WeakReference(T referent, ReferenceQueuesuper T> q) {
    20. super(referent, q);
    21. }
    22. }

    WeakReference源码很简单,所以大部分逻辑都在父类Reference中。Reference类中有个核心的类是ReferenceQueue,这类的注释是这样写的:

    Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected.

    翻译过来大概意思是,在检测到对象的可达性发生改变后,垃圾回收器就将已注册的引用对象添加到Reference queues队列中。

    这个类有三个public方法,enqueue,poll和remove。标准的队列操作,这个很简单不展开。

    然后再回到Reference类,它是这样定义的:

    1. /**
    2. * Abstract base class for reference objects. This class defines the
    3. * operations common to all reference objects. Because reference objects are
    4. * implemented in close cooperation with the garbage collector, this class may
    5. * not be subclassed directly.
    6. *
    7. * @author Mark Reinhold
    8. * @since 1.2
    9. */
    10. public abstract class Reference<T> {

    首先它是一个抽象类,意味着你不能直接创建此类的实例。注释的意思是,这是引用对象的抽象基类,这个类定义了引用对象的常用操作。引用对象的实现一般都和垃圾回收器密切相关。

    这里有个很重要的信息就是这个类和垃圾回收密切相关。

    一个reference的实例有四种状态,

    Active

    这是一个受会受到GC的特别关注的状态,当GC察觉到引用的可达性变化为“合适”的状态之后,reference实例的状态将变化为Pending或Inactive,到底转化为Pending状态还是Inactive状态取决于此Reference对象创建时是否注册了queue.如果注册了queue,则将添加此实例到pending-Reference list中。而新创建的Reference实例的状态是Active。

    Pending

    在pending-Reference list中等待着被Reference-handler 线程入队列queue中的元素处于pending状态。没有注册queue的实例是永远不可能到达这一状态。

    Enqueued

    当实例创建的时候加入了队列后的状态。当实例被从ReferenceQueue中移除时,它的状态变为Inactive。没有注册ReferenceQueue的不可能到达这一状态的。

    Inactive

    终态。一旦一个实例变为Inactive,则这个状态永远都不会再被改变了。

    掌握了这四个状态,继续往下看源码,

    volatile ReferenceQueue<? super T> queue;
    

    这个queue是通过构造函数传入的,表示创建一个Reference实例时,要将其注册到那个queue上。

    然后继续看有个很重要的类,

    1. private static class ReferenceHandler extends Thread {
    2. ...
    3. static {
    4. ThreadGroup tg = Thread.currentThread().getThreadGroup();
    5. for (ThreadGroup tgn = tg;
    6. tgn != null;
    7. tg = tgn, tgn = tg.getParent());
    8. Thread handler = new ReferenceHandler(tg, "Reference Handler");
    9. /* If there were a special system-only priority greater than
    10. * MAX_PRIORITY, it would be used here
    11. */
    12. handler.setPriority(Thread.MAX_PRIORITY);
    13. handler.setDaemon(true);
    14. handler.start();
    15. // provide access in SharedSecrets
    16. SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
    17. @Override
    18. public boolean tryHandlePendingReference() {
    19. return tryHandlePending(false);
    20. }
    21. });
    22. }
    23. ...

    根据代码,我们知道这个线程处理类具有最高的优先级,并且是daemon状态在跑。这个线程的逻辑就是:不断的从Reference构成的pending链表上获取Reference对象,如果pending不为null,则将pending的对象进行clean,如果注册的时候有queue就进行enqueue,否则线程进行wait状态。

    基于以上分析,我们可以总结下Reference的机制:

    pending是由JVM来赋值的,当Reference内部的referent对象的可达状态发生改变时,JVM会将Reference对象放入到pending链表中。然后启动一个ReferenceHandler线程来处理,处理的逻辑就是调用Cleaner#clean,然后根据注册时候是否有队列决定是否调用ReferenceQueue#enqueue方法进行处理。

    应用案例解析

    弱引用一般用在什么场合呢?我们可以通过一些常见的组件的源码来分析下。来看看常见的threadlocal的源码关于弱引用的使用。

    threadLocal是一个线程本地变量,每个线程维护自己的变量副本,它的实现原理简单来讲就是每个线程Thread类都有个属性ThreadLocalMap,Map是自定义实现的Entry[]数组结构,所以可以维护该线程的多个ThreadLocal变量。

    图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key的。

    既然说是Entry,必然有key和value。其中Key即是ThreadLocal变量本身,Value则是具体该线程中的变量真实的副本值。代码如下:

    1. static class ThreadLocalMap {
    2. /**
    3. * The entries in this hash map extend WeakReference, using
    4. * its main ref field as the key (which is always a
    5. * ThreadLocal object). Note that null keys (i.e. entry.get()
    6. * == null) mean that the key is no longer referenced, so the
    7. * entry can be expunged from table. Such entries are referred to
    8. * as "stale entries" in the code that follows.
    9. */
    10. static class Entry extends WeakReference<ThreadLocal<?>> {
    11. /** The value associated with this ThreadLocal. */
    12. Object value;
    13. Entry(ThreadLocal<?> k, Object v) {
    14. super(k);
    15. value = v;
    16. }
    17. }

    然后你会注意到,Entry的Key即ThreadLocal对象是采用弱引用引入的。为什么ThreadLocalMap使用弱引用存储ThreadLocal呢?

    还是看上面那张图。

    ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程迟迟不结束的话(因为大部分时候我们用的都是线程池,核心线程都是长期驻留的),这些key为null的Entry的value就会一直存在一条强引用链:

    Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
    

    永远无法回收,造成内存泄漏。这样看,似乎是弱引用导致了内存泄漏?

    事实上是,无论这里使用强引用还是弱引用,都有可能造成内存泄漏。如果key 使用强引用:引用的ThreadLocal的对象如果置为null,被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

    反过来,如果key使用了弱引用,当jvm发现内存不足时,会自动回收弱引用指向的实例内存,也就是回收对ThreadLocal对象。但是这个时候value还是存在的。不过没有关系,看源码你会发行在调用get或者set操作的时候,都有机会执行回收无效entry的操作。

  • 相关阅读:
    软件测试/测试开发丨为什么接口自动化测试是提升职业技能的关键?
    FPGA - 科学设计复位信号(XILINX)
    第八章:Vue3(下)
    解锁高效检索技能:掌握MySQL索引数据结构的精髓
    kubernetes1.18集群安装实战
    基因组的Phasing原理
    80篇国产数据库实操文档汇总(含TiDB、达梦、openGauss等)
    相机标定计算内参数:每次拍照,相机和标定板都可以变换位置吗?
    Express操作MongoDB
    day5-day6【代码随想录】螺旋矩阵II
  • 原文地址:https://blog.csdn.net/xiaopangcame/article/details/132842968