Волшебное использование сильных и слабых ссылок Void в Java

Java

предисловие

ThreadLocalПри каких обстоятельствах может произойти утечка памяти? Если вы хотите узнать все тонкости этой проблемы, необходимо просмотреть исходный код.После прочтения исходного кода вы обнаружите, что:ThreadLocalиспользуется вstatic class Entry extends WeakReference<ThreadLocal<?>> {}ответ на самом деле является использование слабых ссылокWeakReference.

Краткое изложение содержания этой статьи

  • Сильная ссылка: Object o = new Object()
  • Мягкая ссылка: новая SoftReference(o);
  • Слабая ссылка: новая WeakReference(o);
  • Фантомная ссылка: новая PhantomReference(o);
  • Использование ThreadLocal и причины утечек памяти, вызванных неправильным использованием

Jdk 1.2 добавляет абстрактные классыReferenceиSoftReference,WeakReference,PhantomReference, который расширяет классификацию ссылочных типов для обеспечения более точного контроля над памятью.

Например, наши данные кеша, когда памяти не хватает, надеюсь, кеш может освободить память, или хранить кеш вне кучи и т.д.

Но как определить, какие объекты необходимо удалить (алгоритм сборки мусора, анализ достижимости), время освобождения и время освобождения позволяет нам получить уведомление об освобождении, поэтому JDK 1.2 предоставляет эти ссылочные типы.

Тип котировки когда перерабатывать
сильная цитата Объекты со строгой ссылкой не будут восстановлены, пока доступен корень GC.Если памяти недостаточно, будет выброшен oom
Мягкая ссылка: SoftReference Мягкий ссылочный объект, в корневом каталоге GC, только мягкая ссылка может достичь объекта a, до того, как oom, сборка мусора переработает объект a
Слабая ссылка: WeakReference Слабая ссылка, в корне GC только слабые ссылки могут достичь объекта c, и c будет переработан, когда произойдет gc
Фантомная ссылка: PhantomReference Виртуальная ссылка должна использоваться с ReferenceQueue.Я не знаю, когда она будет переработана, но после переработки вы можете использовать ReferenceQueue, чтобы получить переработанную ссылку.

сильная цитата

Мы часто используем сильные ссылки: Object o = new Object(). Во время сборки мусора переменные со строгой ссылкой не будут переработаны.Только когда установлено o=null, jvm проходит анализ достижимости и корень GC не достигает объекта, сборщик мусора очистит объекты в куче и освободит память . Когда вы продолжите подавать заявку на выделение памяти, она будет расти.

Определите класс Demo, экземпляр Demo занимает размер памяти 10 м, продолжайте добавлять примеры Demo в список, поскольку выделение памяти не может быть применено, программа выдает oom для завершения

// -Xmx600m
public class SoftReferenceDemo {
    // 1m
    private static int _1M = 1024 * 1024 * 1;
    public static void main(String[] args) throws InterruptedException {
        ArrayList<Object> objects = Lists.newArrayListWithCapacity(50);
        int count = 1;
        while (true) {
            Thread.sleep(100);
            // 获取 jvm 空闲的内存为多少 m
            long meme_free = Runtime.getRuntime().freeMemory() / _1M;
            if ((meme_free - 10) >= 0) {
                Demo demo = new Demo(count);
                objects.add(demo);
                count++;
                demo = null;
            }
            System.out.println("jvm 空闲内存" + meme_free + " m");
            System.out.println(objects.size());
        }
    }

    @Data
    static class Demo {
        private byte[] a = new byte[_1M * 10];
        private String str;
        public Demo(int i) {
            this.str = String.valueOf(i);
        }
    }
}

Приведенный выше код работает, завершает работу программы OOM.

jvm 空闲内存41 m
54
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.fly.blog.ref.SoftReferenceDemo$Demo.<init>(SoftReferenceDemo.java:37)
	at com.fly.blog.ref.SoftReferenceDemo.main(SoftReferenceDemo.java:25)

Однако в некоторых бизнес-сценариях нам нужно освободить некоторые ненужные данные, когда памяти не хватает. Например, информацию о пользователе мы храним в кеше.

мягкая ссылка

jdk был добавлен с 1.2Reference ,SoftReferenceявляется одной из категорий, ее роль заключается в том, чтобы достичь объекта a через корень GC, толькоSoftReference, объект a будет выпущен jvm gc до jvm oom.

бесконечный цикл кListДобавить к10mЛевый и правый размер данных (SoftReference), обнаружил, что нет oom.

// -Xmx600m
public class SoftReferenceDemo {
    // 1m
    private static int _1M = 1024 * 1024 * 1;
    public static void main(String[] args) throws InterruptedException {
        ArrayList<Object> objects = Lists.newArrayListWithCapacity(50);
        int count = 1;
        while (true) {
            Thread.sleep(500);
            // 获取 jvm 空闲的内存为多少 m
            long meme_free = Runtime.getRuntime().freeMemory() / _1M;
            if ((meme_free - 10) >= 0) {
                Demo demo = new Demo(count);
                SoftReference<Demo> demoSoftReference = new SoftReference<>(demo);
                objects.add(demoSoftReference);
                count++;
                // demo 为 null,只有 demoSoftReference 一条引用到达 Demo 的实例,GC 将会在 oom 之前回收 Demo 的实例
                demo = null;
            }
            System.out.println("jvm 空闲内存" + meme_free + " m");
            System.out.println(objects.size());
        }
    }
    @Data
    static class Demo {
        private byte[] a = new byte[_1M * 10];
        private String str;
        public Demo(int i) {
            this.str = String.valueOf(i);
        }
    }
}

image-20200625213429845

пройти черезjvisualvmВид с использованием кучи JVM, вы можете увидеть переполнение стека вовремя, чтобы спастись, что много бесплатной памяти, вы принимаете инициативу для выполнения执行垃圾回收, память не будет восстановлена.

слабая ссылка

Ссылка на демонстрацию объекта толькоWeakReferenceКогда достижимы, демонстрация будет восстановлена ​​после GC, чтобы выпустить память.

Следующие процедуры будут работать без остановки, просто разное время выпуска памяти

// -Xmx600m -XX:+PrintGCDetails
public class WeakReferenceDemo {
    // 1m
    private static int _1M = 1024 * 1024 * 1;

    public static void main(String[] args) throws InterruptedException {
        ArrayList<Object> objects = Lists.newArrayListWithCapacity(50);
        int count = 1;
        while (true) {
            Thread.sleep(100);
            // 获取 jvm 空闲的内存为多少 m
            long meme_free = Runtime.getRuntime().freeMemory() / _1M;
            if ((meme_free - 10) >= 0) {
                Demo demo = new Demo(count);
                WeakReference<Demo> demoWeakReference = new WeakReference<>(demo);
                objects.add(demoWeakReference);
                count++;
                demo = null;
            }
            System.out.println("jvm 空闲内存" + meme_free + " m");
            System.out.println(objects.size());
        }
    }

    @Data
    static class Demo {
        private byte[] a = new byte[_1M * 10];
        private String str;
        public Demo(int i) {
            this.str = String.valueOf(i);
        }
    }
}

результат операции,SoftReferenceДоступная память освобождается, когда она почти исчерпана, иWeakReferenceКаждый раз, когда доступная память достигает360mМусор разнесут направо и налево, а память освободится

[GC (Allocation Failure) [PSYoungGen: 129159K->1088K(153088K)] 129175K->1104K(502784K), 0.0007990 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
jvm 空闲内存364 m
36
jvm 空闲内存477 m

фантомная ссылка

Есть также имя幻灵引用Поскольку вы не знаете, когда он был восстановлен, должны соответствовать требуемымReferenceQueue, когда объект перерабатывается, вы можете получить экземпляр PhantomReference из этой очереди.

// -Xmx600m -XX:+PrintGCDetails
public class PhantomReferenceDemo {
    // 1m
    private static int _1M = 1024 * 1024 * 1;

    private static ReferenceQueue referenceQueue = new ReferenceQueue();

    public static void main(String[] args) throws InterruptedException {
        ArrayList<Object> objects = Lists.newArrayListWithCapacity(50);
        int count = 1;
        new Thread(() -> {
            while (true) {
                try {
                    Reference remove = referenceQueue.remove();
                    // objects 可达性分析,可以到达 PhantomReference<Demo>,内存是不能及时释放的,我们需要在队里中拿到那个 Demo 被回收了,然后
                    // 从 objects 移除这个对象
                    if (objects.remove(remove)) {
                        System.out.println("移除元素");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        while (true) {
            Thread.sleep(500);
            // 获取 jvm 空闲的内存为多少 m
            long meme_free = Runtime.getRuntime().freeMemory() / _1M;
            if ((meme_free - 10) > 40) {
                Demo demo = new Demo(count);
                PhantomReference<Demo> demoWeakReference = new PhantomReference<>(demo, referenceQueue);
                objects.add(demoWeakReference);
                count++;
                demo = null;
            }
            System.out.println("jvm 空闲内存" + meme_free + " m");
            System.out.println(objects.size());
        }
    }

    @Data
    static class Demo {
        private byte[] a = new byte[_1M * 10];
        private String str;

        public Demo(int i) {
            this.str = String.valueOf(i);
        }
    }
}

ThreadLocal

ThreadLocalВ нашем фактическом развитии мы все еще используем больше. Так что же это такое (локальная переменная потока), мы знаем局部变量(переменные, определенные внутри метода) и成员变量(свойства класса).

Иногда мы надеемся, что жизненный цикл переменной может проходить через цикл выполнения задачи всего потока (потоки в пуле потоков могут быть назначены для выполнения разных задач), и мы можем получить эту предустановленную переменную при вызове каждого метода. ,ЭтоThreadLocalэффект.

Например, мы хотим получить текущий запрос.HttpServletRequest, а затем можно получить в каждом текущем методе,SpringBootОн уже упакован для нас.RequestContextFilterПосле того, как каждый запрос приходит, он будет проходить черезRequestContextHolderнастраивать线程本地变量, принцип работыThreadLocal.

ThreadLoCal только для вызовов в текущем потоке, вызовы перекрестных потоков не являются приемлемыми, поэтому JDK проходитInheritableThreadLocalнаследоватьThreadLocalреализовать.

ThreadLocal получает информацию о пользователе текущего запроса

Вы можете понять, глядя на заметкиTheadLocalкак пользоваться

/**
 * @author 张攀钦
 * @date 2018/12/21-22:59
 */
@RestController
public class UserInfoController {
    @RequestMapping("/user/info")
    public UserInfoDTO getUserInfoDTO() {
        return UserInfoInterceptor.getCurrentRequestUserInfoDTO();
    }
}

@Slf4j
public class UserInfoInterceptor implements HandlerInterceptor {
    private static final ThreadLocal<UserInfoDTO> THREAD_LOCAL = new ThreadLocal();
    // 请求头用户名
    private static final String USER_NAME = "userName";
    // 注意这个,只有注入到 ioc 中的 bean,才能注入进来
    @Autowired
    private IUserInfoService userInfoService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 判断是不是接口请求
        if (handler instanceof HandlerMethod) {
            String userName = request.getHeader(USER_NAME);
            UserInfoDTO userInfoByUserName = userInfoService.getUserInfoByUserName(userName);
            THREAD_LOCAL.set(userInfoByUserName);
            return true;
        }
        return false;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 用完之后记得释放掉内存
        THREAD_LOCAL.remove();
    }
    // 获取当前线程设置的用户信息
    public static UserInfoDTO getCurrentRequestUserInfoDTO() {
        return THREAD_LOCAL.get();
    }
}

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * 将 UserInfoInterceptor 注入到 ioc 容器中
     */
    @Bean
    public UserInfoInterceptor getUserInfoInterceptor() {
        return new UserInfoInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 调用这个方法返回的就是 ioc 的 bean
        registry.addInterceptor(getUserInfoInterceptor()).addPathPatterns("/**");
    }
}

InheritableThreadLocal

Иногда нам нужно продлить время жизни локальных переменных текущего потока до子线程Переменная, набор родительского потока, получение дочернего потока.InheritableThreadLocalсостоит в том, чтобы обеспечить эту способность.

/**
 * @author 张攀钦
 * @date 2020-06-27-21:18
 */
public class InheritableThreadLocalDemo {
    static InheritableThreadLocal<String> INHERITABLE_THREAD_LOCAL = new InheritableThreadLocal();
    static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
        INHERITABLE_THREAD_LOCAL.set("父线程中使用 InheritableThreadLocal 设置变量");
        THREAD_LOCAL.set("父线程中使用 ThreadLocal 设置变量");
        Thread thread = new Thread(
                () -> {
                    // 能拿到设置的变量
                    System.out.println("从 InheritableThreadLocal 拿父线程设置的变量: " + INHERITABLE_THREAD_LOCAL.get());
                    // 打印为 null
                    System.out.println("从 ThreadLocal 拿父线程设置的变量: " + THREAD_LOCAL.get());
                }
        );
        thread.start();
        thread.join();
    }
}

Анализ исходного кода метода получения ThreadLocal

Вы можете понять, что объект Thead имеет свойство Map, ключ которогоThreadLoalНапример, получить исходный код локальной переменной потока.

public class ThreadLocal<T> {
    public T get() {
        // 获取运行在那个线程中
        Thread t = Thread.currentThread();
        // 从 Thread 拿 Map 
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 使用 ThreadLocal 实例从 Map 获取值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 初始化 Map,并返回初始化值,默认为 null,你可以定义方法,从这个方法加载初始化值
        return setInitialValue();
    }
}

InheritableThreadLocal Получить анализ данных, заданный родительским потоком

Каждый поток также имеет свойство Map с именем inheritableThreadLocals, которое содержит значение, скопированное из родительского потока.

При инициализации дочернего потока он копирует значение карты родительского потока (inheritableThreadLocals) в свою собственную карту Thead Map (inheritableThreadLocals).Каждый поток поддерживает свои собственные inheritableThreadLocals, поэтому дочерний поток не может изменять данные, поддерживаемые родительским потоком, но только дочерний поток может получить данные, установленные родительским потоком.

public class Thread{
    
	// 维护线程本地变量
    ThreadLocal.ThreadLocalMap threadLocals = null;

    // 维护可以子线程可以继承的父线程的数据
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
   // 线程初始化
    public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize) {
        init(group, target, name, stackSize);
    }
    
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (inheritThreadLocals && parent.inheritableThreadLocals != null){
            // 将父线程的 inheritableThreadLocals 数据复制到子线程中去
            this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        }
    }
}

public class TheadLocal{
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        /// 创建自己线程的 Map,将父线程的值复制进去
        return new ThreadLocalMap(parentMap);
    }

    static class ThreadLocalMap {
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];
            // 遍历父线程,将数据复制过来
            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
    }
} 

демонстрационная проверка, приведенный выше анализ

image-20200627232351534

image-20200627225502636

причина утечки памяти

Определяется пул потоков размером 20 и выполняется 50 задач.threadLocalУстановите значение null, чтобы имитировать сценарий утечки памяти. Чтобы исключить мешающие факторы, я установил параметр jvm в-Xms8g -Xmx8g -XX:+PrintGCDetails

public class ThreadLocalDemo {
    private static ExecutorService executorService = Executors.newFixedThreadPool(20);
    private static ThreadLocal threadLocal = new ThreadLocal();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 50; i++) {
            executorService.submit(() -> {
                try {
                    threadLocal.set(new Demo());
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    if (Objects.nonNull(threadLocal)) {
                        // 为防止内存泄漏,当前线程用完,清除掉 value
//                        threadLocal.remove();
                    }
                }
            });
        }
        Thread.sleep(5000);
        threadLocal = null;
        while (true) {
            Thread.sleep(2000);
        }
    }
    @Data
    static class Demo {
        //
        private Demo[] demos = new Demo[1024 * 1024 * 5];
    }
}

При запуске программы журнал gc не печатается, что указывает на то, что сборка мусора не выполняется.

image-20200628020439866

image-20200628020512394

существуетJava VisualVMМы执行垃圾回收, Распределение памяти после переработки, это 20ThreadLocalDemo$Demo[]Его невозможно восстановить, что является утечкой памяти.

image-20200628020811328

Программа выполняет 50 циклов, чтобы создать 50Demo, сборка мусора не будет запускаться во время выполнения программы (гарантируется установкой параметров jvm), поэтомуThreadLocalDemo$Demo[]Количество выживших экземпляров равно50.

Когда я запускал вручнуюGC, Количество экземпляров уменьшено до 20, а не то, что мы ожидаем от 0, это программа, в которой возникла проблема с утечкой памяти.

Почему произошла утечка памяти?

Поскольку каждый поток соответствует одномуThread, размер пула потоков равен 20.ThreadимеютThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMapимеютEntry[] tables, К слаб. Когда мы threadLocal устанавливаем значение null, когда GC ROOTThreadLocalDemo$Demo[]Эталонная цепь все еще существует, просто k переработка, ценность все еще существует, длина таблиц не изменится, она не будет переработана.

image-20200628023936332

ThreadLocal вsetиgetКогда он оптимизирован для случая, когда k равно null, соответствующие таблицы [i] будут установлены в null. Таким образом, одна запись может быть переработана. Но после того, как мы установили для ThreadLocal значение null, вызов метода не может быть выполнен. Можно только ждать, пока Thread снова не вызовет что-то ещеThreadLocalоперация времениThreadLocalMapПри оценке в соответствии с условиями выполните перефразирование Карты и удалите Запись, у которой k равно нулю.

Вышеуказанные проблемы более удобны, а поток используется.线程局部变量,перечислитьremoveАктивно ясноEntryВот и все.


Эта статья написанаБлог Чжан Паньциня www.mflyyou.cn/творчество. Ее можно свободно воспроизводить и цитировать, но с обязательной подписью автора и указанием источника статьи.

При перепечатке в публичную учетную запись WeChat добавьте QR-код публичной учетной записи автора в конец статьи. Имя общедоступной учетной записи WeChat: Mflyyou