- PVSM.RU - https://www.pvsm.ru -
(пост из серии «я склонировал себе исходники hotspot, давайте посмотрим на них вместе»)
Все, кто сталкивается с многопоточными проблемами (будь то производительность или непонятные гейзенбаги), неизбежно сталкиваются в процессе их решения с терминами вроде «inflation», «contention», «membar», «biased locking», «thread parking» и тому подобным. А вот все ли действительно знают, что за этими терминами скрывается? К сожалению, как показывает практика, не все [1].
В надежде исправить ситуацию, я решил написать цикл статей на эту тему. Каждая из них будет построена по принципу «сначала кратко опишем, что должно происходить в теории, а потом отправимся в исходники и посмотрим, как это происходит там». Таким образом, первая часть во многом применима не только к Java, а потому и разработчики под другие платформы могут найти для себя что-то полезное.
Перед прочтением глубокого описания полезно убедиться в том, что вы в достаточной мере разбираетесь в Java Memory Model. Изучить её можно, например, по слайдам [2] Сергея Walrus [3] Куксенко или по моему раннему топику [4]. Также отличным материалом является вот эта презентация [5], начиная со слайда #38.
Как известно, каждый объект в java имеет свой монитор, и потому, в отличие от того же C++, нет необходимости guard-ить доступ к объектам отдельными mutex-ами. Для достижения эффектов взаимного исключения и синхронизации потоков используют следующие операции:
notify
. Выход из метода wait
может оказаться и ложным. После того, как поток, владеющий монитором, сделал wait
, монитором может завладеть любой другой поток.Прежде, чем продолжить, определим важное понятие:
contention — ситуация, когда несколько сущностей одновременно пытаются владеть одним и тем же ресурсом, который предназначен для монопольного использования
От того, есть ли contention на владение монитором, очень сильно зависит то, как производится его захват. Монитор может находиться в следующих состояниях:
На этом абстрактные рассуждения заканчиваются, и мы погружаемся в то, как оно реализовано в hotspot.
Внутри виртуальной машины заголовки объектов в общем случае содержат два поля: mark word и указатель на класс объекта. В частных случаях туда может что-то добавляться: например, длина массива. Хранятся эти заголовки в так называемых oop-ах (Ordinary Object Pointer), и посмотреть на их структуру можно в файле hotspot/src/share/vm/oops/oop.hpp
. Мы же более детально изучим то, что из себя представляет mark word, который описывают в файле markOop.hpp
, находящемся в той же папке. (Не обращайте внимание на наследование от oopDesc
: оно есть лишь по историческим причинам) По-хорошему, его стоит вдумчиво почитать, уделив внимание подробным комментариям, но для тех, кому это не очень интересно, ниже краткое описание того, что же в этом mark word содержится и в каких случаях. Можно ещё посмотреть вот эту [7] презентацию начиная с 90-го слайда.
Состояние | Тег | Содержимое | |||
unlocked, thin, unbiased | 01 |
|
|
|
|
locked, thin, unbiased | 00 |
|
|||
inflated | 10 |
|
|||
biased | 01 |
|
|
|
|
marked for GC | 11 |
Тут мы видим несколько новых значений. Во-первых, identity hash code — тот хеш-код объекта, который возвращается при вызове System.identityHashCode
. Во-вторых, age — сколько сборок мусора пережил объект. И ещё есть epoch, которая указывает число bulk revocation или bulk rebias для класса этого объекта. К тому, зачем это нужно, мы подойдём позже.
Вы заметили, что в случае biased не хватило места одновременно и для identity hash code и для threadID + epoch? А это так, и отсюда есть интересное следствие: в hotspot вызов
System.identityHashCode
приведёт к revoke bias объекта.
Далее, когда монитор занят, в mark word хранится указатель на то место, где хранится настоящий mark word [8]. В стеке каждого потока есть несколько «секций», в которых хранятся разные вещи. Нас интересует та, где хранятся lock record'ы. Туда мы и копируем mark word объекта при легковесной блокировке. Потому, кстати, thin-locked объекты называют stack locked. Раздутый монитор может храниться как у потока, который его раздул, так и в глобальном пуле толстых мониторов.
Пора перейти к коду.
synchronized
Начнём с такого класса:
|
|
и посмотрим, во что он скомпилируется:
javac SynchronizedSample.java && javap -c SynchronizedSample
Я не стану приводить полный листинг, а обойдусь лишь телом метода doSomething
, снабдив его комментариями.
void doSomething();
Code:
0: aload_0 // Толкаем в стек this
1: dup // Дублируем верхушку стека (this)
2: astore_1 // Записываем ссылку с верхушки стека (this) в переменную 1
3: monitorenter // Овладеваем монитором объекта, лежащего на вершине стека (this)
4: aload_1 // Снова толкаем this в стек
5: monitorexit // Отпускаем монитор
6: goto 14 // Пропускаем "catch-блок"
9: astore_2 // (Сюда мы попадаем, если случилось исключение) Записываем исключение в переменную 2
10: aload_1 // Толкаем this на верхушку стека
11: monitorexit // Отпускаем монитор
12: aload_2 // Толкаем исключение на вершину стека
13: athrow // Швыряем исключение
14: return // Конец
Exception table:
from to target type
4 6 9 any // Если происходит исключение при отпускании монитора, идём в "catch-блок"
9 12 9 any // Отпустить монитор важно, пробуем пока не получится
Здесь нас интересуют инструкции monitorenter
и monitorexit
. Можно, конечно, поискать, что они делают, в Яндекпоисковой системе на ваше усмотрение, но это чревато дезинформацией, да и не по-пацански как-то. К тому же, у меня как раз под рукой есть исходники OpenJDK, с которыми вообще можно весело поразвлекаться [9]. В этих исходниках довольно просто узреть, что происходит с байт-кодом в режиме интерпретации. Есть лишь один нюанс: Лёша TheShade [10] Шипилёв сказал [11], что
В общем случае код VM'ного хелпера для какого-нибудь действия может по содержанию отличаться от вклеенного JIT'ом. Вплоть до того, что некоторые оптимизации со стороны JIT'а могут просто не портированы в интерпретатор
Также Лёша порекомендовал взять в зубы PrintAssembly и смотреть сразу на скомпилированный и за-JIT-нутый код, но я решил начать с пути меньшего сопротивления, а потом уже посмотреть, как же оно на самом делетм
Исходники интерпретатора лежат в папочке hotspot/src/share/vm/interpreter
, и их там много. Перечитывать всё на данном этапе не очень целесообразно, потому с помощью grep
найдём места, в которых, вероятно, происходит то, что нам нужно. В первую очередь стоит глянуть на происходящие в bytecodes.hpp
и bytecodes.cpp
объявления:
./bytecodes.hpp:235: _monitorenter = 194, // 0xc2
./bytecodes.cpp:489: def(_monitorenter, "monitorenter", "b", NULL, T_VOID, -1, true);
Как легко можно догадаться, в .hpp
определяется человеческая enum-константа для байт-кода 0xc2
, а в .cpp
эта операция регистрируется с помощью метода def. Рассказывать о нём отдельно в рамках этой статьи смысла особого нет: достаточно будет пояснить, что регистрируется команда monitorenter
, представляющая собой один байт-код без параметров (b
), ничего не возвращающая, вытаскивающая из стека одно значение и способная спровоцировать блокировку или вызов safepoint (о последних позднее).
Следующим представляет интерес файл bytecodeInterpreter.cpp
. В нём есть замечательный метод BytecodeInterpreter::run(interpreterState istate)
, который занимает всего-навсего около 2200 строк, и в общем и целом крутится в цикле, пока тело обрабатываемого метода не закончится. (На самом деле, ещё большой кусок занимается другими полезными делами типа инициализации метода, блокировки, если метод synchronized
, и тому подобного). Наконец, начиная со строки 1667
описывается то, что происходит при встрече операции monitorenter
. В первую очередь, в стеке потока находится свободный монитор (если таких нет, то он запрашивается у интерпретатора с помощью istate->set_msg(more_monitors)
), и туда помещается unlocked-копия mark word. После этого с помощью CAS мы пытаемся записать в mark word объекта указатель на эту копию, которую называют displaced header.
CAS — Compare-and-swap — атомарно сравнивает
*dest
иcompare_value
, и если они равны,*dest
иexchange_value
местами. Возвращается изначальное значение*dest
. (При этом гарантируется двусторонний membar, но о них в следующей статье)
Если CAS удался, то победа (а вместе с ней и монитор) наша, и на этом можно закончить (тег содержится в самом указателе на displaced header — оптимизация). Если нет, то идём дальше, но сначала обратим внимание на важный момент: мы никак не проверили, а не biased ли этот монитор. Вспомнив Лёшины предостережения, поймём, что натолкнулись на оптимизацию, не дошедшую до интерпретатора. К слову сказать, при обработке synchronized
-методов всё проверяется нормально, но это будет несколько позже.
Если CAS не удался, то мы проверяем, не являемся ли мы уже владельцами монитора (рекурсивный захват); и если да, то успех снова за нами, единственное, что мы делаем — это записываем в displaced header у себя на стеке NULL
(дальше узнаем, зачем это нужно). В противном случае мы делаем следующий вызов:
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
Макрос CALL_VM
производит всякие технические операции типа создания фреймов и выполняет переданную функцию. В нашем случае эта функция — InterpreterRuntime::monitorenter
, которая находится в новом файлике interpreterRuntime.cpp
. Метод monitorenter
, в зависимости от того, установлено ли UseBiasedLocking
, вызывает либо ObjectSynchronizer::fast_enter
, либо ObjectSynchronizer::slow_enter
. Начнём с первого.
Сначала пара слов относительно целесообразности такой оптимизации. Некоторые токийские учёные из IBM Research Labs каким-то неизвестным мне методом подсчитали статистику и обнаружили, что на самом-то деле в большинстве случаев синхронизация uncontended. Более того, было предложено даже более сильное утверждение: мониторы большинства объектов на протяжении всей жизни захватываются лишь одним потоком. Так и родилась идея biased locking: кто первый встал, того и тапочки. Подробнее почитать про biased locking можно, например, на этих слайдах [12] (хоть они и слегка устарели), а мы вернёмся в исходники hotspot. Сейчас нас интересует файл src/share/vm/runtime/synchronizer.cpp
, начиная со строки 169. Сначала мы должны попытаться сделать rebias на себя, а если не выйдет — сделать revoke и перейти к обычному thin slow_enter. Оптимистичные попытки происходят в методе BiasedLocking::revoke_and_rebias
, находящемся в файле biasedLocking.cpp
. Опишем их поподробнее:
fast_enter
(attempt_rebias = false
).attempt_rebias
установлено в false
. Если монитором сейчас владеет другой живой поток, то rebias потребует, чтобы этот поток остановился на safepoint'е, после чего он пробежится по стеку этого потока и исправит хранящийся там там заголовок монитора на unbiased.BiasedLockingBulkRebiasThreshold
. Провоцирует смену эпохи, выполняется на глобальном safepoint.BiasedLockingBulkRevokeThreshold
. Провоцирует смену эпохи, выполняется на глобальном safepoint.Напомню тем, кто знал, но забылтм, что такое safepoint:
safepoint — состояние виртуальной машины, в котором исполнение потоков остановлено в безопасных местах. Это позволяет проводить интрузивные операции, вроде revoke bias у монитора, которым поток в данный момент владеет, деоптимизации или взятия thread dump.
Параметр attempt_rebias
в нашем случае всегда true
, однако иногда он может оказаться и false
: например, в случае, когда вызов идёт из VM Thread.
Как вы можете догадаться, bulk-операции — хитрые оптимизации, которые упрощают передачу большого числа объектов между потоками. Если бы не было этой оптимизации, то было бы опасно включать UseBiasedLocking
по умолчанию, поскольку тогда большой класс приложений вечно бы занимался revocation'ами и rebiasing'ами.
Если быстрым путём захватить поток не удалось (т.е, был сделан revoke bias), мы переходим к захвату thin-лока.
Метод, который нас теперь интересует, находится в файле src/share/vm/runtime/synchronizer.cpp
. Тут у нас есть несколько вариантов развития событий.
ObjectSynchronizer::inflate
, куда заглядывать особо внимательно мы не будем: по сути, метод потоко-безопасно и с учётом некоторых технических тонкостей устанавливает монитору флаг, что он раздут.
После раздувания монитора необходимо в него зайти. Метод ObjectMonitor::enter
делает именно это, применяет все мыслимые и немыслимые хитрости, чтобы избежать парковки потока. В число этих хитростей входят, как вы уже могли догадаться, попытки захватить с помощью spin loop'а, с помощью однократных CAS-ов и прочих «халявных методов». Кстати, кажется, я нашёл небольшое несоответствие комментариев с происходящим. вот мы один раз пытаемся войти в монитор spin loop'ом, утверждая, что это делаем лишь однажды:
|
|
А вот чуть дальше, в вызываемом методе enterI
делаем это снова, опять говоря про лишь один раз:
|
|
Мда, парковка на уровне операционной системы — это настолько страшно, что мы готовы почти на всё, чтобы её избежать. Давайте разберёмся, что же в ней такого ужасного.
Должен заметить, что мы сейчас подошли к коду, который писали очень давно, и это заметно. Есть много дубликации, переинжениринга и прочих приятностей. Впрочем, наличие комментариев типа «убрать этот костыль» и «объединить эти с тем» слегка успокаивают.
Итак, что же такое парковка потоков? Все наверняка слышали, что у каждого монитора есть так называемый Entry List (не путать с Waitset) Так вот: он действительно есть, хотя он и является на самом деле очередью. После всех провалившихся попыток дёшево войти в монитор, мы добавляем себя именно в эту очередь, после чего паркуемся:
|
|
Прежде чем перейти непосредственно к парковке, обратим внимание на то, что тут она может быть timed или не timed, в зависимости от того, является ли текущий поток ответственным. Ответственных потоков всегда не более одного, и они нужны для того, чтобы избежать так называемого stranding'a: печальки, когда монитор освободился, но все потоки в wait set по-прежнему запаркованы и ждут чуда. Когда есть ответственный, он автоматически просыпается время от времени (чем больше раз произошёл futile wakeup — пробуждение, после которого захватить лок не удалось — тем больше время парковки. Обратите внимание, что оно не превышает 1000 мсек) и пытается войти в монитор. Остальные потоки могут ждать пробуждения хоть целую вечность.
Теперь настала пора перейти к самой сути парковки. Как вы уже поняли, это что-то, по семантике похожее на знакомые каждому java-разработчику wait/notify
, однако происходит на уровне операционной системы. Например, в linux и bsd, как и можно было ожидать, используются POSIX threads, у которых для ожидания освобождения монитора вызываются pthread_cond_timedwait
(или pthread_cond_wait
). Эти методы меняют статус линуксового потока на WAITING и просят шедулер системы разбудить их, когда произойдёт некоторое событие (но не позже, чем через какой-то промежуток времени, если данный поток ответственный).
Что ж, самое время забраться в ядро linux и посмотреть, как там работает шедулер. Исходники linux, как известно, лежат в git, и склонировать шедулер можно так:
git clone git://git.kernel.org/pub/scm/linux/kernel/git/rostedt/linux-rt.git
Отправимся в папку… Ладно, ладно, шучу. Особенности устройства шедулера в линуксе с точностью до строк в исходном коде — это уже слишком глубоко для безобидной статьи, которая, позвольте напомнить, начиналась с простенького synchronized
-блока :) В целях понижения хардкорности расскажу в общих чертах, как вообще работают шедулеры; как в linux, так и в альтернативных системах. Если кто-то вдруг не понимает, зачем он вообще нужен, то поясню: шедулер — такая сущность, которая делит процессорное время между потоками. Одно из основных понятий — quantum — есмь время, которое выделяется потоку на то, чтобы выполнить то, что ему нужно. В linux это время сильно зависит от многих факторов, но обычно умещается в 10-200 тиков, где тик как правило равен 1 мс. В windows это тоже много от чего зависит, но это может быть 2-15 тиков, где тик — от 10 до 15 мс.
Конечно, не стоит думать, что поток будет исполняться ровно столько времени, сколько ему отведено. Он может сам решить, что сделал всё, что хотел (например, заблокироваться на каком-нибудь I/O-вызове), либо его может насильно выдернуть раньше времени шедулер, отдав остаток его кванта кому-нибудь другому. А может и наоборот решить продлить квант по какой-нибудь причине. Кроме того, у потоков есть приоритет, который, в общем-то, понятно, что делает.
Теперь вы, должно быть, поняли, что парковка — дорого. Мало того, что это системный вызов, так ещё и оказывается, что шедулер может распарковать поток заметно позже, чем вам бы хотелось, поскольку в системе может быть ещё куча потоков, которые шедулер решит исполнять вместо вашего.
Но и это, кстати, ещё не всё: когда процессору на исполнение отдаётся другой поток, происходит смена контекста — тоже довольно дорогая операция, которая может занимать до десятка микросекунд. Более того: каким бы невероятным это не могло казаться, разные потоки как правило интересуют разные данные, потому в кеше может оказаться что-то, что этому потоку совершенно не нужно.
Если смена контекста происходит часто, а потоки работают небольшой промежуток времени, может оказаться, что процессор загружен техническими операциями. Такое может быть, если высок contention, но все воюющие хотят владеть монитором лишь непродолжительный промежуток времени.
В случае с biased locking мы, в общем-то, ничего и не делаем. Мы обнаруживаем, что в displaced header хранится NULL, и просто выходим. Отсюда интересный момент: при попытке отпустить не занятый в данный момент biased lock интерпретатор не выкенет IllegalMonitorStateException
(но за такими вещами следит верификатор байт-кода).
В случае с unbiased locking мы делаем вызов к InterpreterRuntime::monitorexit
. После нескольких проверок (например, что монитор действительно заблокирован: в противном случае швыряется IllegalMonitorStateException
) вызывается ObjectSynchronizer::slow_exit
, который только и делает, что вызывает fast_exit
. Если будете читать исходники, не обращайте внимание на комментарий про fast path. В этом методе возможны следующие варианты развития событий: монитор находится в состоянии stack-locked, inflating или inflated. В первом случае всё просто: возвращаем заголовок объекта в то состояние, в котором оно было до блокировки, и выходим. Во втором случае дожидаемся того, как кто-то закончит раздувать монитор и перейдём к случаю третьему.
В третьем случае мы отпускаем лок и выставляем мембары, после чего смотрим, нет ли сейчас какого-нибудь распаркованного потока, который готов прямо сейчас забрать лок. Такое возможно, если он проснулся и пытается захватить монитор с помощью, например, TrySpin
(см. выше). Если такой обнаруживается, то наша работа на этом завершена. Также она завершена, если очередь потоков, которые хотят получить лок, пуста.
Если же такого потока нет, то, в зависимости от политик (выставляются с помощью Knob_QMode
. Я, честно сказать, ни нашёл ни одного места, где его значение меняется с 0
, выставленного по умолчанию. Знающие людитм, однако, подсказывают, что это, скорее всего, остатки отладки и тюнинга), выбираем, кого будить первого. Это могут оказаться потоки, которые просыпались недавно, или наоборот те, что просыпались более давно. После ещё небольшой цепочки вызовов мы попадаем в платформо-зависимый код os::PlatformEvent::unpark()
, который и говорит нужному потоку сигнал. Например, в linux и bsd используется pthread_cond_signal
.
Собственно, если очень сильно не вдаваться в детали, то это всё, что можно сказать об освобождении монитора.
synchronized
-методыЕсли бы мы написали наш изначальный java-код вот так вот:
synchronized void doSomething() {
// Do something
}
то байт-код у такого метода заметно короче:
synchronized void doSomething();
Code:
0: return
В bytecodeInterpreter.cpp
synchronized
-методы обрабатываются, начиная со строки 767
. Там есть проверка if (METHOD->is_synchronized())
. Внутри у этого условия находится огромная куча if
-ов, связанных с biased locking. Это как раз то, чего внезапно не оказалось при обработке операции monitorenter
. В общем и целом, происходит то, что мы раньше обсуждали, однако тут всё же есть быстрый (без CAS) захват biased монитора потоком-владельцем.
Также, после окончания выполнения тела метода идёт выход из монитора, если метод synchronized.
Обработка этих методов в итоге попадает в synchronizer.cpp
, начиная с 377 строки. Монитор в wait/notify в интерпретаторе обязательно должен быть inflated, потому первое, что эти методы делают — inflate'ят его. После того вызывают у него методы wait
или notify
.
Первый добавляет себя в wait set (на самом деле очередь) и паркуется до тех пор, пока ему не пора будет просыпаться (прошло время, которое просили подождать; произошло прерывание или кто-то вызвал notify).
Notify
же вытаскивает из wait set один поток и добавляет его, в зависимости от политик, в какое-то место в очереди тех, кто хочет захватить монитор. NotifyAll
отличается лишь тем, что вытаскивает из wait set всех.
Те, кто знаком с JMM, знают, что освобождение монитора happens-before захват того же самого монитора. В случае thin это гарантируется CAS-ами; в случае inflated это гарантируется явными вызовами OrderAccess::fence();
. Если же монитор biased, то значит, что им пользуется только один поток: у него исполнение гарантируется в program order и без того. При revoke HB появляется либо во время monitorexit, если поток был жив, (который оказывается уже либо thin, либо inflated), либо при enter (который тоже оказывается либо thin, либо inflated).
Прямо перед выходом из wait выставляется явный fence, чтобы прогарантировать HB.
На самом делетм, всё происходит не так, как мы думаем. Например, когда JIT компилирует наш код в нативный. Или когда мы работаем с другими виртуальными машинами. Впрочем, никто не может прогарантировать, что и в простом случае «интерпретатор в hotspot» всё действительно так, как я тут написал.
В следующих сериях, в первую очередь, необходимо рассказать о memory barriers, которые крайне важны для обеспечения happens-before в JMM. Их очень удобно рассматривать на примере volatile
полей, что я в дальнейшем и сделаю. Также стоит обратить внимание на final-поля и безопасную публикацию, но их уже осветили TheShade [10] и cheremin [13] в своих [14] статьях [15], потому их и можно почитать интересующимся почитать (только осторожно [16]). И, наконец, можно ждать наполненный PrintAssembly рассказ о том, как оно всё отличается, когда в дело вступает JIT.
Желающим повторить путешествие: я пользовался ревизией 144f8a1a43cb [17] из jdk7u. Если ваша ревизия отличается, то могут отличаться и номера строк — К.О.
Biased locking включается не сразу после запуска виртуальной машины, а спустя BiasedLockingStartupDelay
миллисекунд (4000 по умолчанию). Это сделано, поскольку иначе в процессе запуска и инициализации виртуальной машины, загрузки классов и всего прочего появилось бы огромное число safepoints, вызванных постоянным revoke bias у живых объектов.
На всех safepoint вызывается метод ObjectSynchronizer::deflate_idle_monitors
, по названию которого очень легко осознать, что он делает.
Большое спасибо доблестным TheShade [10], artyushov [18], cheremin [13] и AlexeyTokar [19] за то, что они (вы|про)читали статью перед публикацией, убедившись тем самым, что я не принесу в массы вместо света какую-то бредовню, наполненную тупыми шутками и очепатками.
Автор: gvsmirnov
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/8373
Ссылки в тексте:
[1] не все: http://habrastorage.org/storage2/f85/465/236/f854652367b67ade08c136b23451640b.jpg
[2] слайдам: http://shipilev.net/pub/talks/j1-April2012-jmm.pdf
[3] Walrus: http://habrahabr.ru/users/walrus/
[4] раннему топику: http://habrahabr.ru/post/133981/
[5] презентация: http://shipilev.net/pub/talks/j1-April2011-performanceBoF.pdf
[6] CAS: http://en.wikipedia.org/wiki/Compare-and-swap
[7] эту: http://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf
[8] в mark word хранится указатель на то место, где хранится настоящий mark word: http://habrastorage.org/storage2/714/0f5/88b/7140f588b48ef6e185295bb614a3bb6a.jpg
[9] с которыми вообще можно весело поразвлекаться: http://habrahabr.ru/post/142447/
[10] TheShade: http://habrahabr.ru/users/theshade/
[11] сказал: http://shipilev.net/pub/talks/oracle-May2011-concurrency.pdf
[12] этих слайдах: http://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf
[13] cheremin: http://habrahabr.ru/users/cheremin/
[14] своих: http://habrahabr.ru/post/143390/
[15] статьях: http://cheremin.blogspot.com/2012/05/unsafe-publication.html
[16] только осторожно: https://twitter.com/#!/shipilev/status/199554836251951106
[17] 144f8a1a43cb: http://hg.openjdk.java.net/jdk7u/jdk7u/hotspot/rev/144f8a1a43cb
[18] artyushov: http://habrahabr.ru/users/artyushov/
[19] AlexeyTokar: http://habrahabr.ru/users/alexeytokar/
Нажмите здесь для печати.