Санкт-Петербургская группа тестирования JVM


« Java по-русски | Main | "Случайный" выход из... »
20060801 вторник Август 01, 2006

Новые оптимизации компилятора VM. Часть 1: Синхронизация Новые оптимизации JIT-компилятора виртуальной машины Java HotSpot 1.6 позволяют повысить скорость выполнения Java-программ. Такие оптимизации по большей части незаметны для программиста, но знание об особенностях реализации компилятора может оказаться полезным в создании более эффективного кода. Сегодня речь пойдет о том, каким образом оптимизируется синхронизация Java-объектов.

Многие классы библиотеки Java уже заранее рассчитаны на многопоточное использование, хотя в действительности в большинстве программ объекты используются, как правило, одним потоком. Как же оптимизировать Java-программы для работы в таких сценариях? Рассмотрим это более подробно. В языке Java каждый объект является потенциальным монитором. Ключевое слово synchronized позволяет на время захватить объект, при этом гарантируется, что в каждый момент времени объект захвачен не более чем одним потоком.

synchronized(foo)
{
    // объект foo захвачен выполняющимся потоком.
}

С точки зрения реализации, одним из способов эксклюзивного захвата объекта является использование примитивов операционной системы, таких как мьютекс (mutex). Но ассоциировать с каждым Java-объектом мьютекс не является оптимальным решением, поскольку, как было отмечено выше, большинство из них вообще никогда не будет захватываться, а примитивы операционной системы занимают много ресурсов. Их лучше создавать только когда это абсолютно необходимо. Рассмотрим, как эта проблема решается в JIT-компиляторе виртуальной машины HotSpot.

Основная идея состоит в том, что в большинстве случаев захват объекта не является ожидающим, т.е. либо рассматриваемый объект до этого был свободен, либо произошел рекурсивный захват одним и тем же потоком. HotSpot позволяет выполнить такой захват с помощью легковесной блокировки (lightweight locking) или привязанной блокировки (biased locking). В противоположность этому, при тяжеловесном блокировке объекта (heavyweight locking) выполняющемуся потоку приходится ожидать освобождения соответствующего ресурса.

При легковесной блокировке, если захват происходит без ожидания, для входа и выхода из монитора используются процессорные инструкции атомарной модификации памяти типа compare-and-swap (CAS). Однако, если объект уже захвачен, то JVM переходит к использованию механизмов тяжеловесного блокировки.

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

Третий бит указывает на факт связанной блокировки, о которой речь пойдет далее.

Теперь посмотрим, что происходит при попытке захватить объект:

  1. в стеке вызывающего потока создается запись блокировки (lock record), и маркер объекта копируется в эту запись (в случае рекурсивного блокировки вместо маркера помещается 0);
  2. с помощью атомарной инструкции сравнение-обмен (CAS, у процессоров с архитектурой Intel IA32 таковой, например, является команда lock cmpxchg) делается попытка записать в маркер объекта указатель на запись блокировки;
  3. если это удалось, то объект оказывается захваченным с помощью легковесной блокировки. Его маркером теперь является указатель (два нижних бита которого всегда равны 00) на самую верхнюю запись блокировки в стеке;
  4. иначе объект был захвачен другим потоком и происходит обычная тяжеловесная блокировка объекта (с ожиданием).

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

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

Описанная оптимизация позволяет повысить производительность большинства приложений, поскольку чаще всего блокировки не только являются неожидающими, но также производятся рекурсивно одним и тем же потоком. За этот выигрыш приходится платить тем, что блокировки с ожиданием обходятся дороже. Кроме того, стоит иметь в виду, что атомарные инструкции типа CAS могут быть относительно дорогими на многопроцессорных машинах из-за необходимости синхронизации кэшей процессоров.

Привязанная блокировка позволяет учитывать относительную дороговизну атомарных инструкций на многопроцессорных машинах. Объект как бы "привязывается" к захватившему его потоку, и дальнейшие захваты тем же потоком происходят очень быстро, без использования CAS-инструкций. В данном случае просто проверяется, что объект используется привязанным к блокировке потоком.

Если объект привязан к другому потоку, то происходит операция отката привязки (bias revocation). Эта очень дорогая операция:

  1. все потоки останавливается, чтобы исключить изменения состояния блокировки во время этой операции;
  2. с помощью обхода стека перечисляются записи блокировки потока, к которому был привязан объект;
  3. все записи блокировки (если таковые были) приводятся в состояние, которое они имели бы в случае обычной легковесной блокировки;
  4. обновляется маркер объекта: указывает на самую верхнюю (на стеке) запись блокировки или отмечается как незахваченный, если записей блокировки не обнаружено;
  5. поток возобновляется, далее блокировка происходит по описанному ранее сценарию (с помощью CAS-инструкции).

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

Сначала JVM пытается определить, не ли слишком много откатов выполняется для данного типа объектов. В этом случае можно привязать сразу все объекты данного типа к новому потоку. Если, тем не менее, операции отката связывания для отдельных объектов все равно время от времени продолжают случаться, то связанная блокировка будет выключается для данного типа объектов. Эту довольно агрессивную оптимизацию можно отключить с помощью ключа командной строки -XX:-UseBiasedLocking.

А скоро мы расскажем и о других оптимизациях в JVM HotSpot 1.6: escape analysis и lock coarsening.

Смотрите также:

Игорь Привалов

опубликовал vmrobot ( авг 01 2006, 10:21:29 PM MSD ) Permalink Комментарии [2]

Trackback URL: http://blogs.sun.com/vmrobot/entry/%D0%BD%D0%BE%D0%B2%D1%8B%D0%B5_%D0%BE%D0%BF%D1%82%D0%B8%D0%BC%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B8_%D0%BA%D0%BE%D0%BC%D0%BF%D0%B8%D0%BB%D1%8F%D1%82%D0%BE%D1%80%D0%B0_vm_%D1%87%D0%B0%D1%81%D1%82%D1%8C
Комментарии:

Всё бы хорошо - только уж больно "рекламный" получился пост... Что тут можно говорить о непредвзятости, если сотрудник Sun тестировал продукты Sun, сравнивая их с конкурентами. Я, кстати, слышал про случай в компании, где я работаю, как Sun HotSpot JVM просто вылетал с OutOfMemoryError в продакшене под большой нагрузкой - GC не получал для своей работы необходимого количества памяти и просто сыпался. "Полечилось" очень просто - переход на JVM конкурентов :)

опубликовал Дмитрий Июнь 10, 2008 at 09:09 AM MSD #

упс... не туда комментарий... Браузерный back подвёл... :(

опубликовал Дмитрий Июнь 10, 2008 at 09:09 AM MSD #

Опубликовать комментарий:

Имя
E-Mail:
URL:

Ваш комментарий:

HTML Syntax: Отключен

Хиты страниц за сегодня: 39