Дамы и господа, хочу поделиться с вами знатным способом выстрелить себе в ногу, которым я снес себе одну конечность по колено, хоть и мнил себя экспертом в области concurrency-библиотеки. Но подвела меня такая простая штука, как ThreadLocal, нежданно-негаданно бесследно поглотив пару лишних гигабайт памяти сервера.
Безусловно, памяти ваших серверов можно найти лучшее применение, чем хранение мусора. Поэтому не повторяйте мою ошибку. А именно: не стоит пытаться хранить в ThreadLocal ссылки на этот самый ThreadLocal, или на какой-то граф объектов, в конечном итоге ссылающийся на этот самый ThreadLocal.
![картинка взята с http://java.about.com image](https://habrastorage.org/getpro/habr/post_images/1f9/f49/a4c/1f9f49a4c43c988e35824931c1392490.gif)
Для начала приведу кусок кода:
При каждом вызове 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»: на мой взгляд ошибка именно в подходе к использованию этого средства, сама стандартная библиотека работает безукоризненно.
Безусловно, памяти ваших серверов можно найти лучшее применение, чем хранение мусора. Поэтому не повторяйте мою ошибку. А именно: не стоит пытаться хранить в ThreadLocal ссылки на этот самый ThreadLocal, или на какой-то граф объектов, в конечном итоге ссылающийся на этот самый ThreadLocal.
![картинка взята с http://java.about.com image](https://habrastorage.org/getpro/habr/post_images/1f9/f49/a4c/1f9f49a4c43c988e35824931c1392490.gif)
Для начала приведу кусок кода:
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»: на мой взгляд ошибка именно в подходе к использованию этого средства, сама стандартная библиотека работает безукоризненно.