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 はオンライン監視診断製品で、全体的な視点からアプリケーションの load、メモリ、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 つのクラスローダーによってのみロードされることを保証します。
親委任メカニズムは、クラスローダーがクラスのロードタスクを受け取ると、下から上に過去にロードされているかどうかを確認し、上から下にロードを試みることを指します。
下に委任してロードすることは、ロードの優先順位の役割を果たします。起動クラスローダーから下にロードを試み、もしそのロードディレクトリにあれば成功します。
例: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 を呼び出してメモリに保存します。
親委任メカニズムを打破する - スレッドコンテキストクラスローダー JDBC の例#
JDBC の DriverManager は異なるドライバを管理します。
DriverManager クラスは rt.jar パッケージにあるため、起動クラスローダー Bootstrap がロードします。しかし、DriverManager は Application にドライバ jar パッケージを委任します。
質問:DriverManager は jar パッケージ内のドライバがどこにあるかをどうやって知るのですか?
サービスプロバイダインターフェース SPI は JDK に内蔵されたサービス発見メカニズムです。
スレッドコンテキストローダーは実際にはアプリケーションクラスローダーです。
見解:JDBC の例では本当に親委任メカニズムを打破しましたか?
- Bootstrap がロードした DriverManager が Application にドライバクラスをロードすることを委任し、親委任を打破しました。
- JDBC は DriverManager がロードされた後、初期化段階を通じてドライバクラスのロードをトリガーします。クラスのロードは依然として親委任メカニズムに従います。なぜなら、Application がロードする際も loadClass メソッドを通過し、このメソッドには親委任メカニズムが含まれているからです。
マクロ的には、親レベルが子レベルに委任していると言えますが、マイクロ的には、実行レベルで子レベルのクラスローダー内部関数ロジックは依然として親委任を通過していますが、親レベルは実行を拒否しているだけです。
親委任メカニズムを打破する - OSGi モジュール化フレームワーク#
Arthas を利用したホットデプロイによるオンラインバグの解決#
注意事項:
- プログラムを再起動すると、バイトコードファイルは元に戻ります。なぜなら、メモリに置き換えられただけで、class ファイルを jar パッケージに入れて更新しない限り。
- retransform を使用してメソッドやフィールドを追加することはできず、実行中のメソッドを更新することもできません。
JDK 9 以降のクラスローダー#
JDK8 以前、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 プログラムにおけるヒープメモリは最大のメモリ領域であり、スレッドで共有されます。
作成されたオブジェクトはすべてヒープ上に存在し、スタック上の局所変数テーブルにはヒープ上のオブジェクトの参照を保存できます。静的変数もヒープオブジェクトの参照を保存でき、静的変数を通じてオブジェクトをスレッド間で共有できます。
ヒープメモリ溢れ#
ヒープメモリのサイズには上限があります。オブジェクトをヒープに放入し続けて上限に達すると、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 () は文字列定数プール内の文字列を返します。存在しない場合は、文字列の参照を文字列定数プールに放入します。
ここでの文字列定数プールは 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 はオブジェクトが回収可能かどうかを判断するために、そのオブジェクトが参照されているかどうかを基にします。オブジェクトが参照されている場合、そのオブジェクトはまだ使用中であり、回収を許可されません。
質問:A と B の相互参照は削除する必要がありますか?
削除する必要