
На недавно прошедшей
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).
комментарии (36)