23 апреля 2012 в 07:37

Как собрать свою JDK, без блекджека и автоматической сборки мусора tutorial

На недавно прошедшей Java One Руслан cheremin рассказывал о том, что разработчики Disruptor используют JVM без сборщика мусора. У них на то были свои причины, которые не имеют к этому топику никакого отношения.

Я же давно хотел поковыряться в исходниках виртуальной машины, и выпиливание из неё GC – отличное начало. Под катом я расскажу вам, как собрать OpenJDK, выпилить из неё сборщик мусора и снова собрать. Ближе к концу даже будет дан ответ на наверняка пришедший вам в голову вопрос «зачем».



Исходники? Дайте два побольше и посыпьте бинарниками!


Основное блюдо


OpenJDK хранится в mercurial с использованием forest, и самый простой способ заполучить код – сказать

$ hg fclone http://hg.openjdk.java.net/jdk7/jdk7

Если не установлено расширение forest и устанавливать его вы почему-то не хотите, можно сделать и так:

$ hg clone http://hg.openjdk.java.net/jdk7/jdk7 && jdk7/get_source.sh


Ещё один вариант — скачать полные бандлы с оффсайта. Это поможет срезать пару углов, но лишит прелестей использования системы контроля версий.

Интересная особенность: по некоторым причинам, jaxp и jaxws хранятся в отдельном репозитории. Потому их нужно либо вручную скачать с соответствующих сайтов ( jaxp.java.net/ и jax-ws.java.net/ ), либо просто разрешить make скачивать всё необходимое самостоятельно, сказав ALLOW_DOWNLOADS=true. Лично мне такой вариант кажется удобнее. Ах, да, в полных бандлах исходников всё уже скачано за нас.

Инструменты, без которых блюдо не приготовить


Понятное дело, для сборки потребуется много всего. Самое простое — это bootstrap jdk, как минимум версии 1.6. Нужно указать к ней путь через переменную ALT_BOOTDIR. Кроме того, требуется огромная куча всего, начиная от очевидных ant и make и заканчивая CUPS и ALSA. Самый простой способ иметь точно всё — это попросить свой пакетный менеджер удовлетворить все зависимости сборки. Например, с помощью aptitude:

$ aptitude build-dep openjdk-6


Проверяем, что собирается

Для того, чтобы убедиться, что всё необходимое есть, нужно запустить make с целью sanity. Обратите внимание на выставление переменных окружения:

$ LANG=C ALT_BOOTDIR=/usr/lib/jvm/java-6-openjdk make sanity


Если всё хорошо, то вы увидите надпись Sanity check passed

Если всё плохо, то вы получите довольно вразумительное сообщение об ошибке. Исправьте её и попробуйте ещё раз.

Теперь можно собрать саму jdk. К переменным среды добавилась указанная ранее ALLOW_DOWNLOADS.
$ ALLOW_DOWNLOADS=true LANG=C ALT_BOOTDIR=/usr/lib/jvm/java-6-openjdk make


В случае успеха минут через 20-40 вы получите сообщение вида

#-- Build times ----------
Target all_product_build
Start 2012-04-20 01:56:53
End   2012-04-20 02:02:14
00:00:06 corba
00:00:09 hotspot
00:00:06 jaxp
00:00:08 jaxws
00:04:47 jdk
00:00:05 langtools
00:05:21 TOTAL


Можно проверить, что действительно собралось что-то полезное, и перейти к следующему шагу.

$ ./build/linux-amd64/bin/java -version
openjdk version "1.7.0-vasily_p00pkin"
OpenJDK Runtime Environment (build 1.7.0-vasily_p00pkin-gs_2012_04_20_01_06-b00)
OpenJDK 64-Bit Server VM (build 23.0-b21, mixed mode)


У меня альтернативная операционная система...


… Основанная на BSD


Тут всё не так уж и плохо. Под чутким руководством добрых сотрудников Oracle мне удалось собрать hotspot на макбуке в тамбуре Сапсана. А вот всю JDK на следующую ночь уже не очень-то и вышло. Однако сделать это можно, нужно только иметь свежий XCode и много терпения. У меня не оказалось ни того ни другого, и потому я просто завёл машинку помощней в облаке Селектела и проводил эксперименты на ней. В качестве бонуса, сборка в облаке проходит быстрее, при этом никак не нагружая мой ноут, и потому я могу в это время поделать что-то полезное (вместо того, чтобы сражаться на мечах, катаясь на стульях). Если вы по-прежнему хотите собирать на маке, то вот тут есть описание процесса.

… Ну вы поняли, да?


Тут, на самом деле, тоже не всё так плохо. Вооружайтесь cygwin и курите маны.

Начало самого интересного

— Пациент, вы страдаете извращениями?
— Что вы, доктор! Я ими наслаждаюсь!

Теперь перед нами встала задача понять, где в исходниках нужно поколдовать, чтобы выпилить сборщик мусора. Есть три очевидных способа это сделать: спросить у кого-то, кто знает, прочитать все исходники или проявить хитроумие и находчивость. Первый способ не вышел, потому что знающие люди на меня странно смотрели и отодвигались подальше, отказываясь участвовать в столь сомнительных акциях. На второй способ, хоть с идеологической точки зрения он и самый правильный, было жалко времени. Потому остался третий способ.

Давайте рассуждать логически: как кто-то может повлиять на сборщик мусора извне? В голову сразу приходит два пути: с помощью ключиков при запуске (вроде -XX:+UseParallelGC) и с помощью System.gc(). И хотя первый кажется более логичным, я решил всё-таки начать со второго, потому что javadocs не могут полностью удовлетворить интерес относительно того, что же там именно происходит. В java-исходниках этот вызов делегируется в Runtime, где метод уже нативен. Все, кто хоть раз работал с JNI, знают, как составляются имена функций в нативном коде: Java_java_lang_Runtime_gc. Быстрый grep наталкивает на такой код в jdk/src/share/native/java/lang/Runtime.c, в котором нас интересуют следующие строки:
62
63
64
65
66

JNIEXPORT void JNICALL
Java_java_lang_Runtime_gc(JNIEnv *env, jobject this)
{
    JVM_GC();
}

Понятно, теперь ищем JVM_GC. Не менее быстро находим его объявление в src/share/vm/prims/jvm.cpp:
404
405
406
407
408
409


JVM_ENTRY_NO_ENV(void, JVM_GC(void))
   JVMWrapper("JVM_GC");
   if (!DisableExplicitGC) {
     Universe::heap()->collect(GCCause::_java_lang_system_gc);
   }
JVM_END
Тут мы видим аж два очень интересных момента: первый — DisableExplicitGC, который не нуждается в комментариях и метод collect у Universe::heap(). Как всё просто: оказывается, System.gc() только и делает, что синхронно запускает сборщик. Никакой драмы. Эх. Ну да ничего, зато теперь мы знаем, что, скорее всего, в методе collect() можно запретить сборку. С лёгкостью обнаруживаем класс Universe в файле hotspot/src/share/vm/memory/universe.hpp и замечаем, что статический метод heap возвращает CollectedHeap*, а так же наличие метода initialize_heap()

Маленькое лирическое отступление на тему Вселенной


Должен сказать, что качество кода в OpenJDK отменно: хорошая структура, легко понять, что происходит, много комментариев. Вот, например, отличный сниппет:
121
122
123
124
125
126
127


class Universe: AllStatic {
  // Ugh.  Universe is much too friendly.
  friend class MarkSweep;
  friend class oopDesc;
  // Ещё куча friend'ов
  //...
}

Ладно, вернёмся к нашему сборщику. Метод initialize_heap() создаёт кучу, причём в зависимости от того, какой сборщик указал пользователь, выбирается какая-то определённая её реализация. Полный список можно найти в файле hotspot/src/share/vm/gc_interface/collectedHeap.hpp:

192
193
194
195
196
197
198


enum Name {
  Abstract,
  SharedHeap,
  GenCollectedHeap,
  ParallelScavengeHeap,
  G1CollectedHeap
};

Продолжая исследование класса, наконец наталкиваемся на нужный код:

519
520
521
522
523
524
525
526
527
528


// Perform a collection of the heap; intended for use in implementing
// "System.gc".  This probably implies as full a collection as the
// "CollectedHeap" supports.
virtual void collect(GCCause::Cause cause) = 0;

// This interface assumes that it's being called by the
// vm thread. It collects the heap assuming that the
// heap lock is already held and that we are executing in
// the context of the vm thread.
virtual void collect_as_vm_thread(GCCause::Cause cause) = 0;

Тут для нас наиболее полезны комментарии. Для тех, кто недостаточно хорошо знает английский, разъясню: первый метод, просто collect(), предназначен для сборки «извне» (например, из System.gc или, как показывает всё тот же grep, при неудачной аллокации памяти в linux). Второй же запускается из потока виртуальной машины, который отвечает за сборку мусора (и предполагается, что уже держатся все необходимые локи). На ум сразу приходит простое решение: сделать так, чтобы при вызове этих методов сборка не происходила. Я даже первый раз попробовал именно этот подход, только вот ведь незадача: оказывается, всё несколько сложнее, и у каждой реализации кучи есть свои дополнительные места, в которых происходит сборка. Потому пришлось выбрать какую-то конкретную реализацию ( GenCollectedHeap с MarkSweepPolicy как самую простую), и у неё в зависимости от флага (который я обозвал UseTheForce) выходить из методов, производящих сборку, ничего не делая. В итоге изменения в первой версии произошли вот такие.

Пробуем!


Набросаем быстренько класс, который при нормальной работе сборщика мусора не должен бросить OOM, а вот при его отсутствии делает это с огромной радостью:
1
2
3
4
5
6
7
8
9
10
11

public class TheForceTester {

    public static final int ARRAY_SIZE = 1000000;

    public static void main(String[] args) {
        while (true) {
            byte[] lotsOfUsefulData = new byte[ARRAY_SIZE];
        }
    }

}
И запустим это дело с использованием нашей новой виртуальной машины:

$ ./build/linux-amd64/bin/java -XX:+UseTheForce -verbose:gc TheForceTester
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at ru.yandex.holocron.core.TheForceTester.main(TheForceTester.java:10)


Ура! Лютый вин! Более того, в приложение-тестер можно добавить вывод текущего свободного места и убедиться, что всё остальное тоже работает вроде как корректно: куча при Xmx != Xms расширяется, а при равных свободное место уменьшается ровно на столько, на сколько должно в теории. Класс! Осталось только добавить ложку дёгтя.

Disclaimer и всё-таки ответ на Тот Самый Вопрос


Под Тем Самым Вопросом я, конечно же, подразумеваю «А Зачем?!». В начале топика я упоминал Disruptor, для которого крайне критична производительность. Сборщик мусора, как известно, вносит слабо предсказуемые задержки в работу приложения. Поэтому если есть возможность повторно использовать большинство объектов и перезапускаться время от времени, выпил GC — вполне себе адекватный способ ускориться.

Кроме того, because I want to see if I can. Кроме того, любопытно.

Disclaimer следующий: приведённое решение довольно грязное, и служит скорее как proof of concept. В первую очередь потому, что мы фактически сделали сборку мусора моментальной, оставив в виртуальной машине другие разнообразые оверхэды от использования сборщика. По-хорошему, стоило написать свою реализацию CollectedHeap, которая все эти оверхэды полностью бы исключила. Впрочем, и после этого бы наверняка осталось ещё несколько мест, в которых нужно бы было ковыряться.

Что из этого всего следует? Ждите ещё топиков! :)


P.S. Что бы ещё такого сделать?

P.P.S. Собранный под linux-amd64 архив: clck.ru/1-L-9 (Яндекс.Диск)

P.P.P.S. Пожалуйста, не клонируйте у меня весь репозиторий. Он весит 600+ мегабайт, а трафик на той машинке, где он хостится, — платный. Впрочем, это не мешает вам склонироваться на java.net, а потом уже сделать pull одного-единственного коммита (3358:3f014511ecce).
+64
2750
95
gvsmirnov 52,2

комментарии (36)

+22
tkirill128, #
Оффтоп, но всё же: отличное решение с цветными заголовками! Смотрится гораздо живей.
0
mark_ablov, #
На вкус и цвет.
Меня например это отвлекает от чтения.
+2
Fedcomp, #
хорошо выделяется из текста.
+4
gvsmirnov, #
Выдающаяся популярность вашего комментария на фоне остальных вызывает у меня вселенскую грусть :(
0
Neuronix, #
Цветами напомнило FreeBSD Handbook ;)
–10
astudent, #
выпил GC — вполне себе адекватный способ ускориться

Крайне неверное утверждение!
+9
DmitryMe, #
Расскажите, пожалуйста, почему оно неверно.
0
astudent, #
Очень просто. Ручная сборка мусора запускается, когда удобно программисту. Автоматический сборщик работает, когда удобно системе. Этот принципиальный момент делает программы с автоматической сборкой памяти быстрее.
Если не верите, проведите тесты. Если лень, на просторах интернета наверняка найдете готовые результаты.
0
DmitryMe, #
Многократно видел упоминания систем, где автоматический запуск сборки мусора приводит к периодическому полному параличу системы.
0
gvsmirnov, #
Тут сборка мусора вообще не производится.
+3
javax, #
Главное, что без снорщика мусора производительность предсказуема. Поэтому, например, во всех real time спецификациях Явы первым делом убирают сборщик мусора
0
doom369, #
А разве нельзя достичь этого же эффекта опциями JVM? Ну там MaxHeapFreeRatio, MaxTenuringThreshold?
0
javax, #
Насколько я понимаю — нет. Предсказуемым («будет занимать не больше 1% времени») его сделать нельзя по спецификации. В каких то случаях, когда вы ТОЧНО знаете сколько создаёте объектов и сколько времени они живут, что то предсказать можно. Но не в общем случае
0
doom369, #
Да, я как раз имею в виду ситуацию, когда я точно знаю сколько я создаю объектов (как у ТС). Вот тот же LMAX, там, на сколько я знаю, переиспользуются существующие объекты. И сборщик они не отключали. Они его запускают 1 раз ночью. Моя мысли — ТС проделал лишнюю работу, уверен есть возможность минимизировать влияние сборщика.
0
javax, #
В real-time самое главное — предсказуемость.
Если самое главное -performance, может быть можно сделать настроики так, чтобы GC не запускался совсем, но тогда при любом изменении кода надо проверять настройки опять. А откличить сборку — решение одноразовое, так что это выглядит разумным решением
0
gvsmirnov, #
У ТС первичной целью было любопытство ;) Разрешить ручной запуск --это ещё пара мизерных изменений. А вот гарантировать то, что сборка 100% не запустится не вручную вы никакими ключиками и никаким жёсткими ограничениями на создание новых объектов не можете.
0
dryganets, #
В реалтайм версиях явы используются другие алгоритмы сборки мусора
например dl.acm.org/citation.cfm?id=604155

выпил сборщика — не решение,
кстати если система создает небольшое количество объектов — то такая оптимизация многого не даст

большинство объектов просто уйдут в другое поколение
0
zim32, #
И какая средняя продолжительность жизни приложения на такой JVM?
+2
gvsmirnov, #
Некорректный вопрос. Как среднее java-приложение использует память?
0
ashofthedream, #
Если говорить о каком-нибудь среднем j2ee приложении, то плохо. Там сборки мусора часто происходят, раз в день уж точно могут, но и хипы там небольшие, поэтому многие по этому поводу даже и не задумываются
0
gvsmirnov, #
Это был трололо-вопрос, призванный подчеркнуть некорректность того, что задал zim32. Нельзя тут усреднять, есть очень много разных профилей использования памяти.
0
javax, #
Кстати — если надо что то сделать быстро и это надо делать время от времено — есть GroovyServ — т.е. висит JVM все время в воздухе, а быстрый клиент может ей передавать команды
+2
sneer, #
А как теперь вы руками будете собирать мусор?
0
gvsmirnov, #
Очень просто разрешить ручгую сборку. Достаточно проверять, чтто за GCCause передаётся.
0
DmitryMe, #
Интересно вот что. Автоматический запуск сборки запрещают, потому что сборка может идти непредсказуемо долго. Логично, что и запущенная вручную сборка может идти непредсказуемо долго. Как выбрать момент для ручного запуска сборки, чтобы она не парализовала систему?
0
gvsmirnov, #
Завист от специфики приложения, конечно же.
0
ashofthedream, #
Это иногда и не нужно. Достаточно частой практикой является (без всяких RT решений или покупки невероятно дорогущего железа от azul) выделение очень большого количества памяти (что бы хватило, на неделю-пару с запасом, в общем) и потом прибивание серверов раз в несколько дней.

Потому как сборка мусора это настолько непредсказуемая операция по времени, особенно полная, особенно на больших хипах >4Gb особенно на небольших сильносвязанных объектах в достаточно объемных графах… что лучшее ее избежать.

Однажды испытал на своем игровом сервере подобное (хорошо хоть в воскресенье с утра, когда на сервере пара сотен человек было, а не несколько тысяч), минуту CMS вычищал хип из 8 гигов.
+3
TheShade, #
А теперь пора это допилить, сделав флаг -XX:+ExplicitGCOnly, запретив сборку по всем причинам, кроме эксплицитного System.gc(), и сабмиттить JEP.
0
gvsmirnov, #
Sir, yes Sir! Да, допилить до не-костыльной реализации я и так собирался. Про JEP не знал, теперь есть дополнительный стимул допилить.
0
TheShade, #
Ага, это попадает под определение JEP: «This process is explicitly open to aggressive, outside-the-box, and even completely wacky ideas.» ;) Наверное, стоит всё-таки минорные сборки разрешать, чтобы совсем худо не стало. Но это уже задача со звёздочкой, ибо заставляет лезть в кишки конкретных GC.
0
gvsmirnov, #
Минорные — в смысле не STW? Тогда, наверное, тоже отдельным ключиком лучше.
0
TheShade, #
Минорные — это в смысле young gen gc. Они редко вызывают проблемы, и их паузы как правило можно хорошо регулировать размером young gen'а (с точностью до оверхедов на промоушн в old). Штука в том, что если это всё-таки разрешить, то время до полного фейла без GC может серьёзно улучшиться, потому что большая часть мусора приберётся. Это особенно важно, когда какой-нибудь залётный код вот-вот за саллоцирует чего-нибудь, ну пусть даже boxed-значение :) Кроме того, полностью без GC ты заполняешь конкретную арену в young gen, а это может несколько ограничить вместимость: ну, например, если у тебя будет эффективно только один survivor space. Если разрешить промоушн, то сможешь мусорить прямо в old.

(А ещё через пару абзацев таких «оптимизаций» я фактически уговорю включить GC обратно ;))
0
gvsmirnov, #
Тем не менее, разброс продолжительности минорной сборки может быть большим. Единственный use case отключения сборщика, который я могу придумать — это необходимость точно гарантировать отсутсвие непредсказуемых пауз. *те странные ребята*, которые захотят GC выключить, наверняка готовы пожертвовать немного памяти и сделать хип побольше, оставив его почти полностью в распоряжении eden и обделив survivor/old.

Ну и эти же странные люди наверняка имеют почти 146% контроль над тем кодом, что у них исполняется, и потому кто-то залётный ничего левого не саллоцирует.

Да ладно, можешь не уговаривать: я его, в общем-то, и не думал нигде в production выключать. Это же так, чистое любопытство :) Я же не собираюсь найти пару случаев, где отключение сборщика даёт +100500 к производительности и написать громкую статью под названием «Власти скрывают: сборщик мусора неэффективен», снабдив её красивыми графиками, показывающими непонятно что :)
+1
voronaam, #
А ссылки на JavaOne презентацию по теме нет?
+1
gvsmirnov, #
Рассказ Руслана про Disruptor тут, если вы про это.
0
voronaam, #
Именно, спасибо!

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.