На недавно прошедшей 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
, в котором нас интересуют следующие строки:
|
|
Понятно, теперь ищем JVM_GC
. Не менее быстро находим его объявление в src/share/vm/prims/jvm.cpp
:
|
|
Тут мы видим аж два очень интересных момента: первый — 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
:
|
|
Продолжая исследование класса, наконец наталкиваемся на нужный код:
|
|
Тут для нас наиболее полезны комментарии. Для тех, кто недостаточно хорошо знает английский, разъясню: первый метод, просто collect()
, предназначен для сборки «извне» (например, из System.gc
или, как показывает всё тот же grep
, при неудачной аллокации памяти в linux). Второй же запускается из потока виртуальной машины, который отвечает за сборку мусора (и предполагается, что уже держатся все необходимые локи). На ум сразу приходит простое решение: сделать так, чтобы при вызове этих методов сборка не происходила. Я даже первый раз попробовал именно этот подход, только вот ведь незадача: оказывается, всё несколько сложнее, и у каждой реализации кучи есть свои дополнительные места, в которых происходит сборка. Потому пришлось выбрать какую-то конкретную реализацию ( GenCollectedHeap
с MarkSweepPolicy
как самую простую), и у неё в зависимости от флага (который я обозвал UseTheForce
) выходить из методов, производящих сборку, ничего не делая. В итоге изменения в первой версии произошли вот такие.
Пробуем!
Набросаем быстренько класс, который при нормальной работе сборщика мусора не должен бросить OOM, а вот при его отсутствии делает это с огромной радостью:
|
|
И запустим это дело с использованием нашей новой виртуальной машины:
$ ./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 can. Кроме того, любопытно.
Disclaimer следующий: приведённое решение довольно грязное, и служит скорее как proof of concept. В первую очередь потому, что мы фактически сделали сборку мусора моментальной, оставив в виртуальной машине другие разнообразые оверхэды от использования сборщика. По-хорошему, стоило написать свою реализацию CollectedHeap
, которая все эти оверхэды полностью бы исключила. Впрочем, и после этого бы наверняка осталось ещё несколько мест, в которых нужно бы было ковыряться.
Что из этого всего следует? Ждите ещё топиков! :)
P.S. Что бы ещё такого сделать?
P.P.S. Собранный под linux-amd64 архив: clck.ru/1-L-9 (Яндекс.Диск)
P.P.P.S. Пожалуйста, не клонируйте у меня весь репозиторий. Он весит 600+ мегабайт, а трафик на той машинке, где он
Автор: gvsmirnov