ThreadLocal & Утечка Памяти
это упоминается в нескольких сообщениях: неправильное использование ThreadLocal
вызывает утечку памяти. Я изо всех сил пытаюсь понять, как утечка памяти произойдет с помощью ThreadLocal
.
единственный сценарий, который я понял, как показано ниже:
веб-сервер поддерживает пул потоков (например, для сервлетов). Эти потоки могут создать утечку памяти, если переменные в
ThreadLocal
не удаляются, потому что темы не умирают.
в этом сценарии не упоминается "Пермь Пробел " утечка памяти. Это единственный (основной) случай использования утечки памяти?
7 ответов
истощениях PermGen в сочетании с ThreadLocal
часто вызваны утечек classloader.
пример:
Представьте себе сервер приложений, который имеет бассейн рабочих потоков.
Они будут сохранены в живых до завершения работы сервера приложений.
Развернутое веб-приложение использует static ThreadLocal
в одном из своих классов для хранения некоторых поток-локальных данных, экземпляр другой класс (назовем его SomeClass
) веб-приложения. Это делается в рабочем потоке (например, это действие происходит от HTTP-запроса).
важно:
по определению ссылка на ThreadLocal
стоимостью сохраняется до тех пор, пока "владеющий" поток не умрет или если сам ThreadLocal больше недоступен.
Если веб-приложение не понятно ссылка к ThreadLocal
на остановке, плохие вещи будут происходить:
Потому что рабочий поток обычно никогда не умрет и ссылка на ThreadLocal
статично,ThreadLocal
стоимостью еще ссылки экземпляр SomeClass
, класс веб-приложения -даже если веб-приложение было остановлено!
как следствие, веб-приложения загрузчик классов не может быть собран "мусор", что означает все классы (и все статические данные) веб-приложения оставаться загруженными (это влияет на пул памяти PermGen, а также на кучу).
Каждая итерация передислокации веб-приложения увеличит использование permgen (и кучи).
=> это утечка permgen
один из популярных примеров такого рода утечки эта ошибка in log4j (исправлено в то же время).
принятый ответ на этот вопрос, и "серьезные" журналы от Tomcat об этой проблеме вводят в заблуждение. Ключевая цитата:
по определению ссылка на ThreadLocal значение сохраняется до тех пор, пока" владеющий " поток не умрет или если сам ThreadLocal больше недоступен. [мой акцент].
в этом случае единственные ссылки на ThreadLocal находятся в статическом конечном поле класса, который теперь стал целью для GC, и ссылка из рабочих потоков. Однако ссылки из рабочих потоков на ThreadLocal являются WeakReferences!
значения ThreadLocal не являются слабыми ссылками, однако. Поэтому, если у вас есть ссылки в значения ThreadLocal для классов приложений, то они будут поддерживать ссылку на ClassLoader и предотвращать GC. Однако, если ваши значения ThreadLocal являются целыми числами или строками или другим базовым типом объекта (например, стандартная коллекция выше), тогда не должно быть проблемы (они только предотвратят GC загрузчика классов boot/system, что в любом случае никогда не произойдет).
по-прежнему рекомендуется явно очистить ThreadLocal, когда вы закончите с ним, но в случае цитируемая ошибка log4j небо определенно не падало (как вы можете видеть из отчета, значение является пустой хэш-таблицей).
вот некоторый код, чтобы продемонстрировать. Во-первых, мы создаем базовую пользовательскую реализацию classloader без родителя, который печатает в систему.выход на доработку:
import java.net.*;
public class CustomClassLoader extends URLClassLoader {
public CustomClassLoader(URL... urls) {
super(urls, null);
}
@Override
protected void finalize() {
System.out.println("*** CustomClassLoader finalized!");
}
}
затем мы определяем приложение драйвера, которое создает новый экземпляр этого загрузчика классов, использует его для загрузки класса с помощью ThreadLocal, а затем удаляет ссылку на загрузчик классов, позволяя ему быть GC'Ed. Во-первых, в случае, когда значение ThreadLocal является ссылкой на класс, загружен пользовательских загрузчиком:
import java.net.*;
public class Main {
public static void main(String...args) throws Exception {
loadFoo();
while (true) {
System.gc();
Thread.sleep(1000);
}
}
private static void loadFoo() throws Exception {
CustomClassLoader cl = new CustomClassLoader(new URL("file:/tmp/"));
Class<?> clazz = cl.loadClass("Main$Foo");
clazz.newInstance();
cl = null;
}
public static class Foo {
private static final ThreadLocal<Foo> tl = new ThreadLocal<Foo>();
public Foo() {
tl.set(this);
System.out.println("ClassLoader: " + this.getClass().getClassLoader());
}
}
}
когда мы бежим это мы видим, что CustomClassLoader действительно не является собранным мусором (так как поток, локальный в основном потоке, имеет ссылку на экземпляр Foo, который был загружен нашим пользовательским загрузчиком классов):
$ java Main ClassLoader: CustomClassLoader@7a6d084b
однако, когда мы меняем ThreadLocal, чтобы вместо этого содержать ссылку на простое целое число, а не экземпляр Foo:
public static class Foo {
private static final ThreadLocal<Integer> tl = new ThreadLocal<Integer>();
public Foo() {
tl.set(42);
System.out.println("ClassLoader: " + this.getClass().getClassLoader());
}
}
тогда мы видим, что пользовательский classloader is теперь собранный мусор (так как поток локальный в основном потоке только имеет ссылка на целое число, загружается загрузчиком системы):
$ java Main ClassLoader: CustomClassLoader@e76cbf7 *** CustomClassLoader finalized!
(то же самое верно и для Hashtable). Поэтому в случае log4j у них не было утечки памяти или какой-либо ошибки. Они уже очищали хэш-таблицу, и этого было достаточно для обеспечения GC загрузчика классов. IMO, ошибка находится в Tomcat, который без разбора регистрирует эти "серьезные" ошибки при выключении для всех ThreadLocals, которые не были явно .удалить()d, независимо от того, содержат ли они сильную ссылку в класс приложения или нет. Похоже, что по крайней мере некоторые разработчики вкладывают время и усилия в "исправление" фантомных утечек памяти на say-so из небрежных журналов Tomcat.
нет ничего изначально неправильного в локальных потоках: они не вызывают утечки памяти. Они не медлительны. Они более локальны, чем их не-локальные аналоги (т. е. они имеют лучшие свойства скрытия информации). Конечно, ими можно злоупотреблять, но так же, как и большинством других инструментов программирования...
относятся к этому ссылке Джошуа блох
предыдущие сообщения объясняют проблему, но не предоставляют никакого решения. Я обнаружил, что нет никакого способа "очистить" ThreadLocal. В контейнерной среде, где я обрабатываю запросы, я, наконец, просто позвонил .удалить() в конце каждого запроса. Я понимаю, что может быть проблематично использовать управляемые транзакции контейнера.
ниже кода, экземпляр t В для итерации не может быть GC. Это может быть примером ThreadLocal & Memory Leak
public class MemoryLeak {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
TestClass t = new TestClass(i);
t.printId();
t = null;
}
}
}).start();
}
static class TestClass{
private int id;
private int[] arr;
private ThreadLocal<TestClass> threadLocal;
TestClass(int id){
this.id = id;
arr = new int[1000000];
threadLocal = new ThreadLocal<>();
threadLocal.set(this);
}
public void printId(){
System.out.println(threadLocal.get().id);
}
}
}
вот альтернатива ThreadLocal, которая не имеет проблемы с утечкой памяти:
class BetterThreadLocal<A> {
Map<Thread, A> map = Collections.synchronizedMap(new WeakHashMap());
A get() {
ret map.get(Thread.currentThread());
}
void set(A a) {
if (a == null)
map.remove(Thread.currentThread());
else
map.put(Thread.currentThread(), a);
}
}
Примечание: существует новый сценарий утечка памяти, но это маловероятно, и можно избежать, следуя простым линией. Сценарий сохраняет сильную ссылку на объект потока в BetterThreadLocal.
Я никогда не держу сильных ссылок на потоки, потому что вы всегда хотите, чтобы поток был GC'D, когда его работа будет выполнена... так есть вы идете: без утечки памяти ThreadLocal.
кто-то должен проверить это. Я ожидаю, что это будет примерно так же быстро, как Java ThreadLocal (оба по существу делают слабый поиск хэш-карты, только один ищет поток, другой ThreadLocal).
и последнее примечание: моя система (пакета javax) также отслеживает все WeakHashMaps и регулярно очищает их, поэтому последнее супер-маловероятное отверстие заткнуто (долговечно WeakHashMaps, которые никогда не запрашиваются, но все еще имеют устаревшие записи).
утечка памяти вызвана, когда ThreadLocal всегда существует. Если ThreadLocal объект может быть GC, это не приведет к утечке памяти. Поскольку запись в ThreadLocalMap расширяет WeakReference, запись будет GC после ThreadLocal объект GC.
ниже кода создать много ThreadLocal и он никогда не утечка памяти и поток main всегда в прямом эфире.
// -XX:+PrintGCDetails -Xms100m -Xmx100m
public class Test {
public static long total = 1000000000;
public static void main(String[] args) {
for(long i = 0; i < total; i++) {
// give GC some time
if(i % 10000 == 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
ThreadLocal<Element> tl = new ThreadLocal<>();
tl.set(new Element(i));
}
}
}
class Element {
private long v;
public Element(long v) {
this.v = v;
}
public void finalize() {
System.out.println(v);
}
}