This content originally appeared on DEV Community and was authored by faangmaster
Утечки памяти (Memory Leaks) в Java — это ситуации, когда объекты или метаданные классов больше не используются приложением, но сборщик мусора не может их удалить. В результате память расходуется неэффективно, что приводит к падению производительности и в крайних случаях — к ошибке OutOfMemoryError.
Существует множество сценариев, которые могут к этому привести. В этой статье я рассмотрю лишь пару примеров, связанных с ThreadLocal в Java.
Конструкция ThreadLocal позволяет хранить данные, которые будут доступны только конкретному thread-у.
Например,
public class Main {
private static final ThreadLocal<Integer> counter =
ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 3; i++) {
int current = counter.get();
counter.set(current + 1);
System.out.printf("%s -> %d%n",
Thread.currentThread().getName(), counter.get());
}
counter.remove();
};
Thread t1 = new Thread(task, "Worker-1");
Thread t2 = new Thread(task, "Worker-2");
t1.start();
t2.start();
t1.join();
t2.join();
}
}
такой код напечатает:
Worker-2 -> 1
Worker-1 -> 1
Worker-2 -> 2
Worker-2 -> 3
Worker-1 -> 2
Worker-1 -> 3
Может напечатать в разном порядке, но каждый поток (Worker) будет считать независимо от другого начиная с 1. Эти потоки не будут влиять друг на друга через переменную counter.
Как ThreadLocal работает внутри
У всех объектов класса Thread в Java есть поле
ThreadLocal.ThreadLocalMap threadLocals;
Ключ в этой мапе – объект класса ThreadLocal (наша ThreadLocal переменная counter), а значение – значение нашей ThreadLocal переменной для этого потока.
Таким образом каждый поток может менять значения этой переменной не пересекаясь с другими потоками.
ThreadLocal и Thread Pools Java Heap Leaks
В Java можно использовать пулы долгоживущих потоков. Например, создать пул вручную:
ExecutorService workers = Executors.newFixedThreadPool(4);
Либо использовать пул потоков, который предоставляет контейнер приложений (Tomcat, GlassFish, Spring Boot со встроенным Tomcat). В этом случае каждый запрос к вашему приложению обрабатывается в отдельном потоке, при этом потоки не создаются каждый раз с нуля — они переиспользуются из пула.
Т.к. потоки живут долго, то важно правильно использовать ThreadLocal в связке в такими пулами.
Если не вызывать метод remove для ThreadLocal переменных, то это может привести к утечкам памяти.
Например, рассмотрим код:
public class Main {
private static final ExecutorService workers = Executors.newFixedThreadPool(4);
private static final ThreadLocal<byte[]> state = new ThreadLocal<>();
public static void main(String[] args) {
//Using this thread pool
for (int i = 0; i < 100_000; i++) {
workers.submit(() -> {
state.set(some long payload);
......//do some work
});
}
//Doing some other work
.......
//Shutdown of thread pool
workers.shutdown();
}
}
В данном случае у нас пул из четырёх потоков. В него мы отправляем 100 000 задач. В каждой задаче мы переопределяем значение ThreadLocal-переменной state в контексте одного из этих четырёх потоков. После того как мы закончили отправлять задачи и даже после того, как все они завершились, сами потоки продолжают жить. Соответственно, у каждого потока по-прежнему есть ссылка на значение вашей ThreadLocal-переменной. Даже если дальше в приложении этот ThreadPool или сама переменная уже не используется, потоки всё равно будут ссылаться на очень объёмные объекты, которые не будут собираться GC и будут ухудшать производительность приложения.
Поэтому после завершения каждой задачи нужно вызывать метод remove на ThreadLocal переменной:
public class Main {
private static final ExecutorService workers = Executors.newFixedThreadPool(4);
private static final ThreadLocal<byte[]> state = new ThreadLocal<>();
public static void main(String[] args) {
//Using this thread pool
for (int i = 0; i < 100_000; i++) {
workers.submit(() -> {
try {
state.set(some long payload);
......//do some work
} finally {
state.remove();
}
});
}
//Doing some other work
.......
//Shutdown of thread pool
workers.shutdown();
}
}
Добавление:
try {
.....
} finally {
state.remove();
}
позволяет отвязать значения переменной от объектов долгоживущих потоков, благодаря чему GC сможет их собрать.
ThreadLocal и Web Containers and Metaspace Memory Leaks
Давайте рассмотрим другой пример:
public final class MyState {
private final byte[] payload = new byte[10 * 1024 * 1024]; // 10MB для наглядности
.....
}
.....
Веб-фильтр - выполняется на каждом запросе к контейнеру приложения (например, в Томкате)
@WebFilter("/*")
public class MyFilter implements Filter {
private static final ThreadLocal<MyState> state = new ThreadLocal<>();
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
state.set(new MyState());
....
chain.doFilter(req, res);
}
}
В этом кейсе у нас есть веб-приложение, которое работает в контейнере (например, Tomcat). Есть веб-фильтр, который выполняется на каждый входящий запрос. Выполнение происходит в одном из потоков пула, предоставляемого контейнером приложений. В таком случае в качестве значения переменной ThreadLocal используется объект класса MyState.
Более того, в контейнере приложений существуют WebappClassLoader’ы: у каждого приложения внутри контейнера — свой, чтобы классы разных приложений не пересекались.
Если при redeploy приложения не перезапускать весь контейнер (просто положили новый JAR/WAR или нажали в админке redeploy), контейнер создаёт новый WebappClassLoader, а затем удаляет старый. Но т.к. у нас пул поток все еще живет, эти потоки все еще держат ссылки на MyState объекты. Более того, будет жив старый class definition MyClass. А он в свою очередь будет держать старую версию WebappClassLoader. Чем больше таких redeploy мы сделаем, тем больше старых версий WebappClassLoader у нас будет. Это все приводит к утечкам не только в Java Heap, но и в Metaspace. И можно получить
java.lang.OutOfMemoryError: Metaspace
Для фикса нам также нужно добавить:
try {
.....
} finally {
state.remove();
}
This content originally appeared on DEV Community and was authored by faangmaster