Новый перевод от команды Spring АйО расскажет вам, что происходит при запуске самого простого приложения на Java, какие шаги выполняет JVM, сколько классов ей необходимо загрузить, чтобы просто написать “Hello World!” и как все это выглядит на уровне byte code.
Когда вы запускаете приложение на Java, может возникать тенденция поверить в то, что единственный выполняемый в этот момент код — это Java byte code, передаваемый JVM, то есть файлы .class
, скомпилированные javac
. На самом деле во время запуска приложения JVM проходит через сложную последовательность шагов, создавая своего рода маленькую вселенную, в которой приложение будет работать. В этой статье мы посмотрим на все шаги, через которые JVM проходит между $ java
и выводом строки Hello World
. Если вы предпочитаете видео формат, есть также видео на YouTube на канале Java, где рассказывается все то же самое (но на английском — прим. пер.).
Введение
Чтобы этот обзор процедуры запуска не превратился в попытку «вскипятить океан», мы добавим кое-какие ограничения, которые я буду использовать при описании процесса:
Я буду описывать процедуру запуска JVM, как она происходит в JDK 23. Спецификацию JVM для Java SE 23 можно посмотреть здесь.
Я буду использовать реализацию HotSpot JVM в качестве примера. Это наиболее часто используемая реализация JVM, многие популярные дистрибутивы JDK используют HotSpot JVM или ее производные. Альтернативные реализации JVM могут слегка отличаться внутренним поведением.
Ну и наконец, главным примером кода, используемым для описания процедуры старта JVM, будет
HelloWorld
, поскольку это приложение, хоть и является самым простым на свете, все еще включает все ключевые части процедуры запуска JVM.
Несмотря на эти ограничения, после прочтения этой статьи у вас появится довольно полное представление о процессах, через которые проходит JVM при запуске, как и о том, для чего они нужны. Это знание поможет вам при отладке приложения, если при его старте появятся проблемы, а в некоторых особых случаях будет полезно и для улучшения производительности. Хотя об этом мы поговорим ближе к концу статьи.
Инициализация JVM
Когда пользователь вводит команду java
, начинается процедура старта JVM, при этом вызывается функция JNI (Java Native Interface), JNI_CreateJavaVM()
, код этой функции вы можете просмотреть здесь. Эта функция JNI сама по себе выполняет несколько важных процессов.
Валидация ввода пользователя
Первый шаг в процедуре старта JVM — это валидация введенных пользователем данных: JVM аргументы, артефакт для выполнения и classpath. Ниже приведен фрагмент лога, который показывает, как происходит валидация:
[arguments] VM Arguments:
[arguments] jvm_args: -Xlog:all=trace
[arguments] java_command: HelloWorld
[arguments] java_class_path (initial): .
[arguments] Launcher Type: SUN_STANDARD
? Примечание: Вы можете увидеть этот лог, воспользовавшись -Xlog:all=trace
в JVMarg.
Обнаружение системных ресурсов
После валидации ввода пользователя следующим шагом становится обнаружение доступных ресурсов системы: процессоров, системной памяти и системных сервисов, которые может использовать JVM. Доступность системных ресурсов может повлиять на принимаемые JVM решения, которые основаны на ее внутренней эвристике. Например, сборщик мусора, выбираемый JVM по умолчанию, будет зависеть от доступного CPU и системной памяти, однако во многих случаях внутреннюю эвристику JVM могут пересиливать явно заданные JVM аргументы.
[os ] Initial active processor count set to 11
[gc,heap ] Maximum heap size 9663676416
[gc,heap ] Initial heap size 603979776
[gc,heap ] Minimum heap size 1363144
[metaspace] - commit_granule_bytes: 65536.
[metaspace] - commit_granule_words: 8192.
[metaspace] - virtual_space_node_default_size: 8388608.
[metaspace] - enlarge_chunks_in_place: 1.
[os ] Use of CLOCK_MONOTONIC is supported
[os ] Use of pthread_condattr_setclock is not supported
Подготовка окружения
Когда JVM поняла, какие системные ресурсы ей доступны, она начинает готовить окружение. В этот момент реализация HotSpot JVM генерирует hsprefdata
(данные о производительности HotSpot). Эти данные используются инструментами JConsole
и VisualVM
для инспекции и профилирования JVM. Эти данные обычно хранятся в системном каталоге /tmp
. Ниже приведен пример того, как JVM создает эти данные для профилирования, и это будет продолжаться некоторое время в течение start-up-a приложения, параллельно другим процессам.
[perf,datacreation] name = sun.rt._sync_Inflations, dtype = 11, variability = 2, units = 4, dsize = 8, vlen = 0, pad_length = 4, size = 56, on_c_heap = FALSE, address = 0x0000000100c2c020, data address = 0x0000000100c2c050
Важным шагом в процедуре старта JVM является выбор сборщика мусора (англ. garbage collector или GC, прим. пер.). Выбор сборщика мусора может иметь серьезное влияние на производительность приложения. По умолчанию JVM будет выбирать из двух сборщиков, Serial GC и G1 GC, если другие сборщики не указаны в явном виде.
Начиная с JDK 23, JVM выбирает G1 GC по умолчанию, кроме тех случаев, когда в системе меньше 1792 МБ и/или только один процессор: в этом случае будет выбран Serial GC. Конечно, могут быть доступны для выбора и другие сборщики мусора, включая Parallel GC, ZGC и другие, в зависимости от используемой версии и дистрибутива JDK. Каждый из этих сборщиков мусора имеет свои показатели производительности и идеальные рабочие нагрузки.
[gc ] Using G1
[gc,heap,coops] Trying to allocate at address 0x00000005c0000000 heap of size 0x240000000
[os,map ] Reserved [0x00000005c0000000 - 0x0000000800000000), (9663676416 bytes)
[gc,heap,coops] Heap address: 0x00000005c0000000, size: 9216 MB, Compressed Oops mode: Zero based, Oop shift amount: 3
CDS
Примерно на этой стадии JVM начинает искать CDS архив. CDS расшифровывается как Cached Data Storage (закешированное хранилище данных), прежнее толкование этой аббревиатуры — Class Data Storage (хранилище данных классов). CDS архив — это архив файлов класса, обработанных заранее для улучшения скорости процедуры старта JVM . Мы расскажем о том, как улучшается производительность во время старта JVM за счет CDS, в разделе «Связывание классов». Однако, не стоит засорять себе память аббревиатурой «CDS», она устарела, мы поговорим об этом, когда будем рассказывать о будущем процедуры старта JVM.
Комментарий от редакции Spring АйО:
На деле, CDS может расшифровываться как Class Data Sharing, Class Data Storage и в более новых версиях JDK — Cached Data Storage. В то время как первые два компонента означают одно и то же, Cached Data Storage, хоть и совсем немного, другим понятием. Более подробно в ответе эксперта на StackOverFlow.
Комментарий от редакции Spring АйО:
"Связывание классов" в других источниках может называться "Linkage" или "линковка классов"
[cds] trying to map [Java home]/lib/server/classes.jsa
[cds] Opened archive [Java home]/lib/server/classes.jsa.
Создание пространства под методы
Один из последних шагов инициализации JVM — это создание пространства под методы (method area). Это специальная локация в off-heap памяти, где будут храниться данные классов по мере того, как JVM будет их загружать. В то время как пространство под методы не располагается внутри кучи JVM, сборщик мусора все же управляет им. Данные классов, сохраненные в пространстве под методы, допустимо удалять, если загрузчик класса, связанный с этими данными, уже не используется.
? Примечание: Если вы используете реализацию HotSpot для JVM, пространство под методы будет называться метапространством (metaspace).
Комментарий от редакции Spring АйО:
В общем случае, Method Area — это просто пространство, определенное в JLS, где хранится некоторая метаинформация, например "run-time constant pool, field and method data".
И в мире Java существует огромное количество споров на тему того:
- Является ли Metaspace реализацией Method Area в HotSpot JVM
- Является ли Method Area подмножеством Metaspace в HotSpot JVM, иными словами Metaspace содержит Method Area и еще что-нибудь и т.д.
На деле тут четкого ответа нет. Но, как правило, если Вы будете говорить, что Method Area это Metaspace в HotSpot JVM, в целом, грубо, это верно и Вас поймут.
[metaspace,map] Trying to reserve at an EOR-compatible address
[metaspace,map] Mapped at 0x00001fff00000000
Загрузка, связывание и инициализация классов
После завершения первых шагов, которые можно назвать «работой по домашнему хозяйству» начинается собственно процедура старта JVM, включающая в себя загрузку классов, их связывание и инициализацию.
В то время как спецификация JVM описывает эти процессы последовательно, в разделах 5.3-5.5, в HotSpot JVM эти процессы не обязаны происходить именно в таком порядке для отдельно взятого класса. Как отмечено в нижней части схемы, Разрешение (Resolution), которое является частью процесса связывания классов, может произойти на любом этапе, как до начала проверки, так и после инициализации классов. Некоторые процессы, такие как инициализация классов, технически вообще не обязаны происходить. Мы расскажем об этом подробнее в следующих разделах.
Загрузка классов
Процесс загрузки классов описывается в разделе 5.3 JVM спецификации. Загрузка классов — это трехшаговый процесс, при котором JVM находит бинарное представление класса или интерфейса, извлекает из него класс или интерфейс и загружает эту информацию в пространство под методы (Method Area) в JVM, которое, напомним, называется “metaspace” в реализации HotSpot JVM.
Одна из сильных сторон JVM, благодаря которой она стала такой популярной платформой — это ее способность динамически загружать классы, что позволяет JVM загружать сгенерированные классы по мере необходимости в течение всего рантайма JVM. Эта ее способность используется многими популярными фреймворками и инструментами, как, например, Spring и Mockito. На самом деле даже сама JVM может генерировать код по мере необходимости, когда она использует лямбды, как это происходит в классе InnerClassLambdaMetafactory.
JVM поддерживает два способа загрузки классов, либо через загрузчик классов bootstrap (5.3.1), либо через кастомизированный загрузчик классов (5.3.2). Во втором случае это будет класс, расширяющий класс java.lang.ClassLoader. На практике кастомизированные загрузчики классов часто будут определяться как библиотеки от третьих сторон, чтобы поддержать поведение, свойственное такой библиотеке.
В рамках этой статьи мы сфокусируемся на загрузчике bootstrap, который является специальным загрузчиком классов, написанным на машинном коде и предоставляемым JVM. Он инстанцируется на поздних этапах работы метода JNI_CreateJavaVM()
.
Чтобы лучше понимать процесс загрузки классов, нам надо посмотреть на проект HelloWorld
как его видит JVM:
public class HelloWorld extends Object {
public static void main(String[] args){
System.out.println(“Hello World!”);
}
}
Все классы так или иначе расширяют java.lang.Object
. Чтобы JVM могла загрузить HelloWorld
, прежде всего ей надо загрузить все классы, от которых HelloWorld
явно или неявно зависит. Давайте посмотрим на сигнатуры методов в java.lang.Object
:
public class Object {
public Object() {}
public final native Class<?> getClass()
public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native void notify();
public final native void notifyAll();
public final void wait() throws InterruptedException
public final void wait(long timeoutMillis) throws InterruptedException
public final void wait(long timeoutMillis, int nanos) throws InterruptedException
protected void finalize() throws Throwable { }
}
Два важных метода здесь — это public final native Class<?> getClass()
и public String toString()
, поскольку оба эти метода ссылаются на другой класс: java.lang.Class
и java.lang.String
соответственно.
Если мы посмотрим на java.lang.String
, он реализует несколько интерфейсов:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc
Чтобы загрузить класс java.lang.String
, сначала надо загрузить все интерфейсы, которые он реализует, и если мы посмотрим на лог загрузки, мы увидим, что эти классы загружаются в том же порядке, в котором они были определены, при этом java.lang.String
загружается последним:
[class,load] java.io.Serializable source: jrt:/java.base
[class,load] java.lang.Comparable source: jrt:/java.base
[class,load] java.lang.CharSequence source: jrt:/java.base
[class,load] java.lang.constant.Constable source: jrt:/java.base
[class,load] java.lang.constant.ConstantDesc source: jrt:/java.base
[class,load] java.lang.String source: jrt:/java.base
Если мы перейдем к java.lang.Class
, мы увидим, что и он реализует несколько интерфейсов, и некоторые из этих интерфейсов те же самые, которые реализовывает java.lang.String
, а именно java.io.Serializable
и java.lang.constant.Constable
.
public final class Class<T>
implements java.io.Serializable,GenericDeclaration,Type,AnnotatedElement,
TypeDescriptor.OfField<Class<?>>,Constable
Если мы посмотрим на логи JVM, мы увидим, что интерфейсы снова загружаются в том порядке, в каком они были определены, и уже после них загружается класс java.lang.Class
. Классы java.io.Serializable
и java.lang.constant.Constable
загружены не будут, поскольку они загрузились раньше в процессе загрузки класса java.lang.String
.
[class,load] java.lang.reflect.AnnotatedElement source: jrt:/java.base
[class,load] java.lang.reflect.GenericDeclaration source: jrt:/java.base
[class,load] java.lang.reflect.Type source: jrt:/java.base
[class,load] java.lang.invoke.TypeDescriptor source: jrt:/java.base
[class,load] java.lang.invoke.TypeDescriptor$OfField source: jrt:/java.base
[class,load] java.lang.Class source: jrt:/java.base
? Примечание: Обычно JVM придерживается стратегии lazy для своих процессов, в данном случае для загрузки классов. Это означает, что класс загружается только тогда, когда на него активно ссылается другой класс, но поскольку java.lang.Object
является тем корнем, из которого вырастают все классы Java, то JVM применит стратегию eager к классам java.lang.Class
и java.lang.String
. Если вы посмотрите на сигнатуры методов для java.lang.Class
(JavaDoc) и java.lang.String
(JavaDoc), вы можете заметить, что многие из этих классов не будут загружаться при выполнении приложений, подобных HelloWorld
. Например, никаких ссылок на Optional<String> describeConstable()
здесь не будет, а значит загрузки java.util.Optional
не произойдет. Это живой пример свойственной HotSpot стратегии lazy.
Процесс загрузки классов будет продолжаться на протяжении большей части процедуры старта JVM, а в случае настоящего приложения также и во время начала жизненного цикла самого приложения, пока не завершится со временем. Суммарно JVM загрузит примерно 450 классов для сценария HelloWorld
, и именно поэтому я применил аналогию со вселенной, которую JVM создает при старте, поскольку тут реально очень много работы.
Продолжим погружаться во вселенную стартовой процедуры JVM и посмотрим на связывание классов.
Связывание классов
Связывание классов, описанное в разделе 5.4 спецификации JVM, является одним из наиболее комплексных процессов, так как включает три отдельных подпроцесса:
В рамках связывания классов присутствуют еще три процесса: контроль доступа (Access Control), переопределение методов (Method Overriding), и выбор метода (Method Selection), но о них мы в рамках данной статьи говорить не будем.
Если вернуться к диаграмме, проверка, подготовка и разрешение не обязательно происходят в том порядке, в котором они будут описаны в этой статье. Разрешение может пройти раньше проверки, но это может случиться и гораздо позже, после инициализации класса.
Проверка (Verification)
Проверка (5.4.1) — это процесс, в ходе которого JVM убеждается в том, что класс или интерфейс структурно корректен. Этот процесс может вызвать загрузку других классов, если это необходимо, хотя для загруженных таким образом классов не требуется проводить проверку или подготовку.
Возвращаясь к теме CDS, в большинстве нормальных ситуаций классы JDK не будут проходить через активную фазу проверки. Это объясняется тем, что одним из преимуществ CDS является предварительная проверка классов внутри архива, что снижает количество работы, которую JVM должна проделать при старте. Что, в свою очередь, улучшает производительность на старте.
Если вы хотите узнать больше о CDS, вы можете посмотреть мое Stack Walker видео на эту тему, почитать наши статьи по dev.java на CDS, или эту статью по inside.java, где рассказывается о том, как включать классы вашего приложения в архив CDS.
Один из классов, который надо подвергнуть проверке — это HelloWorld
, и мы видим, как JVM это делает, в следующих логах:
[class,init ] Start class verification for: HelloWorld
[verification ] Verifying class HelloWorld with new format
[verification ] Verifying method HelloWorld.<init>()V
[verification ] table = {
[verification ] }
[verification ] bci: @0
[verification ] flags: { flagThisUninit }
[verification ] locals: { uninitializedThis }
[verification ] stack: { }
[verification ] offset = 0, opcode = aload_0
[verification ] bci: @1
Подготовка (preparation)
Подготовка (5.4.2) отвечает за инициализацию статических полей в классе их значениями по умолчанию.
Чтобы лучше понять, как это работает, давайте посмотрим на этот простой пример класса:
class MyClass {
static int myStaticInt = 10; //Initialized to 0
static int myStaticInitializedInt; //Initialized to 0
int myInstanceInt = 30; //Not initialized
static {
myStaticInitializedInt = 20;
}
}
Класс содержит три целочисленных поля: myStaticInt
, myStaticInitializedInt
, и myInstanceInt
.
В этом примере как myStaticInt
, так и myStaticInitializedInt
инициализировались бы числом 0
, то есть значением по умолчанию для примитивного типа int
.
При этом поле myInstanceInt
не будет инициализировано, так как это поле экземпляра, а не класса.
Чуть позже мы немного расскажем о том, когда поля myStaticInt
и myStaticInitializedInt
инициализируются значениями 10
и 20
.
Разрешение (Resolution)
Цель процесса разрешения (5.4.3) состоит в том, чтобы разрешить символические ссылки в пуле констант класса для использования инструкциями JVM.
Для лучшего понимания этого процесса мы воспользуемся инструментом javap
. Это стандартный инструмент командной строки JDK, предназначенный для дизассемблирования .class
файлов Java. Если запускать его с опцией -verbose
, он даст нам представление о том, как JVM интерпретирует загружаемые классы. Давайте запустим javap
на MyClass
:
$ javap –verbose MyClass
class MyClass {
static int myStaticInt = 10; //Initialized to 0
static int myStaticInitializedInt; //Initialized to 0
int myInstanceInt = 30; //Not initialized
static {
myStaticInitializedInt = 20;
}
}
Результат работы этой команды приведен ниже (под спойлером):
Много кода:
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // MyClass.myInstanceInt:I
#8 = Class #10 // MyClass
#9 = NameAndType #11:#12 // myInstanceInt:I
#10 = Utf8 MyClass
#11 = Utf8 myInstanceInt
#12 = Utf8 I
#13 = Fieldref #8.#14 // MyClass.myStaticInt:I
#14 = NameAndType #15:#12 // myStaticInt:I
#15 = Utf8 myStaticInt
#16 = Fieldref #8.#17 // MyClass.myStaticInitializedInt:I
#17 = NameAndType #18:#12 // myStaticInitializedInt:I
#18 = Utf8 myStaticInitializedInt
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 <clinit>
#22 = Utf8 SourceFile
#23 = Utf8 MyClass.java
{
static int myStaticInt;
descriptor: I
flags: (0x0008) ACC_STATIC
static int myStaticInitializedInt;
descriptor: I
flags: (0x0008) ACC_STATIC
int myInstanceInt;
descriptor: I
flags: (0x0000)
MyClass();
descriptor: ()V
flags: (0x0000)
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 30
7: putfield #7 // Field myInstanceInt:I
10: return
LineNumberTable:
line 1: 0
line 4: 4
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #13 // Field myStaticInt:I
5: bipush 20
7: putstatic #16 // Field myStaticInitializedInt:I
10: return
LineNumberTable:
line 2: 0
line 6: 5
line 7: 10
}
? Примечание: Этот вывод был немного обрезан, чтобы удалить метаданные, не относящиеся к теме данной статьи.
Здесь довольно много данных, так что давайте разобьем их на части и пройдемся по ним шаг за шагом, чтобы понять, что все это значит.
Приведенный ниже фрагмент является конструктором по умолчанию для класса MyClass
и генерируется автоматически. Он начинается с вызова конструктора по умолчанию для родительского класса, а именно java.lang.Object
, а затем myInstanceInt
устанавливается в 30
.
MyClass();
descriptor: ()V
flags: (0x0000)
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 //Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 30
7: putfield #7 //Field myInstanceInt:I
10: return
LineNumberTable:
line 1: 0
line 4: 4
? Примечание: Несомненно, вы заметили aload_0
, invokespecial
, bipush
, putfield
, и т.д. Это JVM инструкции, opcode, который JVM использует для выполнения своей работы.
Справа от invokespecial
и putfield
находятся числа #1
и #7
соответственно. Это ссылки на пул констант MyClass
(4.4). Давайте посмотрим на него более пристально:
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // MyClass.myInstanceInt:I
#8 = Class #10 // MyClass
#9 = NameAndType #11:#12 // myInstanceInt:I
#10 = Utf8 MyClass
#11 = Utf8 myInstanceInt
#12 = Utf8 I
#13 = Fieldref #8.#14 // MyClass.myStaticInt:I
#14 = NameAndType #15:#12 // myStaticInt:I
#15 = Utf8 myStaticInt
#16 = Fieldref #8.#17 // MyClass.myStaticInitializedInt:I
#17 = NameAndType #18:#12 // myStaticInitializedInt:I
#18 = Utf8 myStaticInitializedInt
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 <clinit>
#22 = Utf8 SourceFile
#23 = Utf8 MyClass.java
В пуле констант класса MyClass
находятся все его символические ссылки. Чтобы JVM могла выполнить JVM инструкцию invokespecial
,ей необходимо разрешить связь с конструктором по умолчанию класса constructor of java.lang.Object
. Если вернуться к пулу констант, строки 1-6 предоставляют информацию, необходимую для формирования такой связи.
? Примечание: <init>
— это специальный метод, который javac
автоматически генерирует для каждого конструктора в классе.
Тот же паттерн повторяется для команды putfield, которая ссылается на строку 7 в пуле констант, которая в комбинации со строками 8-12 предоставляет необходимую информацию для разрешения связей для установки значения переменной myInstanceInt
. Чтобы узнать больше о пуле констант, обратитесь к соответствующему разделу спецификации JVM.
Причина того, что процесс разрешения может произойти как до проверки, так и после инициализации класса, состоит в том, что он выполняется в режиме lazy, только когда JVM пытается выполнить JVM инструкцию в классе. Не все загруженные классы содержат исполняемые JVM инструкции. Например, класс java.lang.SecurityManager
загружается, но не используется, потому что он устарел и выводится из обращения. Возможна и такая ситуация, когда в классе нечего инициализировать, и он автоматически помечается JVM как инициализированный. Что подводит нас к теме инициализации классов…
Инициализация классов
Мы наконец-то добрались до инициализации классов, о которой рассказывается в разделе 5.5 JVM спецификации. Инициализация классов включает в себя присвоение значения ConstantValue
статическим полям и выполнение любых статических блоков инициализации в классе, если они там есть. Этот процесс начинается, когда JVM вызывает любые new
, getstatic
, putstatic
или invokestatic
JVM инструкции на классе.
Инициализация класса осуществляется специальным no args методом, void <clinit>
, который, как и <init>
, автоматически генерируется javac
. Угловые скобки (< >
) были включены специально, так как они не являются валидными символами для имени метода и таким образом не позволяют пользователям Java написать собственные кастомизированные методы <init>
или <clinit>
.
Это не является гарантией того, что метод <clinit>
будет создаваться всегда, поскольку он нужен только если в классе присутствуют статические блоки инициализации или поля. Если в классе нет ни того, ни другого, <clinit>
не генерируется, а JVM сразу помечает класс как инициализированный, если на нем вызывается new
, по сути пропуская этап инициализации класса. Таким образом разрешение может произойти после инициализации класса.
Поскольку в MyClass
присутствуют два статических поля и статический блок инициализации, в нем есть метод <clinit>
, что возвращает нас к выводу команды javap
:
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #13 // Field myStaticInt:I
5: bipush 20
7: putstatic #16 // Field myStaticInitializedInt:I
10: return
LineNumberTable:
line 2: 0
line 6: 5
line 7: 10
Структура <clinit>
напоминает <init>
, но без вызова конструктора родительского класса, а вместо putfield
используются такие конструкции JVM, как putstatic
.
Hello World!
Рано или поздно наступит момент, когда JVM выполнит всю подготовительную работу. Необходимую для начала выполнения пользовательского кода внутри public static void main()
, где и находится сообщение Hello World!
:
[0.062s][debug][class,resolve] java.io.FileOutputStream
...
Hello World!
По итогу JVM загрузит примерно 450 классов, и некоторая часть этих классов будет также связана и инициализирована. На моем M4 MacBook Pro, как можно увидеть по логам, весь процесс занял всего 62 миллисекунды, даже с учетом ОЧЕНЬ подробного логирования. Полный лог можно посмотреть на GitHub.
Project Leyden
Сейчас для процесса старта JVM наступили очень интересные времена. Этот процесс и раньше постоянно совершенствовался с каждым релизом, а начиная с JDK 24 первая наработка из Project Leyden будет включена в релиз JDK в его главную ветку.
Project Leyden ставит целью сократить: время старта, время достижения пиковой производительности и memory footprint. Он вырос из CDS и продолжает его традиции. По мере интеграции Project Leyden CDS постепенно уступит место AOT (ahead-of-time). Возможности Project Leyden позволят записать поведение JVM во время тестового прогона, сохранить эту информацию в кеше и затем загрузить ее из кеша при следующих запусках. Если вы хотите узнать больше о Project Leyden, обязательно посмотрите это видео.
Главной возможностью Project Leyden станет JEP 483: AOT загрузка классов и их связывание. Мы уже рассказали о загрузке и связывании классов в этой статье, так что преимущества AOT выполнения этих процедур вместо того, чтобы выполнять их при старте, должны быть очевидны.
Заключение
Как видно из этой статьи, процедура запуска JVM — это очень комплексная процедура. Возможность реагировать на доступность системных ресурсов, предоставить средства для инспектирования и профилирования JVM, динамической загрузки классов и многого другого приводит к серьезному усложнению всей процедуры.
Какие выводы можно сделать из всего сказанного, помимо более глубокого понимания JVM? Как минимум два аспекта стоят того, чтобы обратить на них внимание, отладка и производительность, хотя возможность их применения может быть несколько ограниченной.
Отладка
Процедура запуска JVM довольно надежна, и, как правило, если происходит ошибка, то ее причиной является ошибка пользователя или, возможно,какая-то проблема в библиотеке, полученной от третьей стороны. Мы надеемся, что более глубокое понимание того, что JVM пытается сделать и почему, может помочь вам разобраться с самыми трудно устранимыми или сложными в понимании проблемами процедуры запуска.
Улучшения производительности
Другое потенциальное преимущество состоит в том, что, вооружившись этим знанием, вы можете найти пусть и небольшие, но возможности для улучшения производительности вашего приложения на старте. Особенно учитывая, что JEP 483 интегрируется в JDK 24, вынос загрузки классов и их связывание в AOT может еще больше улучшить производительность при старте.
Однако, я бы напомнил вам о том, что в большинстве случаев код «первой стороны» (то есть ваш) составляет лишь незначительную часть от кода, который выполняет JVM. С учетом всех библиотек, фреймворков и самого JDK, зачастую код вашего приложения оказывается лишь верхушкой айсберга.
Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.