Как стать автором
Обновить
0

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

Время на прочтение3 мин
Количество просмотров19K
Дамы и господа, хочу поделиться с вами знатным способом выстрелить себе в ногу, которым я снес себе одну конечность по колено, хоть и мнил себя экспертом в области 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»: на мой взгляд ошибка именно в подходе к использованию этого средства, сама стандартная библиотека работает безукоризненно.
Теги:
Хабы:
Всего голосов 35: ↑33 и ↓2+31
Комментарии6

Публикации

Информация

Сайт
maxifier.ru
Дата регистрации
Численность
51–100 человек
Местоположение
США

Истории