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

в 3:37, , рубрики: hotspot, java, openjdk, wtf, ненормальное программирование, метки: , , ,

Как собрать свою JDK, без блекджека и автоматической сборки мусораНа недавно прошедшей 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 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).

Автор: gvsmirnov


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js