• 强软弱虚四种引用以及ThreadLocal源码解析


    六、强软弱虚四种引用以及ThreadLocal源码

    强软弱虚引用

    强引用

    当我们使用Object obj = new Object()创建一个对象时,指向这个对象的引用就称为强引用。只要这个引用还指向一个对象,那么指向的这个对象就不会被垃圾回收器回收。

    package com.gouxiazhi.reference;
    
    /**
     * 强引用
     * @author 赵帅
     * @date 2021/1/17
     */
    public class ReferenceDemo1 {
    
        public static void main(String[] args) {
            // 启动时设置jvm初始堆和最大堆大小为3M -Xms3M -Xmx3M
    
            // 先创建一个1M的字节数组,这是强引用
            byte[] content = new byte[1024 * 1024];
            // 再创建一个2M的字节数组,因为上面已经创建了1M了,强引用只要content还指向对象,就不会被垃圾回收器回收。因此会抛出内存溢出OOM异常。
            // 当打开下面这句代码时,content不再指向一个对象,那么上面创建的对象就会被垃圾回收器回收,就可以正常运行了。
    //        content = null;
    
            byte[] bytes = new byte[2 * 1024 * 1024];
        }
    }
    

    软引用

    使用软引用创建的对象,当内存不够时,就会被垃圾回收器回收。

    package com.gouxiazhi.reference;
    
    import java.lang.ref.SoftReference;
    
    /**
     * 软引用
     * @author 赵帅
     * @date 2021/1/17
     */
    public class ReferenceDemo2 {
    
        public static void main(String[] args) {
            // 启动时设置jvm初始堆和最大堆大小为3M -Xms3M -Xmx3M
    
            // 使用软引用创建一个1M的数组。
            SoftReference<byte[]> reference = new SoftReference<>(new byte[1024 * 1024]);
    
            System.out.println("reference = " + reference.get());
            // 上面使用软引用创建了一个1M的数组,下面再创建2M的数组时,因为堆内存空间不够,就会调用gc清理掉软引用的指向的对象。因此不会抛出异常。
            // 如果上面不使用软引用,而使用 byte[] a = new byte[1024*1024];就会抛出内存溢出异常。
            byte[] bytes = new byte[2 * 1024 * 1024];
            // 因为创建上面的对象内存不够,因此软引用指向的对象已经被回收
            System.out.println("reference = " + reference.get());
    
        }
    }
    

    弱引用

    使用弱引用创建的对象,只要垃圾回收器看见,就会回收。

    package com.gouxiazhi.reference;
    
    import java.lang.ref.WeakReference;
    import java.util.Arrays;
    
    /**
     * @author 赵帅
     * @date 2021/1/17
     */
    public class ReferenceDemo3 {
        public static void main(String[] args) {
    
            // 弱引用指向的对象,只要垃圾回收器看见就立马回收掉。
            WeakReference<byte[]> weakReference = new WeakReference<>(new byte[1024 * 1024]);
            System.out.println("weakReference = " + Arrays.toString(weakReference.get()));
            System.gc();
            System.out.println("weakReference = " + Arrays.toString(weakReference.get()));
        }
    }
    
    

    虚引用

    主要用来管理堆外内存等。

    package com.gouxiazhi.reference;
    
    import java.lang.ref.PhantomReference;
    import java.lang.ref.Reference;
    import java.lang.ref.ReferenceQueue;
    import java.util.ArrayList;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author 赵帅
     * @date 2021/1/17
     */
    public class ReferenceDemo4 {
    
        public static class M{
            @Override
            protected void finalize() throws Throwable {
                System.out.println("对象被回收了");
                super.finalize();
            }
        }
    
        public static void main(String[] args) {
            ReferenceQueue<M> referenceQueue = new ReferenceQueue<>();
            // 虚引用的使用,除了指向一个对象,还需要指定一个引用队列。
            PhantomReference<M> reference = new PhantomReference<>(new M(), referenceQueue);
            System.out.println("reference = " + reference);
            // 虚引用指向的对象无法被获取到,弱引用被垃圾回收器看见就会被回收,虚引用比弱引用级别更低
            System.out.println("reference = " + reference.get()); // null
    
            // 虚引用一般都是指向一个堆外内存,因为垃圾回收器只能回收堆内存,无法管理堆外内存.
            // 如果使用java管理堆外内存。假设M代表着堆外内存,那么当虚引用被回收时,他会将自身放入referenceQueue引用队列,开启另一个线程监听这个队列,
            // 当这个队列取到内容时,就代表要回收这块堆外内存了,可以执行回收堆外内存操作
    
            new Thread(() -> {
                
                // 如果从队列中取到虚引用,那么就表示需要回收这个堆外内存了。
                Reference<? extends M> poll = referenceQueue.poll();
                if (poll != null) {
                    System.out.println("虚引用对象被jvm回收了" + poll);
                }
            }).start();
    
    
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            ArrayList<byte[]> list = new ArrayList<>();
    
            while (true) {
                list.add(new byte[4 * 1024]);
            }
    
        }
    }
    

    ThreadLocal源码

    查看ThreadLocal的set方法源码:

        public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    

    可以看到,在set方法中,首先获取当前线程。然后通过getMap(t)获取了一个ThreadLocalMap对象。然后将要保存的对象存入了这个Map中,key值就是ThreadLoacl对象本身,value值为要保存的数据。

    然后我们再点开getMap方法:

        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    

    返回的是t.threadLocals属性值,而且这个值是ThreadLocalMap类型的。查看ThreadLocalMap类:

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

    可以看到ThreadLocalMap中存放数据的Entry节点继承自WeakReference<?>,因此说ThreadLocal底层用的是弱引用,而且在存储时,Map的key作为弱引用,也就是ThreadLocal对象本身作为弱引用存放,值是强引用存放的。

    查看ThreadLocalMap类的set方法:

    					private void set(ThreadLocal<?> key, Object value) {
    
                // 获取存放数据的数组,底层数据结构
                Entry[] tab = table;
                // 获取数组的长度
                int len = tab.length;
                // 计算key要存放的下标
                int i = key.threadLocalHashCode & (len-1);
    
                // 从下标i开始遍历数组,如果下标i 有元素了,也就是hash冲突了,那么就往后插并将下标自增
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                  
                  // 获取下标为i的entry元素
                    ThreadLocal<?> k = e.get();
    							// 如果此位置的key与要保存的key相同则替换值
                    if (k == key) {
                        e.value = value;
                        return;
                    }
    								// 如果key是空的话,说明这个位置的key已经过期被回收,则替换值为新的值。
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
    
                // 如果下标i为空,说明这个位置是空的。插入这个位置
                tab[i] = new Entry(key, value);
                int sz = ++size;
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    

    简单分析上面整个过程,整个threadlocal存放数据过程甬道图如下:

    ![image-20210118143336157](/Users/zhaoshuai/Library/Application Support/typora-user-images/image-20210118143336157.png)

    当调用threadLocal的set方法时:

    1. 获取当前线程的threadLocals属性。
    2. 调用threadLocals属性的set方法。
      • 获取threadLocalMap的entry数组。
      • 计算当前threadLocal对象的下标。
      • 获取下标的entry,如果没有则新建一个entry,如果有的话,判断当前的key是否与要存入的key一致,如果一致,则替换值。如果key为空的话,则替换值。如果上面条件都不满足,则新建一个entry对象。

    ThreadLocal造成的内存泄漏

    因为ThreadLocal是将自身作为弱引用存放在ThreadLocalMap中的,因此当一个ThreadLocal对象的强引用消失时。那么这个key将会被回收,这时原来这个key对应的value值如果没有被移除的话,那么就永远无法被访问到了。而且因为这个value值是作为entry节点的value引用指向的,value引用是一个强引用,那么这时,这个value属性就永远无法被回收,也无法被访问,就会造成内存泄漏。

    使用ThreadLocal如何避免内存泄漏?

    使用ThreadLocal set了一个值以后,在这个线程结束之前,一定要调用remove方法移除存放的值。

  • 相关阅读:
    win7下命令行添加系统服务
    java执行cmd命令
    grails-BuildConfig.groovy中grails.war.resources
    密码学
    Grails框架优劣势
    groovy+idea+Maven项目加载自身jar包
    cmd查看我的电脑盘符和文件
    MySQL insert插入
    MySQL截取字符串函数方法
    mysql 替换语句
  • 原文地址:https://www.cnblogs.com/Zs-book1/p/14292904.html
Copyright © 2020-2023  润新知