【dubbo】【3】InternalThreadLocalMap 从Java引用类型与ThreadLocal讲起

109 阅读4分钟

Java引用类型

强引用

强引用,只要引用关系没有被=null,gc就不会回收

image.png

public class M {

    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize");
    }
}

Object的finalize()方法是对象被回收时被调用的方法。一般来说不要去修改这个方法,执行你的代码可能会导致垃圾回收变慢,进而引发OOM的风险。

public class NormalReference {
    public static void main(String[] args) throws IOException {
        M m = new M();
        m = null;
        System.gc(); //DisableExplicitGC
        System.out.println(m);

        System.in.read(); // 阻塞main线程,给垃圾回收线程时间执行
    }
}

输出结果:说明gc会在合适的时间回收没有引用的对象

null
finalize

软引用

软引用适合用来做缓存。如果gc回收时内存够,不会回收软引用,内存不够了才会回收软引用。

public class T02_SoftReference {
    // 要做这个实验,需要把设置 -Xmx=20M 在idea中可以设置VM options = -Xmx20M
    public static void main(String[] args) {
        SoftReference<byte[]> sr = new SoftReference<>(new byte[1024 * 1024 * 10]);// 1024 * 1024 = 1M

        
        System.out.println(sr.get()); // 拿得到,没有回收
        System.gc();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(sr.get()); // 拿得到,没有回收

        // 再分配一个数组,heap将装不下,这时候系统会垃圾回收,先回收一次,如果不够,会把软引用干掉
        byte[] b = new byte[1024 * 1024 * 12];
        System.out.println(sr.get()); // 拿不到,回收了

    }
}

image.png

虚引用

虚引用的应用场景:

image.png

虚引用的数据永远拿不到,只有在回收的时候,把对象放进ReferenceQueue中。

public class T04_PhantomReference {
    private static final List<Object> LIST = new LinkedList<>();
    private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();

    public static void main(String[] args) throws InterruptedException {
        PhantomReference<M> phantomReference = new PhantomReference<>(new M(), QUEUE);
        System.out.println(phantomReference.get()); // 虚引用的数据永远拿不到

        ByteBuffer b = ByteBuffer.allocateDirect(1024);

        new Thread(() -> {
            while (true) {
                LIST.add(new byte[1024 * 1024]);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(phantomReference.get());
            }
        }).start();

        new Thread(() -> {
            while (true) {
                Reference<? extends M> poll = QUEUE.poll();
                if (poll != null) {
                    System.out.println("--- 虚引用对象被jvm回收了 ----" + poll.get());
                }
            }
        }).start();

        Thread.sleep(1);

    }
}

直接内存的使用方式:(这里面就包含了虚引用的使用)

public class TestDirectByteBuffer {
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
}

image.png

弱引用

垃圾回收器看到弱引用就当垃圾看直接回收。

Q:那有这个跟没有这个引用有什么区别?不都是直接被gc回收吗?

A:经典应用场景就是ThreadLocal。WeakHashMap。


    public class T03_WeakReference {
       public static void main(String[] args) throws InterruptedException {
           WeakReference<M> wr = new WeakReference<>(new M());

           System.out.println(wr.get()); // 能拿到
           System.gc();
           Thread.sleep(1); // 睡1s让gc回收完
           System.out.println(wr.get()); // 拿不到,我们并没有把wr=null,但是System.gc()方法执行后直接就回收了,垃圾回收器看到弱引用就直接当垃圾回收了
       }
    }

输出结果:

com.example.code.gc.M@4d7e1886
finalize
null

image.png

ThreadLocal

public class ThreadLocal2 {
   static ThreadLocal<Person> tl = new ThreadLocal<>();

   public static void main(String[] args) {
       new Thread(() -> {
           try {
               Thread.sleep(1);
               // 第一个线程设置对象
               tl.set(new Person("zhangsan"));
               System.out.println(tl.get());
//                tl.remove();
           } catch (InterruptedException e) {
               throw new RuntimeException(e);
           }
       }).start();

       new Thread(() -> {
           try {
               Thread.sleep(2);
               // 第二个线程获取对象,拿不到,即使都是操作的同一个对象tl
               System.out.println(tl.get());
           } catch (InterruptedException e) {
               throw new RuntimeException(e);
           }
       }).start();
   }

   static class Person{
       public Person(String name) {
           this.name = name;
       }

       String name = "zhangsan";
   }
}

输出结果:同一个tl对象,都执行get方法,一个能拿到,一个拿不到。这里其实就说明了,ThreadLocal是跟线程相关的。

com.example.code.gc.ThreadLocal2$Person@1b9c704e
null

image.png

ThreadLocal源码

跟线程挂钩

image.png

image.png 拿到的是当前线程池的成员变量

image.png

Thread销毁的时候,ThreadLocalMap也被回收了,但是很多Thread是不会被回收的,比如说线程池里面的核心线程。要避免内存泄漏,这里entry的key设置为虚引用,外面的tl被设置为null就能保证key会被回收了。但是还是会有内存泄漏的风险。key为null了,value就访问不到。value是一个强引用。这种情况最好还是要调用tl.remove()方法进行回收。才能保证内存不会泄漏。

ThreadLocal与Thread的关系

image.png

image.png

Dubbo里面的InternalThreadLocalMap

解决的问题:为什么dubbo存线程上下文不存在ThreadLocal里面,本质是因为觉得往ThreadLocalMap里面放的value的时候是通过Hash的开发地址法,效率比较低。

所以参考了Netty改造后的ThreadLocal。通过下标的方式去寻址。提高了从ThreadLocal存和找value的速度。

image.png

dubbo2.7.1源码参考

image.png

image.png

image.png image.png 参考文档:Dubbo中的InternalThreadLocal的简单分析