JVM の初歩#
JVM とは何か#
JVM は Java Virtual Machine の略で、本質的にはコンピュータ上で動作するプログラムであり、その役割は Java バイトコードファイルを実行することです。
まず、javac を使用してソースコード.java を.class バイトコードファイルにコンパイルし、その後 java コマンドを使用して JVM を起動し、バイトコードを実行します。JVM は、解釈実行と JIT コンパイル実行を混合してコンピュータ上で最終的に実行します。
JVM の機能#
JVM の機能
- 解釈と実行:
- バイトコードファイル内の命令をリアルタイムで機械語に解釈し、コンピュータに実行させる。
- メモリ管理:
- オブジェクト、メソッドなどに自動的にメモリを割り当てる。
- 自動的なガベージコレクションメカニズムにより、使用されなくなったオブジェクトを回収する。
- JIT コンパイル(Just-In-Time):
- ホットコードを最適化し、実行効率を向上させる。
バイトコードマシンは認識できず、JVM がリアルタイムで機械語に解釈して実行する必要があります。
C 言語を観察すると、.c ソースコードを直接機械語にコンパイルすることは明らかに多くの欠点があります。
そのため、JIT コンパイル機能が追加され、ホットバイトコード命令を解釈して最適化し、メモリに保存することで、後続の呼び出しが可能になり、再度解釈するプロセスを省略し、性能を最適化します。
Java 仮想マシンの仕様#
現在のバージョンの二次開発に必要な仕様を規定しています:class バイトコードファイルの定義、クラスとインターフェースのロードと初期化、命令セットなどの内容。
『Java 仮想マシン仕様』は仮想マシン設計の要求であり、Java 設計の要求ではありません。つまり、仮想マシンは Groovy や Scala など他の言語で生成された class バイトコードファイルの上で動作することができます。
名称 | 作者 | サポートバージョン | コミュニティ活性度(github star) | 特徴 | 適用シーン |
---|---|---|---|---|---|
HotSpot (Oracle JDK 版) | Oracle | すべてのバージョン | 高 (閉源) | 最も広く使用されており、安定して信頼性が高く、コミュニティが活発で JIT が Oracle JDK のデフォルト仮想マシンをサポート | デフォルト |
HotSpot (Open JDK 版) | Oracle | すべてのバージョン | 中 (16.1k) | 上記と同じでオープンソース、Open JDK のデフォルト仮想マシン | デフォルトで JDK に二次開発のニーズがある |
GraalVM | Oracle | 11, 17, 19 企業版は 8 をサポート | 高(18.7k) | 多言語サポート、高性能、JIT、AOT サポート | マイクロサービス、クラウドネイティブアーキテクチャが多言語混合プログラミングを必要とする |
Dragonwell JDK 龍井 | Alibaba | 標準版 8, 11, 17 拡張版 11, 17 | 低 (3.9k) | OpenJDK に基づく強化、高性能、バグ修正、安全性向上 JWarmup、ElasticHeap、Wisp 特性サポート | 電子商取引、物流、金融分野での性能要求が比較的高い |
Eclipse OpenJ9 (元 IBM J9) | IBM | 8, 11, 17, 19, 20 | 低 (3.1k) | 高性能、拡張可能な JIT、AOT 特性サポート | マイクロサービス、クラウドネイティブアーキテクチャ |
HotSpot が最も広く使用されています。
バイトコードファイルの詳細#
Java 仮想マシンの構成#
クラスローダー ClassLoader:コアコンポーネントであるクラスローダーは、バイトコードファイルの内容をメモリにロードする役割を担っています。
ランタイムデータエリア:JVM が使用するメモリを管理し、作成されたオブジェクト、クラスの情報などがこの領域に格納されます。
実行エンジン:JIT コンパイラ、インタプリタ、ガベージコレクタを含みます。実行エンジンはインタプリタを使用してバイトコード命令を機械語に解釈し、JIT コンパイラを使用して性能を最適化し、ガベージコレクタを使用して使用されなくなったオブジェクトを回収します。
ネイティブインターフェース:C/C++ でコンパイルされたメソッドを呼び出します。ネイティブメソッドは Java で native キーワードを使って宣言されます。例:public static native void sleep(long millis) throws InterruptedException;
バイトコードファイルの構成#
バイトコードの確認#
バイトコードファイルビューア jclasslib
バイトコードファイルの構成#
バイトコードファイルの構成部分
- 基本情報:マジックナンバー、バイトコードファイルに対応する Java バージョン番号、アクセス修飾子(public、final など)、親クラスおよびインターフェース情報
- 定数プール:文字列定数、クラスまたはインターフェース名、フィールド名を保存し、主にバイトコード命令で使用されます。
- フィールド:現在のクラスまたはインターフェースで宣言されたフィールド情報
- メソッド:現在のクラスまたはインターフェースで宣言されたメソッド情報、コアコンテンツはメソッドのバイトコード命令です。
- 属性:クラスの属性、例えばソースコードのファイル名、内部クラスのリストなど。
基本情報#
ファイルはファイル拡張子によってファイルタイプを特定することはできません。ファイル拡張子は自由に変更でき、ファイルの内容には影響しません。
ソフトウェアはファイルの最初の数バイト(ファイルヘッダー)を使用してファイルのタイプを検証します。ソフトウェアがそのタイプをサポートしていない場合、エラーが発生します。
Java バイトコードファイルでは、ファイルヘッダーをマジックと呼びます。
Java 仮想マシンは、バイトコードファイルの最初の 4 バイトが 0xcafebabe であるかどうかを検証します。そうでない場合、そのバイトコードファイルは正常に使用できず、Java 仮想マシンは対応するエラーをスローします。
主バージョン番号は、現在のバイトコードバージョンが JVM と互換性があるかどうかを判断するために使用されます。
主副バージョン番号は、バイトコードファイルをコンパイルする際に使用された JDK バージョン番号を指します。主バージョン番号は大バージョン番号を識別するために使用され、副バージョン番号は異なる主バージョン番号の識別に使用されます。
JDK 1.0 - 1.1 は 45.0 - 45.3 を使用しました。
JDK 1.2 以降の大バージョン番号の計算方法は主バージョン番号 - 44 で、例えば 52 の主バージョン番号は JDK 8 です。
互換性のない状況が発生した場合、例えばバイトコードファイルバージョン 52 ですが、JVM バージョンは 50 の場合
- JDK をアップグレードする
- バイトコードファイルの要求バージョンを下げ、依存関係のバージョンを下げるか、依存関係を変更する
一般的には 2 を選択し、依存関係を調整します。JDK のアップグレードは比較的大きな動作であり、互換性の問題を引き起こす可能性があります。
定数プール#
文字列リテラルの占有を節約でき、1 つだけ保存し、文字列は String クラスの定数を保存し、UTF-8 のリテラル定数を指します。
状況:a="abc"; abc="abc"
この場合、UTF-8 のリテラル定数は 1 つだけで、String クラスの定数によって参照され、変数abc
のname
として使用されます。
メソッド#
導入:int i=0;i=i++;
最終的に i の値は何ですか?
局所変数テーブルは宣言の順序に基づいてインデックスとして使用され、ここでの引数 args は 0、i と j は 1 と 2 です。オペランドスタックはオペランドを格納するために使用されます。
int i=0; int j=i+1;
バイトコード命令の解析
-
iconst_0、定数 0 をオペランドスタックに置きます。この時点でスタックには 0 だけがあります。
-
istore_1、オペランドスタックからポップし、局所変数テーブルの 1 番目の位置に保存します。
-
iload_1、局所変数テーブルの 1 番目の位置の数をオペランドスタックに置きます。つまり 0 を置きます。
-
iconst_1、オペランドスタックに定数 1 をプッシュします。
-
iadd、オペランドスタックの上部の 2 つの数を加算し、スタックに戻します。つまりオペランドスタックには定数 1 だけが残ります。
-
istore_2、オペランドスタックの上部の要素 1 をポップし、局所変数テーブルの 2 番目の位置に保存し、変数 j の代入を完了します。
-
return 文が実行され、メソッドが終了し、戻ります。
int i=0; i=i++;
メソッドのバイトコード
int i=0; i=++i;
メソッドのバイトコード
一般的に、バイトコード命令の数が多いほど性能が悪くなります。以下の 3 つの + 1 の性能はどうでしょうか?
int i=0,j=0,k=0;
i++;
j = j + 1;
k += 1;
3 つの典型的なバイトコード生成ですが、実際には JIT コンパイラがこれらをすべてiinc
に最適化する可能性があります。
i++;
(iinc
)j = j + 1;
(iload
,iconst_1
,iadd
,istore
)k += 1;
(iload
,iconst_1
,iadd
,istore
)
フィールド#
フィールドには現在のクラスまたはインターフェースで宣言されたフィールド情報が格納されます。
以下の図では、フィールド a1 と a2 が定義されており、これらのフィールドはフィールドのこの部分に表示され、フィールドの名前、記述子(フィールドの型)、アクセス修飾子(public/private static final など)も含まれます。
属性#
属性は主にクラスの属性を指し、ソースコードのファイル名、内部クラスのリストなどが含まれます。
バイトコードの一般的なツール#
javap は JDK に付属の逆コンパイルツールで、コンソールを通じてバイトコードファイルの内容を確認できます。
直接入力して javap で全てのパラメータを確認し、javap -v xxx.class
で具体的なバイトコード情報を確認します。jar パッケージの場合は、最初にjar –xvf
コマンドで解凍する必要があります。
jclasslib にも IDEA プラグインバージョンがあり、コードコンパイル後のバイトコードファイルの内容を確認できます。
新しいツール:アリババの Arthas
Arthas はオンライン監視診断製品で、全体的な視点からアプリケーションのロード、メモリ、GC、スレッドの状態情報をリアルタイムで確認でき、アプリケーションコードを変更することなくビジネス上の問題を診断し、リフレクションを利用します。
Arthas ドキュメントの jar パッケージをダウンロードして実行するだけです。
関連コマンド
dump -d /tmp/output java.lang.String
バイトコードファイルをローカルに保存します。jad --source-only demo.MathGame
クラスのバイトコードをソースコードに逆コンパイルし、コードを確認するために使用します。
クラスのライフサイクル#
ライフサイクルの概要#
ロード、接続、初期化、使用、アンロード
ロード段階#
-
ロード段階の最初のステップは、クラスローダーがクラスの完全修飾名に基づいて、さまざまなチャネルを通じてバイナリストリームの形式でバイトコード情報を取得することです。プログラマーは Java コードを使用してさまざまなチャネルを拡張できます。
- ローカルディスクからファイルを取得
- 実行時に動的プロキシを生成する、例えば Spring フレームワーク
- Applet 技術を使用してネットワークからバイトコードファイルを取得
-
クラスローダーがクラスをロードした後、JVM はバイトコード内の情報をメソッドエリアに保存し、メソッドエリアには InstanceKlass オブジェクトが生成され、クラスのすべての情報が保存されます。その中には多態性などの特定の機能を実現するための情報も含まれています。
-
JVM は同時にヒープ上にメソッドエリアのデータに類似した java.lang.Class オブジェクトを生成します。これは Java コード内でクラス情報を取得し、静的フィールドデータを保存する役割を果たします。JDK8 以降です。
接続段階#
接続段階は 3 つのサブ段階に分かれています:
- 検証、内容が『Java 仮想マシン仕様』を満たしているかどうかを検証します。
- 準備、静的変数に初期値を割り当てます。
- 解析、定数プール内のシンボル参照をメモリへの直接参照に置き換えます。
接続段階 - 検証#
検証の主な目的は、Java バイトコードファイルが『Java 仮想マシン仕様』の制約を遵守しているかどうかを検出することです。この段階では通常、プログラマーが関与する必要はありません。
- ファイル形式の検証、例えばファイルが 0xCAFEBABE で始まるかどうか、主副バージョン番号が現在の Java 仮想マシンバージョンの要件を満たしているかどうか、JDK バージョンがファイルバージョンより小さくないかどうか。
- メタ情報の検証、例えばクラスには親クラスが必要です(super は null であってはならない)。
- プログラム実行命令の意味の検証、例えばメソッド内の命令実行中に不正な位置にジャンプすること。
- シンボル参照の検証、例えば他のクラスの private メソッドにアクセスしていないかどうか。
接続段階 - 準備#
準備段階では、静的変数 static にメモリを割り当て、初期値を設定します。基本データ型と参照データ型のそれぞれには初期値があります。注意:ここでの初期値は各データ型のデフォルト値であり、コード内で設定された初期値ではありません。
データ型 | 初期値 |
---|---|
int | 0 |
long | 0L |
short | 0 |
char | ‘\u0000’ |
byte | 0 |
boolean | false |
double | 0.0 |
参照データ型 | null |
以下の例では、接続段階 - 準備サブ段階でvalue
にメモリが割り当てられ、初期値 0 が設定され、初期化段階で値が 1 に変更されます。
public class Student {
public static int value = 1;
}
例外は、final 修飾の変数です。final 修飾の変数はその後値が変更されないため、準備段階でコード内の値が割り当てられます。
接続段階 - 解析#
解析段階では、主に定数プール内のシンボル参照を直接参照に置き換えます。シンボル参照はバイトコードファイル内で番号を使用して定数プール内の内容にアクセスします。
直接参照はメモリ内のアドレスを使用して具体的なデータにアクセスします。
初期化段階#
初期化段階では、バイトコードファイル内の clinit(クラスの初期化)メソッドのバイトコード命令が実行され、静的変数への値の割り当てや静的コードブロック内のコードの実行(コードの順序に従って)を含みます。
clinit メソッドの実行順序はコードの順序と一致します。
putstatic 命令は、オペランドスタック上の数をポップし、ヒープ内の静的変数の位置に置きます。バイトコード命令内の #2 は定数プール内の静的変数 value を指し、解析段階で変数のアドレスに置き換えられます。
以下のいくつかの方法がクラスの初期化を引き起こします:
- クラスの静的変数または静的メソッドにアクセスする。注意:変数が final で、等号の右側が定数である場合、クラスの初期化はトリガーされません。なぜなら、この変数は接続段階の準備段階で値が割り当てられているからです。
- Class.forName (String className) を呼び出すことで、引数を使用して初期化を制御できます。
- そのクラスのオブジェクトを new する時。
- Main メソッドを実行する現在のクラス。
Java の起動パラメータに-XX:+TraceClassLoading
を追加すると、ロードおよび初期化されたクラスを印刷できます。
例題
clinit 命令は特定の状況では発生しません。例えば:
- 静的コードブロックがなく、静的変数の代入文がない。
- 静的変数の宣言があるが、代入文がない。
- 静的変数の定義に final キーワードを使用している。このような変数は接続段階の準備段階で直接値が割り当てられます。
継承の状況:
- 親クラスの静的変数に直接アクセスしても、子クラスの初期化はトリガーされません。
- 子クラスの初期化 clinit が呼び出される前に、親クラスの clinit が初期化されます。
例題
子クラスの初期化前に親クラスを初期化します。
親クラスの静的変数に直接アクセスしても子クラスの初期化はトリガーされません。
配列の作成は、配列内の要素のクラスを初期化しません。
public class Test2 {
public static void main(String[] args) {
Test2_A[] arr = new Test2_A[10];
}
}
class Test2_A {
static {
System.out.println("Test2 Aの静的コードブロックが実行されました");
}
}
final 修飾の変数に代入される内容が指示を実行する必要がある場合、clinit メソッドが初期化を実行します。
public class Test4 {
public static void main(String[] args) {
System.out.println(Test4_A.a);
}
}
class Test4_A {
public static final int a = Integer.valueOf(1);
static {
System.out.println("Test4_Aの静的コードブロックが実行されました");
}
}
クラスローダー#
クラスローダー ClassLoader は、JVM がアプリケーションにクラスとインターフェースのバイトコードデータを取得するための技術を提供します。クラスローダーは、ロードプロセス中のバイトコードの取得とメモリへのロードの部分にのみ関与します。
クラスローダーはバイナリストリームの形式でバイトコードファイルの内容を取得し、取得したデータを JVM に渡します。仮想マシンはメソッドエリアとヒープ上に対応するオブジェクトを生成してバイトコード情報を保存します。
クラスローダーの分類#
クラスローダーは 2 種類に分けられます。一つは Java コードで実装されたもので、もう一つは JVM の底層ソースコードで実装されたものです。
JDK 8 以前のバージョン#
クラスローダー BootStrap は、JRE 内のコア jar パッケージをロードし、Java コードからはこの底層の ClassLoader にアクセスできません。
拡張クラスローダー Extension アプリケーションクラスローダー Application は、sun.misc.Launcher 内にあり、静的内部クラスで、URLClassLoader から継承され、ディレクトリまたは指定された jar パッケージを介してバイトコードファイルをメモリにロードする機能を持っています。
-Djava.ext.dirs=jarパッケージのディレクトリ
パラメータを使用して、拡張 jar パッケージのディレクトリを拡張できます。;(windows):(macos/linux)を使用してディレクトリパスを分割します。
アプリケーションクラスローダーは classpath 下のクラスファイルをロードし、デフォルトではプロジェクト内のクラスと maven を介して導入されたサードパーティの jar パッケージ内のクラスをロードします。
クラスローダーの親委譲メカニズム#
Java 仮想マシンには複数のクラスローダーがあるため、親委譲メカニズムの核心は、あるクラスを誰がロードするかを解決することです。
メカニズムの作用:
- 悪意のあるコードが JDK のコアライブラリ(例えば java.lang.String)を置き換えるのを防ぎ、コアライブラリの完全性と安全性を確保します。
- 重複ロードを避け、1 つのクラスが 1 つのクラスローダーによってのみロードされることを保証します。
親委譲メカニズムは、あるクラスローダーがクラスのロードタスクを受け取ると、下から上に過去にロードされたかどうかを確認し、上から下にロードを試みることを指します。
下に委譲してロードすることは、ロードの優先順位を持つことを意味します。起動クラスローダーから下にロードを試み、もしそのロードディレクトリに存在すれば成功します。
例:dev.chanler.my.C が classpath にある場合;Application から上に探し、どれもロードされていない;Bootstrap から下に試みても、どれもロードディレクトリにないため、Application のみが成功してロードできます。C は classpath にあるためです。
問題:
- もしあるクラスが 3 つのクラスローダーのロード位置に重複して存在する場合、誰がロードするべきですか?
- 起動クラスローダーがロードします。親委譲メカニズムに従い、優先順位が最も高いです。
- String クラスは上書きできますか?自分のプロジェクト内で java.lang.String クラスを作成しても、ロードされますか?
- できません。rt.jar パッケージ内の String クラスが起動クラスローダーによってロードされます。
- クラスの親委譲メカニズムとは何ですか?
- あるクラスローダーが特定のクラスをロードしようとすると、下から上に過去にロードされたかどうかを確認し、もしロードされていれば直接返します。最上層のクラスローダーまで確認し、ロードされていなければ、上から下にロードを試みます。
- アプリケーションクラスローダーの親クラスローダーは拡張クラスローダーで、拡張クラスローダーの親クラスローダーは起動クラスローダーですが、コード内では null です。なぜなら、Bootstrap にはアクセスできないからです。
- 親委譲メカニズムの利点は 2 つあります。第一に、悪意のあるコードが JDK のコアライブラリを置き換えるのを防ぎ、例えば java.lang.String のように、コアライブラリの完全性と安全性を確保します。第二に、クラスが重複してロードされるのを防ぎます。
親委譲メカニズムの破壊#
親委譲メカニズムを破る方法は 3 つありますが、本質的には最初の方法だけが本当に親委譲メカニズムを破ることができます:
- カスタムクラスローダー:カスタムクラスローダーを作成し、loadClass メソッドをオーバーライドします。Tomcat はこの方法を使用してアプリケーション間のクラスの隔離を実現します。
- スレッドコンテキストクラスローダー:コンテキストクラスローダーを使用してクラスをロードします。例えば JDBC や JNDI など。
- Osgi フレームワークのクラスローダー:歴史的に Osgi フレームワークは新しいクラスローダーメカニズムを実装し、同じレベル間でのクラスのロードを委譲することを許可しましたが、現在はあまり使用されていません。
親委譲メカニズムの破壊 - カスタムクラスローダー#
Tomcat プログラムは複数の Web アプリケーションを実行できます。もしこれらの 2 つのアプリケーションが Servlet クラスのような同じ限定名を持つ場合、Tomcat はこれらの 2 つのクラスがロードできることを保証し、異なるクラスである必要があります。したがって、親委譲メカニズムを破らなければ、2 つ目の Servlet クラスをロードすることはできません。
Tomcat はカスタムクラスローダーを使用してアプリケーション間のクラスの隔離を実現します。各アプリケーションには、対応するクラスをロードするための独立したクラスローダーがあります。
ClassLoader の 4 つのコアメソッド
public Class<?> loadClass(String name)
クラスロードのエントリポイントで、親委譲メカニズムを提供します。内部でfindClassを呼び出します。重要です。
protected Class<?> findClass(String name)
クラスローダーのサブクラスによって実装され、バイナリデータを取得してdefineClassを呼び出します。例えばURLClassLoaderはファイルパスに基づいてクラスファイル内のバイナリデータを取得します。重要です。
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
クラス名の検証を行い、仮想マシンの底層のメソッドを呼び出してバイトコード情報を仮想マシンのメモリにロードします。
protected final void resolveClass(Class<?> c)
クラスライフサイクルの接続段階を実行します。loadClassはデフォルトでfalseです。
loadClass メソッドはデフォルトで resolve が false で、接続段階と初期化段階を実行しません。
Class.forName はロード、接続、初期化を行います。
親委譲メカニズムを破るためには、loadClass 内のコアロジックを再実装する必要があります。
カスタムクラスローダーの親はデフォルトで AppClassLoader です。
/**
* 親委譲メカニズムを破る - カスタムクラスローダー
*/
public class BreakClassLoader1 extends ClassLoader {
private String basePath;
private final static String FILE_EXT = ".class";
// ロードディレクトリを設定
public void setBasePath(String basePath) {
this.basePath = basePath;
}
// commons ioを使用して指定されたディレクトリからファイルをロード
private byte[] loadClassData(String name) {
try {
String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
FileInputStream fis = new FileInputStream(basePath + tempName + FILE_EXT);
try {
return IOUtils.toByteArray(fis);
} finally {
IOUtils.closeQuietly(fis);
}
} catch (Exception e) {
System.out.println("カスタムクラスローダーのロードに失敗しました。エラーの理由:" + e.getMessage());
return null;
}
}
// loadClassメソッドをオーバーライド
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// javaパッケージの場合は、親委譲メカニズムを使用
if(name.startsWith("java.")){
return super.loadClass(name);
}
// ディスクから指定されたディレクトリからロード
byte[] data = loadClassData(name);
// 仮想マシンの底層メソッドを呼び出し、メソッドエリアとヒープエリアにオブジェクトを作成
return defineClass(name, data, 0, data.length);
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
// 最初のカスタムクラスローダーオブジェクト
BreakClassLoader1 classLoader1 = new BreakClassLoader1();
classLoader1.setBasePath("D:\\lib\\");
Class<?> clazz1 = classLoader1.loadClass("com.itheima.my.A");
// 2番目のカスタムクラスローダーオブジェクト
BreakClassLoader1 classLoader2 = new BreakClassLoader1();
classLoader2.setBasePath("D:\\lib\\");
Class<?> clazz2 = classLoader2.loadClass("com.itheima.my.A");
System.out.println(clazz1 == clazz2);
Thread.currentThread().setContextClassLoader(classLoader1);
System.out.println(Thread.currentThread().getContextClassLoader());
System.in.read();
}
}
問題:2 つのカスタムクラスローダーが同じ限定名のクラスをロードしても、衝突しないのですか?
- 衝突しません。同じ JVM 内では、同じクラスローダー+同じクラス限定名のクラスのみが同じクラスと見なされます。
- Arthas で
sc-d
クラス名の方法を使用して具体的な状況を確認できます。
親委譲メカニズムは loadClass 内にあり、loadClass は findClass を呼び出し、findClass をオーバーライドすることで、バイナリデータを取得して defineClass を呼び出すことで、さまざまなチャネルからバイトコードファイルを合理的にロードすることができます。例えば、データベース内のクラスをロードし、バイナリ配列に変換して defineClass を呼び出してメモリに保存します。
親委譲メカニズムの破壊 - スレッドコンテキストクラスローダー JDBC の例#
JDBC の DriverManager は異なるドライバを管理します。
DriverManager クラスは rt.jar パッケージにあり、起動クラスローダーによってロードされますが、DriverManager は Application にドライバ jar パッケージを委譲します。
問題:DriverManager は jar パッケージ内のドライバがどこにあるかをどうやって知るのですか?
Service Provider Interface SPI は JDK に組み込まれたサービス発見メカニズムです。
スレッドコンテキストローダーはデフォルトでアプリケーションクラスローダーです。
見解:JDBC の例では本当に親委譲メカニズムを破っていますか?
- 起動クラスローダーによってロードされた DriverManager が Application にドライバクラスをロードさせることは、親委譲を破っています。
- JDBC は DriverManager がロードされた後、初期化段階を通じてドライバクラスのロードをトリガーしますが、クラスのロードは依然として親委譲メカニズムに従います。なぜなら、Application がロードする際も loadClass メソッドを通じて行われ、loadClass メソッドには親委譲メカニズムが含まれているからです。
マクロ的には、親レベルが子レベルに委譲されていると言えますが、マイクロ的には、実行レベルで子レベルのクラスローダー内部の関数ロジックは依然として親委譲を通じて進行しています。ただし、親レベルはすべて実行を拒否します。
親委譲メカニズムの破壊 - OSGi モジュール化フレームワーク#
Arthas を利用したホットデプロイでオンラインバグを解決#
注意事項:
- プログラムを再起動すると、バイトコードファイルは元に戻ります。なぜなら、メモリに置き換えられただけで、jar パッケージに class ファイルを置くことで更新しない限り、元に戻ります。
- retransform を使用すると、メソッドやフィールドを追加することはできず、実行中のメソッドを更新することもできません。
JDK 9 以降のクラスローダー#
JDK 8 以前は、Extension と Application は rt.jar パッケージ内の sun.misc.Launcher.java からの URLClassLoader を継承していました。
JDK 9 以降、モジュールの概念が導入され、クラスローダーの設計が大きく変わりました。
起動クラスローダーは Java で実装され、jdk.internal.loader.ClassLoaders クラスにあります。
Java の BootClassLoader は BuiltinClassLoader から継承され、モジュールからロードするバイトコードリソースファイルを見つけることを実装しています。
プラットフォームクラスローダーはモジュール化方式でバイトコードファイルをロードし、継承関係は URLClassLoader から BuiltinClassLoader に変わり、BuiltinClassLoader はモジュールからバイトコードファイルをロードすることを実装しています。主に古いバージョンとの互換性のために使用され、特別なロジックはありません。
JVM メモリ領域 ランタイムデータエリア#
ランタイムデータエリアは、JVM が使用するメモリを管理します。例えば、オブジェクトの作成と破棄などです。
『Java 仮想マシン仕様』は各部分の役割を規定しており、2 つの大きなブロックに分かれています:スレッド非共有とスレッド共有。
スレッド非共有:プログラムカウンタ、Java 仮想マシンスタック、ネイティブメソッドスタック。
スレッド共有:メソッドエリア、ヒープ。
プログラムカウンタ#
プログラムカウンタ Program Counter Register は PC レジスタとも呼ばれ、各スレッドはプログラムカウンタを使用して現在実行中のバイトコード命令のアドレスを記録します。
ケース:
ロード段階で、仮想マシンはバイトコードファイル内の命令をメモリに読み込み、元のファイル内のオフセットをメモリアドレスに変換します。各バイトコード命令はメモリアドレスを持ちます。
コード実行中、プログラムカウンタは次のバイトコード命令のアドレスを記録し、現在の命令が完了した後、仮想マシンの実行エンジンはプログラムカウンタに基づいて次の命令を実行します。ここでは簡単にするためにオフセットを代用していますが、実際のメモリ実行時には保存されるのはアドレスです。
最後の行に到達すると、return 文が実行され、現在のメソッドが終了し、プログラムカウンタにはメソッドの出口のアドレスが置かれ、呼び出したメソッドに戻ります。
したがって、プログラムカウンタはプログラム命令の進行を制御し、分岐、ジャンプ、例外などのロジックを実現します。次に実行する命令のアドレスをプログラムカウンタに置くだけで済みます。
マルチスレッドの場合、プログラムカウンタは CPU が切り替え前に次に解釈実行する命令のアドレスを記録し、切り替え後に続けて解釈実行するのに便利です。
問題:プログラムカウンタは実行中にメモリ溢れを引き起こすことがありますか?
- メモリ溢れは、プログラムが特定のメモリ領域を使用する際に、格納するデータが必要とするメモリサイズが仮想マシンが提供できるメモリ上限を超えることを指します。
- 各スレッドは固定長のメモリアドレスのみを保存するため、プログラムカウンタはメモリ溢れを引き起こすことはありません。
- プログラマーはプログラムカウンタに対して何の処理も行う必要はありません。
JVM スタック#
Java 仮想マシンスタック Java Virtual Machine Stack は、メソッド呼び出し中の基本データを管理するためにスタックデータ構造を使用し、先入れ後出し(First In Last Out)です。各メソッドの呼び出しは、スタックフレーム Stack Frame を使用して保存されます。
Java 仮想マシンスタックのスタックフレームには、主に 3 つの側面が含まれます:
- 局所変数テーブル:局所変数をすべて保存する役割を持ちます。
- オペランドスタック:オペランドスタックはスタックフレーム内で仮想マシンが命令を実行する際に一時データを保存するための領域です。
- フレームデータ:フレームデータには動的リンク、メソッド出口、例外テーブルの参照が含まれます。
局所変数テーブル#
局所変数テーブルは、メソッド実行中にすべての局所変数を保存する役割を持ちます。
局所変数テーブルには 2 種類があります:
- 一つはバイトコードファイル内のもの。
- もう一つはスタックフレーム内のもので、メモリに保存され、スタックフレーム内の局所変数テーブルはバイトコードファイル内の内容に基づいて生成されます。
有効範囲:この局所変数がバイトコード内でアクセス可能な有効範囲。
開始 PC はどのオフセットからこの変数にアクセスできるかを示し、変数が初期化されていることを確認します。
長さは開始 PC からこの局所変数の有効範囲の長さを示します。例えば j の有効範囲は第 4 行のバイトコード return です。
スタックフレーム内の局所変数テーブルは配列であり、1 つの位置は 1 つのスロットを表します。long と double は 2 つのスロットを占有します。
インスタンスオブジェクトの this とメソッドパラメータも局所変数テーブルの先頭にあり、定義順序に従って保存されます。
問題:以下のコードは何スロットを占有しますか?
public void test4(int k,int m){
{
int a = 1;
int b = 2;
}
{
int c = 1;
}
int i = 0;
long j = 1;
}
this、k、m、a、b、c、i、j は 9 つのスロットですか?そうではありません。
スペースを節約するために、局所変数テーブル内のスロットは再利用可能です。ある局所変数がもはや有効でなくなると、現在のスロットは再び使用されることができます。この場合、a、b、c は後で使用されなくなるため、i、j に再利用されます。一方、インスタンスオブジェクト this 参照とメソッドパラメータはメソッドライフサイクル全体にわたって存在するため、それらが占有するスロットは再利用されません。
したがって、局所変数テーブルのスロット数は、実行時に最小限必要なスロット数であり、これはコンパイル時に決定できます。実行中にスタックフレーム内に適切な長さの局所変数テーブル配列を作成するだけで済みます。
オペランドスタック#
オペランドスタックはスタックフレーム内で仮想マシンが命令を実行する際に一時データを保存するための領域であり、スタック構造です。
コンパイル時にオペランドスタックの最大深さを決定でき、そのため実行時に正しくメモリサイズを割り当てることができます。
ケース:オペランドスタックの最大深さは 2 です。
フレームデータ#
フレームデータには動的リンク、メソッド出口、例外テーブルの参照が含まれます。
動的リンク#
現在のクラスのバイトコード命令が他のクラスの属性またはメソッドを参照する場合、シンボル参照の番号を対応するランタイム定数プール内のメモリアドレスに変換する必要があります。
動的リンクは、番号からランタイム定数プールのメモリアドレスへのマッピング関係を保存します。
メソッド出口#
メソッド出口は、メソッドが正常または異常に終了したとき、現在のスタックフレームがポップされ、プログラムカウンタが上のスタックフレーム内の次の命令のアドレスを指すべきことを示します。つまり、呼び出し元の次の行の命令アドレスです。
例外テーブル#
例外テーブルはコード内の例外処理情報を保存し、例外キャッチの有効範囲と例外が発生した後にジャンプするバイトコード命令の位置を含みます。
例:この例外テーブルでは、例外キャッチの開始オフセットは 2、終了オフセットは 4 であり、2 - 4 の実行中にjava.lang.Exception
オブジェクトまたはそのサブクラスオブジェクトがスローされると、それをキャッチし、オフセット 7 の命令にジャンプします。
スタックメモリ溢れ#
JVM スタックがスタックフレームを過剰に占有し、メモリがスタックメモリの最大サイズを超えると、メモリ溢れが発生します。つまり、エラー StackOverflowError です。
仮想マシンパラメータを設定できます-Xss1m
-Xss1024K
1M の仮想マシンスタックメモリは 10676 個のスタックフレームを収容できます。
各バージョンの JVM はスタックサイズに対して要求があります。HotSpot JVM は Windows 64 ビットで JDK 8 に対して最小 180K、最大 1024M を要求します。
ネイティブメソッドスタック#
HotSpot JVM では、Java 仮想マシンスタックとネイティブメソッドスタックは実装上同じスタックスペースを使用し、ネイティブメソッドのパラメータ、局所変数、戻り値などの情報を保存します。
ネイティブメソッドは、C 言語で記述され、JVM 内部で実装され、Java コード内で公開されて呼び出すことが許可されています。
ヒープメモリ#
一般的に、Java プログラムにおいてヒープメモリは最大のメモリ領域です。スレッド共有です。
作成されたオブジェクトはすべてヒープ上に存在し、スタック上の局所変数テーブルにはヒープ上のオブジェクトの参照を格納できます。静的変数もヒープオブジェクトの参照を格納でき、静的変数を介してオブジェクトをスレッド間で共有できます。
ヒープメモリ溢れ#
ヒープメモリのサイズには上限があり、オブジェクトをヒープに放入し続けて上限に達すると、OutOfMemory OOM エラーがスローされます。このコードでは、100M サイズのバイト配列を作成し、ArrayList コレクションに放入し続け、最終的にヒープメモリの上限を超えて OOM エラーがスローされます。
/**
* ヒープメモリの使用と回収
*/
public class Demo1 {
public static void main(String[] args) throws InterruptedException, IOException {
ArrayList<Object> objects = new ArrayList<Object>();
System.in.read();
while (true){
objects.add(new byte[1024 * 1024 * 100]);
Thread.sleep(1000);
}
}
}
3 つの重要な値#
ヒープスペースには 3 つの注目すべき値があります。used、total、max
used は現在使用中のヒープメモリを指し、total は JVM がすでに割り当てた利用可能なヒープメモリ、max は JVM が許可する最大ヒープメモリであり、total は最大で max のサイズに拡張できます。
Arthas ではdashboard -i 更新頻度(5000ms)
コマンドを使用してヒープメモリのこれら 3 つの値 used、total、max を確認できます。
何の仮想マシンパラメータも設定しない場合、max はデフォルトでシステムメモリの 1/4、total はデフォルトでシステムメモリの 1/64 です。
ヒープ内のオブジェクトが増え続けると、used が大きくなり、total 内の使用可能なメモリが不足し、メモリをさらに要求し、上限は max になります。
問題:used = max = total のとき、ヒープメモリは溢れますか?
いいえ、ヒープメモリの溢れの判断条件は複雑であり、GC の説明で詳しく説明します。
ヒープサイズの設定#
ヒープのサイズを変更するには、仮想マシンパラメータ–Xmx
(max 最大値)と-Xms
(初期の total)を使用できます。
構文:-Xmx値 -Xms値
単位:バイト(デフォルト、1024 の倍数でなければならない)、k または K(KB)、m または M(MB)、g または G(GB)
制限:-Xmx
max は 2MB より大きくなければならず、-Xms
total は 1MB より大きくなければなりません。
推奨:-Xmx
max と-Xms
total を同じ値に設定することで、メモリの要求と割り当てのオーバーヘッドを減らし、メモリの過剰後のヒープの収縮を防ぎます。
メソッドエリア#
メソッドエリアは基本情報を保存する場所で、スレッド共有です。含まれるもの:
- クラスのメタ情報、すべてのクラスの基本情報を保存します。
- ランタイム定数プール、バイトコードファイル内の定数プールの内容を保存します。
- 文字列定数プール、文字列定数を保存します。
クラスのメタ情報#
メソッドエリアは各クラスの基本情報、すなわちメタ情報 InstanceKlass オブジェクトを保存します。
クラスのロード段階で完了し、クラスのフィールド、メソッドなどのバイトコードファイル内の内容が含まれ、同時に実行中に使用する必要がある仮想メソッドテーブル(多態性の基礎)などの情報も保存されます。
ランタイム定数プール#
メソッドエリアはクラスのメタ情報を保存するだけでなく、ランタイム定数プールも保存し、定数プールにはバイトコード内の定数プールの内容が保存されます。
バイトコードファイルは番号でテーブルを参照する方法で定数を見つけます。この定数プールは静的定数プールと呼ばれ、定数プールがメモリにロードされると、メモリアドレスを介して定数プール内の内容に迅速にアクセスできます。この定数プールはランタイム定数プールと呼ばれます。
メソッドエリアの実装#
メソッドエリアは『Java 仮想マシン仕様』で設計された仮想概念であり、各 Java 仮想マシンの実装は異なります。Hotspot の設計は以下の通りです:
- JDK 7 以前のバージョンでは、メソッドエリアはヒープ領域内の永続世代スペースに保存され、ヒープのサイズは仮想マシンパラメータで制御されます。
- JDK 8 以降のバージョンでは、メソッドエリアはメタスペースに保存され、メタスペースはオペレーティングシステムが管理する直接メモリ内にあり、デフォルトではオペレーティングシステムが耐えられる上限を超えない限り、常に割り当てることができます。
メソッドエリアの溢れ#
ByteBuddy ツールを使用して動的にバイトコードデータを生成し、メモリにロードし続け、メソッドエリアの溢れをシミュレートします。
文字列定数プール#
メソッドエリアにはクラスのメタ情報、ランタイム定数プールの他に、文字列定数プール StringTable という領域があります。
文字列定数プールはコード内で定義された定数文字列の内容を保存します。例えば「123」の 123 は文字列定数プールに放入されます。
new で作成されたオブジェクトはヒープメモリに保存されます。
初期設計では、文字列定数プールはランタイム定数プールの一部であり、保存位置も一致していました。文字列定数プールとランタイム定数プールは分割され、JDK 7 以降、文字列定数プールはヒープメモリに存在します。
問題:アドレスは等しいですか?
/**
* 文字列定数プールの例
*/
public class Demo2 {
public static void main(String[] args) {
String a = "1";
String b = "2";
String c = "12";
String d = a + b;
System.out.println(c == d);
}
}
指しているのは同じアドレスではありません。
問題:指しているアドレスは同じですか?
package chapter03.stringtable;
/**
* 文字列定数プールの例
*/
public class Demo3 {
public static void main(String[] args) {
String a = "1";
String b = "2";
String c = "12";
String d = "1" + "2";
System.out.println(c == d);
}
}
バイトコードファイルを確認すると、コンパイル段階で 1 と 2 が直接接続されていることがわかります。したがって、指しているのは文字列定数プール内のオブジェクトです。
2 つの問題のまとめ
文字列の変数接続は StringBuilder を使用してヒープメモリに保存されますが、定数接続はコンパイル段階で直接接続されます。
JDK 7 以降、string.intern () は文字列定数プール内の文字列を返します。存在しない場合は、文字列の参照を文字列定数プールに放入します。
ここで、文字列定数プールの java は JVM が自動的に放入します。
問題:静的変数はどこに保存されますか?
- JDK 6 以前のバージョンでは、静的変数はメソッドエリアに保存され、つまり永続世代に存在します。
- JDK 7 以降のバージョンでは、静的変数はヒープ内の Class オブジェクトに保存され、永続世代から脱却しました。
直接メモリ#
直接メモリ Direct Memory は『Java 仮想マシン仕様』には存在せず、Java ランタイムのメモリ領域には含まれません。
JDK 1.4 で NIO メカニズムが導入され、直接メモリが使用され、主に以下の 2 つの問題を解決するために使用されます:
- Java ヒープ内のオブジェクトがもはや使用されなくなった場合、回収される必要があり、回収時にオブジェクトの作成と使用に影響を与えます。
- IO 操作、例えばファイルを読む場合、ファイルを直接メモリ(バッファ)に読み込んでから、データを Java ヒープにコピーする必要があります。
ファイルを直接メモリに放入し、ヒープ上で直接メモリの参照を維持することで、データのコピーオーバーヘッドやファイルオブジェクトの作成回収オーバーヘッドを回避できます。
サイズを割り当てるには、パラメータXX:MaxDirectMemorySize=サイズ
を使用できます。
JVM ガベージコレクション#
C/C++ のような自動ガベージコレクションメカニズムのない言語では、オブジェクトがもはや使用されなくなると、手動で解放する必要があります。そうしないと、メモリリークが発生します。メモリリークは、もはや使用されないオブジェクトがシステム内で回収されないことを指し、メモリリークの蓄積はメモリ溢れを引き起こす可能性があります。
オブジェクトを解放するプロセスはガベージコレクションと呼ばれ、Java はオブジェクトの解放を簡素化するために自動ガベージコレクション GC メカニズムを導入しました。ガベージコレクタは主にヒープ上のメモリの回収を担当します。
問題:ガベージコレクタはどの部分のメモリを担当しますか?
スレッド非共有部分は、スレッドの作成に伴って作成され、スレッドの破棄に伴って破棄されます。メソッドのスタックフレームはメソッドが終了すると自動的にスタックからポップされ、メモリが解放されるため、必要ありません。したがって、ガベージコレクションが必要なのは、スレッド共有のメソッドエリアとヒープです。
メソッドエリアの回収#
メソッドエリアで回収できる内容は主に使用されなくなったクラスです。
クラスがアンロードされるかどうかを判断するには、次の条件を満たす必要があります:
- このクラスのすべてのインスタンスオブジェクトが回収されており、ヒープ内にそのクラスのインスタンスオブジェクトやサブクラスオブジェクトが存在しない。
Class<?> clazz = loader.loadClass (name: "com.itheima.my.A");
Object o = clazz.newInstance ();
◎ = nu11;
- このクラスをロードしたクラスローダーが回収されている。
- このクラスに対応する java.lang.Class オブジェクトがどこにも参照されていない。
2 つの仮想マシンパラメータ-XX:+TraceClassLoading
-XX:+TraceClassUnloading
を使用すると、クラスのロードとアンロード、つまり回収のログを見ることができます。
ガベージコレクションを手動でトリガーする必要がある場合は、System.gc () メソッドを呼び出すことができますが、必ずしも即座にガベージを回収するわけではなく、Java 仮想マシンにガベージコレクションのリクエストを送信するだけです。具体的にガベージコレクションが必要かどうかは Java 仮想マシンが判断します。
ヒープ回収#
参照カウント法と到達可能性分析法#
GC はオブジェクトが回収可能かどうかを判断するために、そのオブジェクトが参照されて