Глубокое погружение в процесс запуска JVM

Введение
При запуске Java-приложения может показаться, что единственное, чем занимается в этот момент виртуальная машина (JVM) — это выполняет переданный в нее байт-код, то есть `.class-файлы`, скомпилированные `javac`. Но реальность такова, что во время запуска JVM проходит через сложную последовательность шагов, создавая целую вселенную для обеспечения работы приложения.
В этой статье мы рассмотрим, какие этапы проходит JVM — от ввода команды `$ java` до вывода `Hello World`.
Если вы предпочитаете видеоформат, то можете ознакомиться с данной темой на YouTube.
Чтобы не углубляться в излишние детали, я ограничу описание процесса запуска следующими рамками:
  • JVM будет запускаться в рамках JDK 23. Спецификацию JVM читайте по ссылке
  • В качестве реализации JVM воспользуемся самой популярной — HotSpot. Внутреннее поведение других реализаций может немного отличаться
  • В качестве примера будет использоваться код, выводящий `HelloWorld`. Несмотря на простоту этого приложения, с его помощью мы сможем рассмотреть все ключевые этапы запуска JVM
После прочтения статьи у вас должно сложиться достаточно полное понимание процессов, через которые проходит JVM во время запуска. Эти знания могут быть полезны при отладке приложения, если во время его запуска возникнут проблемы, а в некоторых узкоспециализированных случаях — для улучшения производительности запуска. Об этом мы поговорим ближе к концу статьи.
  1. Инициализация JVM
Введенная пользователем команда `java` активирует процедуру запуска JVM, в рамках которой выполняется ее инициализация и вызывается функция JNI (Java Native Interface) `JNI_CreateJavaVM()`. Она выполняет несколько важных задач, рассматриваемых далее.

Проверка (валидация) входных данных пользователя

Первым действием, выполняемым в процессе запуска JVM, является проверка переданных пользователем данных: аргументов JVM, исполняемого артефакта (class-файл, jar-файл) и `classpath`. Ниже приведён лог процесса валидации:
[arguments] VM Arguments:  
[arguments] jvm_args: -Xlog:all=trace  
[arguments] java_command: HelloWorld  
[arguments] java_class_path (initial): .  
[arguments] Launcher Type: SUN_STANDARD
Примечание: для отображения полного лога запустите JVM с аргументом `-Xlog:all=trace`.

Обнаружение системных ресурсов

На следующем шаге происходит определение доступных системных ресурсов: процессоров, оперативной памяти и системных служб, которые может использовать JVM.
Наличие системных ресурсов может повлиять на решения, которые 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 генерирует `hsprefdata` (данные о производительности JVM), сохраняя их в системном каталоге `/tmp`. Эти данные используются такими инструментами, как `JConsole` и `VisualVM` для анализа и профилирования JVM. Ниже приведен один из примеров таких данных. Их формирование будет продолжаться некоторое время в процессе запуска JVM параллельно другим процессам.
[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 является выбор сборщика мусора (GC), что может существенно повлиять на производительность приложения. По умолчанию JVM выбирает между `Serial GC` и `G1 GC`, если не указано иное.
Начиная с JDK 23, по умолчанию используется `G1 GC` при условии, что система имеет более 1792 МБ доступной оперативной памяти и/или более одного процессора. В противном случае будет выбран `Serial GC`. Конечно, в зависимости от конкретной версии дистрибутива, могут быть доступны и другие GC такие, как `Parallel GC`, `ZGC` и проч. Каждый из GC имеет свои особенности, связанные с производительностью и оптимальностью рабочих нагрузок.
[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) — это архив предварительно скомпилированных class-файлов для улучшения времени запуска JVM. Мы рассмотрим этот вопрос в разделе «Связывание классов». Однако не забивайте себе голову подробностями про CDS, поскольку это устаревшая технология скоро выйдет из употребления. Позже мы объясним ее недостатки.
[cds] trying to map [Java home]/lib/server/classes.jsa
[cds] Opened archive [Java home]/lib/server/classes.jsa

Создание Method Area

Одним из последних шагов инициализации JVM является создание Method Area (пространства под методы). Это специальная область памяти вне кучи (off-heap), в которой хранятся метаданные классов по мере их загрузки в JVM. Хотя Method Area не размещается в куче, эта область все же управляется GC. Метаданные классов, хранящиеся в Method Area, могут быть удалены, если загрузивший их загрузчик классов больше не используется (недоступен, так как на него не осталось активных ссылок).
[metaspace,map] Trying to reserve at an EOR-compatible address
[metaspace,map] Mapped at 0x00001fff00000000
Примечание: в HotSpot пространство под методы называется Metaspace (метапространство).
2. Загрузка, связывание и инициализация классов
С этого момента начинается основная часть процесса запуска JVM, включающая загрузку, связывание и инициализацию классов. Хотя спецификация JVM описывает эти процессы последовательно (разделы 5.3–5.5), в HotSpot JVM они могут происходить в другом порядке.
Этапы запуска JVM
Указанное в нижней части диаграммы Разрешение (Resolution), как часть процесса под названием Связывание классов (Class Linking), может выполняться в любой момент, начиная с этапа Проверки (Verification) и заканчивая этапом Инициализации классов (Class Initialization). Более того, некоторые процессы, такие как инициализация, в некоторых случаях могут вообще не выполняться.
Мы подробно разберем каждый из этих этапов в последующих разделах.

Загрузка классов

Загрузка классов описывается в разделе 5.3 спецификации JVM. Он включает следующие этапы:
  • Поиск бинарного представления класса или интерфейса (class-файлы)
  • Создание на его основе внутреннего представления класса или интерфейса
  • Загрузка этой информации в Method Area (Metaspace)
Одной из самых мощных особенностей JVM, принесшей ей широкую популярность, является способность динамически загружать сгенерированные классы во время ее выполнения. Этот функционал активно используется популярными фреймворками и инструментами, такими как Spring, Mockito и проч. Более того, сама JVM использует генерацию кода по мере необходимости, например, при работе с лямбда-выражениями, что можно наблюдать в классе InnerClassLambdaMetafactory.
JVM поддерживает загрузку классов с помощью:
На практике кастомные загрузчики часто определяются в сторонних библиотеках для реализации специфического поведения этих библиотек.
В этой статье мы сосредоточимся только на `bootstrap` — специальном загрузчике классов, встроенном в JVM. Он создается на поздних этапах выполнения `JNI_CreateJavaVM()`.
Чтобы лучше понять процесс загрузки классов, рассмотрим следующую программу с точки зрения JVM.
public class HelloWorld extends Object {

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}
Все классы в конечном итоге наследуются от `java.lang.Object`.
Чтобы JVM могла загрузить `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() на java.lang.Class
  • public String toString() на java.lang.String
Поскольку класс `java.lang.String` реализует несколько интерфейсов, то перед его загрузкой JVM сначала должна загрузить все его интерфейсы.
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc
Глядя на логи видно, что эти классы/интерфейсы загружаются в порядке их определения, а `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` также реализует несколько интерфейсов.
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 Loading, ленивая загрузка) при выполнении своих процессов, в данном случае — при загрузке классов. Это означает, что класс загружается только тогда, когда на него явно ссылается другой класс. Однако, поскольку `java.lang.Object` — это особый корневой класс для всех классов в Java, JVM заранее загружает (Eager Loading, жадная загрузка) `java.lang.Class` и `java.lang.String`.
Если посмотреть на сигнатуры методов java.lang.Class и java.lang.String, можно заметить, что многие классы не загружаются при выполнении такого приложения, как `HelloWorld`.
Например, метод `Optional<String> describeConstable()` никогда не вызывается, а значит, `java.util.Optional` не загружается. Это и есть наглядный пример отложенной загрузки HotSpot.
Процесс загрузки классов продолжается на протяжении большей части запуска JVM, а в реальном приложении — и в течение значительной части начального этапа жизненного цикла приложения, прежде чем всё стабилизируется.
Для `HelloWorld` JVM загружает около 450 классов. По этой причине я использовал аналогию с созданием вселенной при запуске JVM, поскольку она выполняет огромный объем работы.
Продолжим погружение во "вселенную" запуска JVM, рассмотрев связывание классов.

Связывание классов

Связывание, рассматриваемое в разделе 5.4 спецификации JVM, является одним из наиболее сложных процессов, так как включает три подпроцесса:
В рамках связывания выполняются еще три процесса: контроль доступа (Access Control), переопределение методов (Method Overriding) и выбор метода (Method Selection), которые мы не будем рассматривать в этой статье.
Указанные на схеме проверка, Подготовка (Preparation) и разрешение необязательно выполняются в том порядке, в котором они рассматриваются в этой статье. Разрешение может происходить как до проверки, так и после инициализации.

Проверка

Проверка (раздел 5.4.1) — это процесс, при котором JVM анализирует структуру класса или интерфейса на корректность. При необходимости этот процесс может инициировать загрузку других классов, которым такая проверка уже не требуется.
Возвращаясь к теме CDS, в большинстве обычных ситуаций классы из JDK не проходят проверку. Это связано с тем, что одно из преимуществ CDS заключается в том, что классы, содержащиеся в архиве, уже были проверены, что снижает нагрузку на JVM при запуске и, как следствие, улучшает скорость запуска.
Если вы хотите узнать больше о CDS, посмотрите мое видео на YouTube, а также наши статьи на dev.java и inside.java.
По логам видно, как JVM выполняет проверку класса `HelloWorld`:
[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

Подготовка

Во время подготовки (раздел 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.

Разрешение

На данном этапе (раздел 5.4.3) происходит сопоставление (разрешение) символьных ссылок в пуле констант класса с их фактическими значениями (замена имен классов, методов и полей, записанных в пуле, на реальные ссылки на объекты и адреса в памяти) для последующего использования инструкциями JVM.
Для лучшего понимания данного процесса воспользуемся ` javap`. Это стандартный инструмент командной строки JDK для дизассемблирования class-файлов. Запустим его с опцией `-verbose` для класса `MyClass`, чтобы увидеть, как JVM интерпретирует загружаемые классы.
$ javap -verbose MyClass
Результат этой команды показан ниже:
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`. Он начинается с вызова конструктора по умолчанию родительского класса `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опкоды, которые она использует для выполнения своей работы.
Справа от `invokespecial` и `putfield` стоят числа #1 и #7. Это ссылки на пул констант класса `MyClass` (раздел 4.4). Рассмотрим их подробнее:
#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` хранятся все его символические ссылки. Для выполнения инструкции `invokespecial` JVM необходимо установить связь с конструктором по умолчанию класса `java.lang.Object`. Из вывода выше видно, что записи с 1 по 6 содержат информацию, необходимую для установления этой связи.
Примечание: `<init>` — специальный метод, который `javac` автоматически генерирует для каждого конструктора в классе.
Аналогичный паттерн повторяется с инструкцией `putfield`, которая ссылается на запись 7. В сочетании с записями 8 по 12 она содержит всю необходимую информацию для установления связи при присвоении значения переменной `myInstanceInt`. Для получения дополнительной информации о пуле констант ознакомьтесь с соответствующим разделом в спецификации JVM.
Причина, по которой процесс разрешения может происходить как до проверки, так и после инициализации классов, заключается в том, что он использует ленивую стратегию выполнения (lazily). Разрешение ссылок происходит только тогда, когда JVM пытается выполнить инструкцию в классе. Не все загруженные классы могут иметь такие инструкции. Например, `java.lang.SecurityManager` загружается, но никогда не используется, так как устарел и постепенно выводится из обращения (является deprecated с Java 9). Также возможно ситуация, когда в классе нечего инициализировать, и он автоматически помечает JVM как инициализированный.

Инициализация классов

Рассмотрим инициализацию, описанную в разделе 5.5 спецификации JVM.
Инициализация класса включает присвоение значения `ConstantValue` статическим полям и выполнение статических блоков инициализации, если они присутствуют. Этот процесс запускается, когда JVM выполняет одну из инструкций `new`, `getstatic`, `putstatic` или `invokestatic` по отношению к классу.
Инициализация класса осуществляется с помощью специального метода без аргументов — `void <clinit>`. Как и <init>, этот метод автоматически создается компилятором `javac`. Угловые скобки (< >) используются специально, так как они недопустимы в именах методов. Это предотвращает возможность создания пользовательских методов с именами `<init>` или `<clinit>` в Java.
Метод `<clinit>` генерируется только, если в классе есть статические инициализаторы или статические поля. В противном случае при вызове `new` JVM сразу помечает класс как инициализированный, фактически пропуская этап его инициализации. Это также объясняет, почему разрешение может выполняться после инициализации.
Поскольку в `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>`, но без вызова конструктора родительского класса. Вместо инструкций JVM `putfield` используются `putstatic`.
3. Выполнение Hello World!
Рано или поздно JVM завершит подготовку и начнет выполнение пользовательского кода внутри `public static void main()`, где находится сообщение "Hello, World!":
[0.062s][debug][class,resolve] java.io.FileOutputStream  
...  
Hello World!
В общей сложности JVM загрузит около 450 классов, часть из которых будет связана и инициализирована. На моем MacBook Pro M4, как видно из логов, весь процесс занял всего 62 миллисекунды, несмотря на ОЧЕНЬ подробное логирование. Полный лог можно посмотреть у меня на GitHub.
4. Project Leyden
Уже сегодня происходят важные изменения, направленные на ускорение запуска JVM. Хотя с каждым выпуском JDK процесс старта постепенно совершенствуется, начиная с JDK 24, первые наработки из Project Leyden будут включены в этот релиз.
Project Leyden ставит перед собой цель уменьшить время запуска, время достижения пиковой производительности и потребление памяти, развивая и заменяя наработки, сделанные в CDS (Class Data Sharing). По мере интеграции Project Leyden, CDS уступит место AOT (Ahead-of-Time). Project Leyden будет работать за счет записи поведения JVM во время обучающего запуска, сохранения этой информации в кэше и последующей загрузки данных из кэша при новых запусках. Если вам интересно узнать больше о Project Leyden, обязательно посмотрите это видео.
Первая функция Project Leyden — это JEP 483: Ahead-of-Time Class Loading & Linking. Выше мы уже рассмотрели загрузку и связывание классов, так что у вас уже должно сложиться понимание того, какие преимущества дает выполнение этих операций заранее, а не во время запуска.
Заключение
Как было рассмотрено в этой статье, процесс запуска JVM — это сложный механизм. Способность адаптироваться к доступным системным ресурсам, предоставлять средства для инспекции и профилирования, динамически загружать классы и многое другое, сопровождается значительными накладными расходами.
Какие выводы можно сделать из всего сказанного, помимо более глубокого понимания JVM? Стоит отметить два аспекта: отладку и производительность, хотя их применение может быть довольно узким.

Отладка

Процесс запуска JVM отличается высокой надежностью, и если ошибки все же возникают, их причиной обычно является очевидная ошибка пользователя или проблема в сторонней библиотеке. Более глубокое понимание того, что именно делает JVM и почему, может помочь разобраться в сложных или трудно объяснимых проблемах, возникающих при запуске.

Улучшение производительности

Еще одним возможным преимуществом является поиск способов ускорить запуск приложения. Особенно с интеграцией JEP 483 в JDK 24, изменения в механизме загрузки и связывания классов могут еще больше улучшить производительность старта. Однако стоит помнить, что код, который вы пишете, зачастую составляет лишь небольшую часть того, что выполняется в JVM. Между сторонними библиотеками, фреймворками и самим JDK ваш код нередко оказывается лишь верхушкой айсберга.
Первоисточник:
«A Deep Dive into JVM Start-up»
Переводчик: Чимаев Максим
Оцените статью, если она вам понравилась!