A Deep Dive into JVM Start-up

原文はこちら。
The original article was written by Billy Korando (Developer Advocate at Oracle).
https://inside.java/2025/01/28/jvm-start-up/

Javaアプリケーションを起動すると、実行対象のコードはJVMに渡されたJavaバイトコード、すなわちjavacでコンパイルされた.classファイルだけと信じがちですが、実のところは、起動時にJVMが複雑な一連のステップを踏んで、Javaアプリケーションを実行するための真の環境を作り出します。本記事では、JVMが$ javaからHello Worldの表示までのステップを順を追って説明します。動画形式をご希望の場合は、Java YouTubeチャンネルでもご覧いただけます。

Preamble

このJVMの起動に関するチュートリアルがboil the ocean(無茶)な領域に踏み込まないように、このプロセスを説明するにあたり、いくつかの制約を設けます。

  1. JDK 23における動作に基づいて、JVMの起動プロセスを説明します。Java SE 23のJVM仕様は以下のURLで確認ください。
    The Java® Virtual Machine Specification Java SE 23 Edition
    https://docs.oracle.com/javase/specs/jvms/se23/html/index.html
  2. HotSpot JVM実装を例として使用します。これは最も広く使用されているJVM実装であり、多くの一般的なJDKディストリビューションではHotSpot JVMまたはその派生版が使用されています。他のJVM実装では、内部動作が若干異なる可能性があります。
    HotSpot
    https://en.wikipedia.org/wiki/HotSpot_(virtual_machine)
    https://ja.wikipedia.org/wiki/HotSpot
  3. 最後に、JVMの起動プロセスを説明する際に使用する主なコード例は、HelloWorldです。これは最も単純なアプリケーションですが、JVMの起動プロセスのすべての重要な領域を実行します。

このような制約があるにもかかわらず、この記事を読めば、JVMの起動時に実行されるプロセスと、そのプロセスが必要な理由について、十分に理解できるはずです。この知識は、起動時に問題が発生した場合にアプリケーションをデバッグする際や、ニッチなケースでは起動パフォーマンスの向上にも役立つでしょう。ただし、この点については、記事の終わりの方で少し詳しく説明します。

JVM Initialization(JVM初期化)

ユーザーがjavaコマンドを実行すると、JNI(Java Native Interface)関数のJNI_CreateJavaVM()が呼び出され、JVM起動プロセスを開始します。この関数のコードは以下で確認できます。

jdk/src/hotspot/share/prims/jni.cpp
https://github.com/openjdk/jdk/blob/jdk-23%2B0/src/hotspot/share/prims/jni.cpp#L3661C1-L3661C93

このJNI関数は、それ自体でいくつかの重要な処理を行います。

Validating User Input

JVMの起動プロセスの最初のステップは、ユーザー入力の検証です。JVMの引数、実行対象の成果物、およびクラスパスです。以下のログ出力は、この検証プロセスが発生していることを示しています。

[arguments] VM Arguments:
[arguments] jvm_args: -Xlog:all=trace 
[arguments] java_command: HelloWorld
[arguments] java_class_path (initial): .
[arguments] Launcher Type: SUN_STANDARD

💡Note: このログは、JVMの引数 -Xlog:all=trace を使用しても確認できます。

Detecting System Resources

ユーザー入力を検証後、次のステップでは、利用可能なシステムリソース(プロセッサ、システムメモリ、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

Preparing the Environment

利用可能なシステムリソースを把握した後、JVMは環境準備を始めます。ここで、HotSpot JVM実装が、hsprefdata(HotSpotパフォーマンスデータ)を生成します。このデータは、JConsoleやVisualVMなどのツールがJVMの検査やプロファイルに使用します。このデータは通常、システムの/tmpディレクトリに保存されます。以下は、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

Choosing the Garbage Collector

JVM起動における重要なステップは、ガベージ・コレクター(GC)の選択です。どのGCを使用するかによって、アプリケーションのパフォーマンスに大きく影響します。特に指示がない限り、デフォルトでは、JVMは2個のGC、Serial GCとG1 GCから選択します。

JDK 23では、

  • システムに1792MB未満のシステムメモリしか搭載されていない、または
  • プロセッサが1つしかない

の場合、JVMはSerial GCを選択します。条件を満たさない場合には、JVMはデフォルトでG1 GCを選択します。もちろん、Parallel GC、ZGCなど、他のGCも利用可能です。ただし、利用可能なGCは、使用するJDKのバージョンやディストリビューションによって異なります。また、それぞれの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)は、事前処理済みのクラスファイルのアーカイブであり、JVMの起動パフォーマンス向上に寄与します。CDSがJVMの起動パフォーマンスをどのように向上させるかについては、Class Linkingのセクションで説明します。ただし、「CDS」を記憶に留めないでください。これは廃止予定の機能です。その理由については、JVMの起動パフォーマンスの将来について後ほど説明します。

[cds] trying to map [Java home]/lib/server/classes.jsa
[cds] Opened archive [Java home]/lib/server/classes.jsa.

Creating the Method Area

JVMの最後の初期化ステップの1つに、メソッド領域の作成があります。これは、JVMがロードする際にクラスデータを格納する特別なオフヒープメモリ領域です。メソッド領域はJVMのヒープ内にはありませんが、ガベージコレクタの管理対象です。メソッド領域に格納されたクラスデータは、関連するクラスローダがスコープ内に存在しない場合、削除対象になります。

💡 Note: HotSpot JVM実装を使っている場合、メソッド領域はメタスペース (metaspace) と呼ばれます。

[metaspace,map] Trying to reserve at an EOR-compatible address
[metaspace,map] Mapped at 0x00001fff00000000

Class Loading, Linking, and Initialization(クラスローディング、リンク、初期化)

初期の準備作業が完了すると、JVMの起動プロセスにおける本当の本番が始まります。ここでは、クラスローディング、リンク、初期化が実行されます。

JVM仕様では、これらのプロセスを順を追って説明しています(セクション5.3~5.5)。

5.3. Creation and Loading
https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-5.html#jvms-5.3
5.4. Linking
https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-5.html#jvms-5.4
5.5. Initialization
https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-5.html#jvms-5.5

しかし、これらのプロセスは、特定のクラスに対して、HotSpot JVM上で必ずしもその順序で発生するとは限りません。図の下部に記載されているように、Resolution(解決)はClass Linkingに含まれていますが、Verification(検証)の前からClass Initialization(クラス初期化)の後までのどの時点でも発生する可能性があります。Class Initializationなどの一部のプロセスは、実際には発生しない場合もあります。これらについて以後のセクションで説明します。

Class Loading(クラスローディング)

Class Loadingは、JVM仕様のセクション5.3で説明されています。

5.3. Creation and Loading
https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-5.html#jvms-5.3

クラスローディングは次の3段階のプロセスです。

  • JVMがクラスまたはインターフェースのバイナリ表現を特定
  • 特定したバイナリ表現からクラスまたはインターフェースを導出
  • 導出した情報をJVMメソッド領域にロード

HotSpot JVM実装を使用している場合、メソッド領域は「メタスペース」とも呼ばれます。

JVMの大きな強みのひとつであり、それが広く普及したプラットフォームとなった理由のひとつは、動的にクラスをロードする能力です。これにより、JVMの実行中に必要に応じて生成されたクラスをロードできます。この機能は、SpringやMockitoなど、多くの人気のあるフレームワークやツールで使用されています。実際、InnerClassLambdaMetafactoryクラスで確認できるように、JVM自体もラムダを使う際にオンデマンドでコード生成しています。

jdk/src/java.base/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java
https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java

JVMでは、

の2方法でクラスをロードできます。後者は、java.lang.ClassLoaderクラスを拡張するクラスです。実際には、カスタム・クラス・ローダーは、そのライブラリの動作をサポートするためにサードパーティのライブラリで定義されることが多いでしょう。

本記事では、ブートストラップ・クラスローダーにのみ焦点を当てます。これは、JVMが提供するマシンコードで記述された特別なクラスローダーです。これは、JNI_CreateJavaVM()の後期段階でインスタンス化されます。

クラスローディングのプロセスをよりよく理解するために、JVMが認識するHelloWorldを見てみましょう。

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 { }
}

2個の重要なメソッドはそれぞれ別のクラスを参照します。

メソッド参照するクラス
public final native Class<?> getClass()java.lang.Class
public String toString()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.Serializablejava.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.Serializablejava.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

💡 Note: 一般的に、JVMはプロセス(この場合はクラスローディング)に対してlazy strategy(遅延ロード戦略)を採用しています。通常、クラスは他のクラスから参照された場合にのみロードされますが、java.lang.ObjectはすべてのJavaクラスの特別なルートクラスであるため、JVMはjava.lang.Classjava.lang.Stringを積極的にロードします。java.lang.Classおよびjava.lang.Stringのメソッドシグネチャを見ると、HelloWorldのようなアプリケーションを実行している際には、多くのクラスがロードされていないことに気づくでしょう。

String (Java SE 23 & JDK 23)
https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/Class.html
Class (Java SE 23 & JDK 23)
https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/String.html

例えば、Optional<String> describeConstable() は参照されることがないため、java.util.Optionalはロードされることはありません。これはHotSpotの「遅延ロード」戦略が実際に機能している(あるいは機能していない)例です。

クラスローディングのプロセスは、JVMの起動の大部分を通じて継続し、現実のアプリケーションでは、そのアプリケーションのライフサイクルの初期段階の大部分において継続し、最終的に落ち着くことになります。合計すると、HelloWorldのシナリオではJVMは450のクラスをロードします。これが、JVMの起動時に宇宙が創造されるという例えを用いた理由です。JVMは多くの作業を行っているからです。

それでは、JVMの軌道という宇宙をさらに深く掘り下げていきましょう。次はクラスリンクです。

Class Linking(クラスのリンク)

Class Linkingは、JVM仕様の5.4で取り扱われていますが、3個の異なるサブプロセスを含む複雑なプロセスです。 

クラスリンクには、アクセス制御、メソッドオーバーライド、メソッド選択という3つのプロセスがありますが、この記事では説明しません。

Verificationの図に戻ります。Verification、Preparation、Resolutionは、必ずしもこの記事で取り上げる順番で起こるわけではありません。Resolutionは、Verificationより早い段階で起こることもあれば、クラス初期化(Class Initialization)より遅い段階で起こることもあります。

Verification(検証)

Verificationは、クラスまたはインターフェースが構造的に正しいことを確認するプロセスです。

5.4.1. Verification
https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-5.html#jvms-5.4.1

このプロセスでは、必要に応じて他のクラスのロードが開始される場合がありますが、結果としてロードされたクラスは、検証や準備を行う必要はありません。

CDSの話題に戻りますが、ほとんどの通常の状態では、JDKクラスはVerificationステップを積極的に通過することはありません。これは、アーカイブに含まれるクラスがすでに検証済みであるため、JVMが起動時に実行が必要な作業が減り、結果として起動時のパフォーマンスが向上するからです。CDSが提供する利点の1つです。

CDSについてさらに詳しく知りたい方は、私のStack Walkerのビデオ、dev.javaのCDSに関する記事、またはCDSアーカイブにアプリケーションのクラスを含める方法についてのinside.javaの記事をご覧ください。

CDS and AppCDS in Hotspot
https://dev.java/learn/jvm/cds-appcds/
AppCDS Autogenerate – Sip of Java
https://inside.java/2022/09/26/sip067/

検証が必要なクラスは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(準備)

Preparationでは、クラスの静的フィールドをデフォルト値に初期化します。

5.4.2. Preparation
https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-5.html#jvms-5.4.2

Preparationをより理解するために、以下の単純なクラスの例を使ってみましょう。

class MyClass {
  static int myStaticInt = 10; //Initialized to 0
  static int myStaticInitializedInt; //Initialized to 0
  int myInstanceInt = 30; //Not initialized
  static {
    myStaticInitializedInt = 20;
  }
} 

このクラスには、3個の整数型フィールドがあります。

  • myStaticInt
  • myStaticInitializedInt
  • myInstanceInt

この例では、myStaticIntmyStaticInitializedIntはどちらも、プリミティブ型intのデフォルト値である0に初期化されます。

myInstanceIntはインスタンスフィールドであり、クラスフィールドではないため、初期化されません。

myStaticIntmyStaticInitializedInt1020に初期化されるタイミングについては、少し後で説明します。

Resolution(解決)

Resolutionの目的は、JVM命令で使用されるクラスの定数プール(Constant Pool)内のシンボリック参照を解決することです。

5.4.3. Resolution
https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-5.html#jvms-5.4.3

Resolutionをよりよく理解するために、javapツールを使用します。

The javap Command
https://docs.oracle.com/en/java/javase/23/docs/specs/man/javap.html

これは、Javaの.classファイルを逆アセンブルするための標準 JDK コマンドラインツールです。-verbose オプションを付けて実行すると、JVM がロードするクラスをどのように解釈するのかがわかります。MyClassに対してjavapを実行してみましょう。

$ 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
}

💡 Note: 出力をわずかに省略していますが、それは本記事に関係ないメタデータを削除しているためです。

多くの内容が含まれていますので、ひとつひとつ確認しながら、その意味を理解していきましょう。

以下のセグメントは、MyClassの(自動生成された)デフォルトコンストラクタです。MyClassの親クラスであるjava.lang.Objectのデフォルトコンストラクタの呼び出しから始まり、myInstanceInt30という値に設定します。

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

💡 Note: きっとaload_0invokespecialbipushputfieldなどに気づいたことでしょう。これらはJVM instruction(命令)であり、JVMが実際に作業する際に使うopcode(オペコード)です。

Chapter 6. The Java Virtual Machine Instruction Set
https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-6.html
Opcode
https://en.wikipedia.org/wiki/Opcode

invokespecialputfieldの右側に、それぞれ#1#7の数字があります。これらはMyClassの定数プールへの参照です。

4.4. The Constant Pool
https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-4.html#jvms-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を実行するには、java.lang.Objectのデフォルトコンストラクタへのリンクを解決する必要があります。定数プールを参照すると、エントリー1から6でこのリンクを形成するために必要な情報が提供されています。

💡 Note: <init>は、javacがクラスの各コンストラクタに対して自動的に生成する特別なメソッドです。

このパターンは、定数プールのエントリー7を参照するputfieldでも繰り返されています。エントリー8から12と組み合わせて、myInstanceIntを設定するために必要なリンクを解決するため情報を提供します。定数プールについての詳細は、JVM仕様の該当セクションをご覧ください。

4.4. The Constant Pool
https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-4.html#jvms-4.4

ResolutionプロセスがVerification前からClass Initialization後まで発生しうる理由は、JVMがクラス内のJVM命令を実行しようとする時だけ、解決プロセスが遅延的に実行されるからです。ロードされるすべてのクラスでJVM命令が実行されるわけではありません。例えば、java.lang.SecurityManagerクラスはロードされるが、途中で終了するため、実際には実行されることはありません。

JEP 486: Permanently Disable the Security Manager
https://openjdk.org/jeps/486

また、クラス内に初期化するものが何もなく、JVMによって自動的に初期化されたとマークされることもあります。それはClass Initialization(クラスの初期化)ですが…

Class Initialization(クラス初期化)

最後に、Class Initializationです。これはJVM仕様の5.5に説明があります。

5.5. Initialization
https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-5.html#jvms-5.5

クラスの初期化では以下のことを実施します。

  • 静的フィールドにConstantValueを割り当てる
  • (存在する場合には)クラスの静的イニシャライザを実行する

Class Initializationは、JVMがクラスのnewgetstaticputstaticinvokestaticのいずれかのJVM命令を呼び出したときに開始します。

クラスの初期化は、<init>と同様にjavacが自動的に生成する特別な引数なしのメソッド

void <clinit>

が取り扱います。角括弧(< >)が含まれているのは意図的なもので、これらはメソッド名として有効な文字ではないため、Javaユーザーが独自の<init>メソッドや<clinit>メソッドを書けないようにしています。

 <clinit>メソッドは常に作成されるわけではありません。というのは、このメソッドはクラス内に静的初期化子やフィールドがある場合にのみ必要だからです。クラス内にいずれもない場合、<clinit>メソッドは生成されません。newがそのクラスに対して呼び出された場合、JVMは即座にそのクラスを初期化済みとしてマークし、事実上、クラス初期化をスキップし、クラス初期化後にResolution(解決)が起こる可能性があります。

MyClassには2つの静的フィールドと静的イニシャライザブロックがあるため、<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>に似ていますが、親クラスのコンストラクタへの呼び出しを行わず、putstaticのようなJVM命令をputfieldの代わりを使用する点に違いがあります。

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ミリ秒でした。完全なログは以下からご覧いただけます。

hello-world-jvm-trace-log.txt
https://gist.github.com/wkorando/34294017e1c7653ab9f9ed5c02f017a3

Project Leyden

これは、JVM上のスタートアップにとって、非常にエキサイティングな出来事です。JDKのリリースごとにスタートアッププロセスが継続的に改良されてきましたが、JDK 24から、Project Leydenからの最初の機能がメインラインのJDKリリースに統合されます。

Project Leyden
https://openjdk.org/projects/leyden/

Project Leyden は、以下を削減することを目的としています。CDSで実現した成果をベースにして、さらにそれを上回るものとなる予定です。

  • 起動時間
  • ピークパフォーマンスまでの時間
  • メモリ使用量

Project Leydenが統合されると、AOT(Ahead-of-Time)がCDSに置き換わります。Project Leyden の機能は、

トレーニング実行中にJVMの挙動を記録
その情報をキャッシュに保存
その後の起動時にそのキャッシュから読み込む

ことで機能します。Project Leydenについてさらに詳しく知りたい方は、以下の動画をご覧ください。

Project Leydenの最初の機能は、JEP 483: Ahead-of-Time Class Loading & Linkingです。

JEP 483: Ahead-of-Time Class Loading & Linking
https://openjdk.org/jeps/483

クラス・ローディングとリンクについてはすでにこの記事で取り上げたので、起動時ではなく事前にその作業を行うことの利点は、今ではかなり明確になっているはずです。

Conclusions

本記事で取り上げたように、JVM起動プロセスは複雑です。利用可能なシステムリソースへの対応、JVMの検査やプロファイルを行う手段の提供、クラスの動的ロードなど、かなり複雑なオーバーヘッドが伴います。

JVMに対する深い理解のほかに、この内容から何が得られるのでしょうか? 2つの重要な点として、デバッグとパフォーマンスが挙げられますが、この学びの適用はやや限定的かもしれません。

Debugging

JVM起動プロセスは非常に信頼性が高く、通常、エラーが発生する場合は、その原因の多くは、明らかなユーザーのミスであるか、サードパーティのライブラリの問題です。JVMが何をしようとしているのか、またその理由についてより深く理解できれば、より根強く残る、あるいは理解が難しい起動時の問題について、より良い指針が得られるかもしれません。

Performance Improvement

もう一つの潜在的な利点は、この知識があれば、アプリケーションの起動パフォーマンスを改善する小さな機会を見つけられるかもしれないということです。特に、JEP 483がJDK 24に統合されたことで、今後はクラスローディングとリンクの動作が改善され、起動パフォーマンスがさらに向上する可能性があります。ただし、ほとんどのユースケースでは、あなたが作成する1次コードは、JVM上で実行されるコードのごく一部であることが多いという点には注意が必要です。ライブラリ、フレームワーク、JDK自体のうち、アプリケーションを構成するコードは氷山の一角にすぎないことが多いからです。

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください