Подтвердить что ты не робот

ThreadLocal и утечка памяти

Это упоминается в нескольких сообщениях: неправильное использование ThreadLocal вызывает утечку памяти. Я пытаюсь понять, как произойдет утечка памяти с помощью ThreadLocal.

Единственный сценарий, который я выяснил ниже:

Веб-сервер поддерживает пул потоков (например, для сервлетов). Эти потоки могут создавать утечку памяти, если переменные в ThreadLocal не удаляются, потому что потоки не умирают.

В этом сценарии не упоминается утечка памяти "Пермского пространства". Это единственный (основной) случай использования утечки памяти?

4b9b3361

Ответ 1

Исключения PermGen в сочетании с ThreadLocal часто вызваны утечками загрузчика классов.

Пример:
Представьте себе сервер приложений, у которого есть пул рабочих потоков.
Они будут сохранены до тех пор, пока сервер приложений не завершит работу. Развернутое веб-приложение использует static ThreadLocal в одном из своих классов, чтобы хранить некоторые локальные данные потока, экземпляр другого класса (позволяет называть его SomeClass) веб-приложения. Это делается внутри рабочего потока (например, это действие происходит из запроса HTTP).

Важно:
По определению ссылка на значение ThreadLocal сохраняется до тех пор, пока нить "владеющего" не умрет или если сам ThreadLocal больше не доступен.

Если веб-приложение не сможет очистить ссылку до ThreadLocal при завершении работы, произойдут плохие вещи:
Поскольку рабочий поток обычно никогда не умирает, а ссылка на ThreadLocal статична, значение ThreadLocal все еще ссылается на экземпляр SomeClass, класс веб-приложения - , даже если веб-приложение было остановлено!

Как следствие, веб-приложение загрузчик классов не может быть собрано с мусором, что означает, что все классы ( и все статические данные) веб-приложения остаются загруженными (это влияет на пул памяти PermGen, а также на кучу).
Каждая итерация повторного развертывания веб-приложения будет увеличивать использование permgen (и кучи).

= > Это утечка пергентов

Один из популярных примеров такого утечки - эта ошибка в log4j (исправлена ​​тем временем).

Ответ 2

Принятый ответ на этот вопрос и "суровые" журналы Tomcat об этой проблеме вводят в заблуждение. Ключевая цитата:

По определению ссылка на значение ThreadLocal сохраняется до тех пор, пока не исчезнет нить "владеющего" или если сам ThreadLocal больше не доступен. [Мой акцент].

В этом случае единственные ссылки на ThreadLocal находятся в статическом конечном поле класса, который теперь стал мишенью для GC и ссылкой из рабочих потоков. Однако ссылки из рабочих потоков на ThreadLocal WeakReferences!

Однако значения ThreadLocal не являются слабыми ссылками. Итак, если у вас есть ссылки в значениях классов ThreadLocal для приложений, то они будут поддерживать ссылку на ClassLoader и предотвращать GC. Однако, если ваши значения ThreadLocal являются целыми числами или строками или некоторым другим базовым типом объекта (например, стандартным набором из вышеперечисленного), тогда не должно быть проблемы (они будут только предотвращать GC загрузочного/системного загрузчика классов, что никогда не произойдет в любом случае).

По-прежнему хорошей практикой является явная очистка ThreadLocal, когда вы закончите с ней, но в случае указанной ошибки log4j небо, безусловно, не падало (как вы можете видеть из отчета, значение - пустой Hashtable).

Вот какой код для демонстрации. Во-первых, мы создаем базовую пользовательскую реализацию загрузчика классов без родителя, который печатает в System.out при завершении:

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: [email protected]

Однако, когда мы меняем ThreadLocal вместо ссылки на простой Integer, а не экземпляр 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());
    }
}

Затем мы видим, что пользовательский загрузчик классов теперь собрал мусор (поскольку поток, локальный в основном потоке, имеет только ссылку на целое число, загруженное системным загрузчиком):

$ java Main
ClassLoader: [email protected]
*** CustomClassLoader finalized!

(То же самое происходит с Hashtable). Поэтому в случае log4j у них не было утечки памяти или какой-либо ошибки. Они уже очищали Hashtable, и этого было достаточно для обеспечения GC загрузчика классов. IMO, ошибка находится в Tomcat, которая без разбора регистрирует эти ошибки "SEVERE" при выключении для всех ThreadLocals, которые явно не были .remove() d, независимо от того, содержат ли они сильную ссылку на класс приложения или нет. Похоже, что, по крайней мере, некоторые разработчики тратят время и силы на "исправление" phantom утечек памяти, скажем, на неаккуратные журналы Tomcat.

Ответ 3

В локаторах потоков нет ничего естественного: они не вызывают утечки памяти. Они не медленны. Они более локальные, чем их не-потоковые локальные копии (т.е. Они обладают лучшими свойствами скрытия информации). Разумеется, они могут быть неправильно использованы, но так могут большинство других программных средств...

Обратитесь к этой ссылке от Джошуа Блоха

Ответ 4

Предыдущие сообщения объясняют проблему, но не предоставляют никакого решения. Я обнаружил, что нет возможности "очистить" ThreadLocal. В контейнерной среде, где я обрабатываю запросы, я, наконец, только что вызвал .remove() в конце каждого запроса. Я понимаю, что это может быть проблематично с помощью транзакций, управляемых контейнерами.

Ответ 5

Ниже кода, экземпляр 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);
        }
    }
}

Ответ 6

Вот альтернатива 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);
  }
}

Примечание. Существует новый сценарий утечки памяти, но он маловероятен и его можно избежать, следуя простой направляющей. Сценарий поддерживает сильную ссылку на объект Thread в BetterThreadLocal.

Я никогда не буду ссылаться на потоки, потому что вы всегда хотите, чтобы поток был GC'd, когда его работа была выполнена... так что вы идете: безтекстовая память ThreadLocal.

Кто-то должен проверить это. Я ожидаю, что это будет примерно так же быстро, как Java ThreadLocal (как в основном, так и в случае слабого поиска хэш-карт, только один ищет поток, другой - ThreadLocal).

Пример программы в JavaX.

И последнее примечание: моя система (JavaX) также отслеживает все файлы WeakHashMaps и регулярно очищает их, поэтому последний супер-маловероятное отверстие подключено (долговечные WeakHashMaps, которые никогда не запрашиваются, но все еще имеют устаревшие записи).