Утечка памяти с ThreadLocal

    Дамы и господа, хочу поделиться с вами знатным способом выстрелить себе в ногу, которым я снес себе одну конечность по колено, хоть и мнил себя экспертом в области concurrency-библиотеки. Но подвела меня такая простая штука, как ThreadLocal, нежданно-негаданно бесследно поглотив пару лишних гигабайт памяти сервера.

    Безусловно, памяти ваших серверов можно найти лучшее применение, чем хранение мусора. Поэтому не повторяйте мою ошибку. А именно: не стоит пытаться хранить в ThreadLocal ссылки на этот самый ThreadLocal, или на какой-то граф объектов, в конечном итоге ссылающийся на этот самый ThreadLocal.

    image


    Для начала приведу кусок кода:

    class X {
      ThreadLocal<Anchor> local = new ThreadLocal<Anchor>();
      class Anchor {
        byte[] data = new byte[1024 * 1024];
      }
      public Anchor getOrCreate() {
        Anchor res = local.get();
        if (res == null) {
          res = new Anchor();
          local.set(res);
        }
        return res;
      }
      public static void doLeakOneMoreInstance() {
        new X().getOrCreate();
      }
      public static void main(String[] args) throws Exception {
        while (true) {
          doLeakOneMoreInstance();
          System.out.println(Runtime.getRuntime().freeMemory() / 1024 / 1024 + " MB of heap left");
        }
      }
    }
    


    При каждом вызове doLeakOneMoreInstance создается новый экземпляр X, у него вызывается метод, который выставляет значение ThreadLocal, и затем ссылка на X безвозвратно теряется. Ссылка же на экземпляр ThreadLocal, созданный в конструкторе, за пределы X никогда не выходит. Казалось бы, после этого на весь созданный граф объектов внешних ссылок нет и быть не может, и они могут быть безболезненно удалены GC.

    Но не тут-то было. Стоит запустить этот код с каким-то небольшим ограничением по размеру кучи, как JVM упадет, оставив после себя лишь сообщение «java.lang.OutOfMemoryError: Java heap space», венчающее стектрейс (впрочем, приведенный класс настолько прожорлив, что и пары гигабайт ему хватит лишь на пару миллисекунд).

    Попробуйте, прежде чем читать дальше, в качестве самопроверки ответить на вопрос: как избавиться от OOM, дописав в приведенном фрагменте лишь одно ключевое слово?

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

    В чем же, собственно, проблема?

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

    Чтобы ответить на этот вопрос, нужно немного углубиться в недра ThreadLocal. Дело в том, что данные ThreadLocal-переменных хранятся не в них самих, а непосредственно в объектах Thread. Каждый Thread имеет собственный экземпляр словаря со «слабыми» ключами (аналог WeakHashMap), где в качестве ключей выступают экземпляры ThreadLocal. Когда вы просите у ThreadLocal-переменной отдать её значение, она на самом деле получает текущий поток, извлекает из него словарь, и получает значение из словаря, используя себя любимую в качестве ключа.

    Если на ThreadLocal не остается ссылок, то используемая в словаре в качестве ключа ссылка на неё благополучно зануляется, а при вставке новых элементов происходит подчистка записей, ссылающихся на удаленные GC объекты.

    В этом-то механизме и кроется проблема: в словаре внутри потока хранятся слабые ссылки на ключи, а вот на значения хранятся прямые ссылки! Если каким-то образом изнутри значения ThreadLocal (в примере — объекта типа Anchor) оказывается достижим содержащий его ThreadLocal (в примере — поскольку Anchor является не-статическим классом, в нем неявно присутствует ссылка на объект типа X, который в свою очередь ссылается на ThreadLocal), то GC не сможет нормально удалить ThreadLocal, и тот остается висеть мертвым грузом до скончания веков, вернее покуда жив поток-владелец.

    Ну и ответ на вопрос для самопроверки теперь весьма тривиален: чтобы не происходила утечка памяти, достаточно дописать ключевое слово static классу Anchor, разомкнув тем самым порочный круг ссылок.

    Надо сказать, что из описанных особенностей ThreadLocal растут ноги у еще одной неприятности: до тех пор пока поток, к которому относится значение, остается жив, никто не гарантирует удаление ассоциированного с ним значения ThreadLocal, даже если ссылка на ThreadLocal будет потеряна: дело в том, что очистка старых значений происходит только при обращениях к ThreadLocal значениям ассоциированным с этом потоком, и если поток ожидает сетевого ввода/вывода, спит или выполняет любую другую длительную операцию, ожидание может затянуться на неопределенный срок.

    Будьте внимательны с ThreadLocal, коллеги! Не кладите ссылки на ThreadLocal в них самих, не храните в них петабайты данных. Порой проще и надежнее использовать Map<Thread, Value>, чем следить за правильным использованием ThreadLocal — в этом случае вы по-крайней мере контролируете жизненный цикл ваших объектов.

    P.S. Да, и я сознательно назвал статью «утечка памяти С ThreadLocal», а не «утечка памяти В ThreadLocal»: на мой взгляд ошибка именно в подходе к использованию этого средства, сама стандартная библиотека работает безукоризненно.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 6

      +2
      По моему ошибка не в самом thread local, а в известной проблеме «утечки памяти» из за не статических внутренних классов. Хотя Вы это написали :) Но я таки бы сделал упор, что это ошибка утечки памяти с внутренними классами, а как одно из проявлений — работа с ThreadLocal.
      Ссылки по теме:
      blogs.oracle.com/olaf/entry/memory_leaks_made_easy
      habrahabr.ru/post/132500/
      P.S. А вообще статья понравилась!
        +1
        Но я таки бы сделал упор, что это ошибка утечки памяти с внутренними классами, а как одно из проявлений — работа с ThreadLocal.

        Внутренние классы — это случай, в котором проще всего словить такую утечку, и который потом очень сложно обнаружить.

        Однако в запутанных иерархиях классов легко и непринужденно можно выстрелить в ногу и без внутренних классов. К слову там, где я это впервые нашел, не было никаких внутренних классов :-)

        Пожалуй, у описанной мною проблемы ноги растут из использования словарей со слабыми ссылками на ключи, на которых построены ThreadLocal-ы.
        И при встрече в коде с ThreadLocal или WeakHashMap у программиста должен в голове раздаваться тревожный звоночек: «а что туда кладется? хорошо ли я подумал, прежде чем так сделать? не лучше ли предварительно переупаковать мои данные в какую-то простую структуру без циклических ссылок?»

        P.S. За ссылки спасибо.
        +6
        Типичный сценарий использования ThreadLocal — делать его самого static.
        Нестатичный ThreadLocal — экзотика, которая как раз и приводит к описанным проблемам.

        Кстати, вместо getOrCreate() есть ThreadLocal.initialValue().
          +4
          Блох писал: «Делайте внутренние классы нестатическими лишь при крайней необходимости...»
            0
            Как я понимаю, проблемы бы не возникло, если бы ключи внутри мапы ThreadLocalVariable -> ThreadLocalValue были бы тоже weak?
              0
              Как я понимаю, проблемы бы не возникло, если бы ключи внутри мапы ThreadLocalVariable -> ThreadLocalValue были бы тоже weak?

              Ключи там и так на WeakReference-ах сделаны, но это не помогает т.к. в примере каждый объект ThreadLocal остается видим напрямую (не-weak) из своего значения.

              Вы возможно хотели сказать, «если бы значения внутри мапы… были тоже weak»?
              В этом случае конечно утечки бы не возникало. Но и пользоваться такой мапой было бы практически невозможно — она теряла бы значения в произвольные моменты времени при сборке мусора, если нет других ссылок на значения.

            Only users with full accounts can post comments. Log in, please.