предисловие
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);
}
}
}
пройти через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++;
}
}
}
}
}
}
демонстрационная проверка, приведенный выше анализ
причина утечки памяти
Определяется пул потоков размером 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 не печатается, что указывает на то, что сборка мусора не выполняется.
существуетJava VisualVM
Мы执行垃圾回收
, Распределение памяти после переработки, это 20ThreadLocalDemo$Demo[]
Его невозможно восстановить, что является утечкой памяти.
Программа выполняет 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 переработка, ценность все еще существует, длина таблиц не изменится, она не будет переработана.
ThreadLocal вset
иget
Когда он оптимизирован для случая, когда k равно null, соответствующие таблицы [i] будут установлены в null. Таким образом, одна запись может быть переработана. Но после того, как мы установили для ThreadLocal значение null, вызов метода не может быть выполнен. Можно только ждать, пока Thread снова не вызовет что-то ещеThreadLocal
операция времениThreadLocalMap
При оценке в соответствии с условиями выполните перефразирование Карты и удалите Запись, у которой k равно нулю.
Вышеуказанные проблемы более удобны, а поток используется.线程局部变量
,перечислитьremove
Активно ясноEntry
Вот и все.
Эта статья написанаБлог Чжан Паньциня www.mflyyou.cn/творчество. Ее можно свободно воспроизводить и цитировать, но с обязательной подписью автора и указанием источника статьи.
При перепечатке в публичную учетную запись WeChat добавьте QR-код публичной учетной записи автора в конец статьи. Имя общедоступной учетной записи WeChat: Mflyyou