Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Утрата памяти с ThreadLocal

Anna | 1.06.2014 | нет комментариев
Дамы и господа, хочу поделиться с вами знатным методом выстрелить себе в ногу, которым я снес себе одну конечность по колено, хоть и мнил себя специалистом в области 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»: на мой взор оплошность именно в подходе к применению этого средства, сама стандартная библиотека работает идеально.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB