研究ノート: Java 仮想マシン - JVM メモリ構造、ガベージ コレクション、クラス ロード、およびバイトコード テクノロジ

学習ビデオソース: https://www.bilibili.com/video/BV1yE411Z7AP
Java クラスロードメカニズムと ClassLoader の詳細な説明 推奨記事: https://yichun.blog.csdn.net/article/details/102983363

学習メモ - JVM


序文

学習ビデオアドレス

ここに画像の説明を挿入します


1.JVMとは何ですか?

定義: Java 仮想マシン - Java 仮想マシン (Java プログラムの実行環境)

アドバンテージ:

  1. 一度コンパイルすればどこでも実行可能
  2. 自動メモリ管理、ガベージコレクション機能
  3. 配列の添字の範囲外チェック
  4. ポリモーフィズム

JDK、JRE、JVM を比較します。
ここに画像の説明を挿入します


2. JVMのメモリ構造

1. プログラムカウンターレジスタ

プログラムカウンタ(レジスタ、プログラムカウンタレジスタ)

機能: 次の JVM 命令の実行アドレスを記録する
特徴: 1. スレッドプライベート、2. メモリオーバーフローが発生しない。

以下の図では、左側はバイナリ バイトコードと JVM 命令、右側は Java ソース コードです。
ここに画像の説明を挿入します

2. 積み重ねる

Java仮想マシンスタック

実行時に各スレッドが必要とするメモリは、Java 仮想マシン スタックに対応します。各スタックは、各メソッド呼び出しによって占有されるメモリに対応する複数のスタック フレーム (フレーム) で構成されます。各スタックには 1 つのアクティビティのみが存在できます。スタックフレームは現在実行中のメソッドに対応します。スレッド非公開です。

Q&A

  1. ガベージ コレクションにはスタック メモリが関係しますか?
    スタック フレーム メモリは、スタック フレームがスタックからポップされると解放され、ガベージ コレクションは必要ありません。ガベージ コレクションはヒープ内のオブジェクトをリサイクルすることのみを担当します。
  2. スタックメモリの割り当ては大きいほうが良いのでしょうか?
    割り当てられるスタック メモリが大きいほど、より多くの再帰呼び出しを実行できますが、これは同時に実行されるスレッドの数に影響を与える可能性があります。500MB のメモリがあり、各スタック メモリに 1MB が割り当てられている場合、実行可能なスレッドの理想的な数は 500、各スタック メモリは 2MB、実行可能なスレッドの理想的な数は 250 です。一般に、Linux システムのデフォルトのスタック メモリは 1024KB です。
  3. メソッド内のローカル変数はスレッドセーフですか?
    ローカル変数は、共有されるのではなく、各スレッドに対してプライベートです。変数がメソッドのスコープをエスケープしない場合、スレッド セーフティの問題は発生しません。それ以外の場合、変数の共有によりスレッド セーフティの問題が発生する可能性があります。
  4. スタック メモリ オーバーフロー (java.lang.StackOverflowError)の原因
    : 1. スタック フレームが多すぎる; 2. スタック フレームが大きすぎる

ネイティブメソッドスタック

ネイティブ メソッド スタックの実装は Java 仮想マシン スタックに似ていますが、ネイティブ メソッドを提供します。仮想マシンの仕様では、ローカル メソッド スタック内のメソッドの言語、使用法、およびデータ構造が必須ではないため、特定の仮想マシンが自由に実装できます。一部の仮想マシン (Sun HotSpot 仮想マシンなど) では、ローカル メソッド スタックと仮想マシン スタックを 1 つに直接結合することもあります。スレッドも非公開です。

Q&A

  1. CPU 使用率が過剰なスレッドやコードを見つけるにはどうすればよいですか?
    まず、topこのコマンドを使用して、どのプロセスが CPU を過剰に占有しているかを特定し、次に、ps H -eo pid,tid,%cpu | grep $pidこのコマンドを使用して、どのスレッドが CPU を過剰に占有しているかを特定します。最後に、スレッドの tid を 16 進数に変換して、その nid を取得しますjstack $pid。スレッドを検索し、特定の実行クラスとコード行を見つけます。この時点で、コードのどこに問題があるかがわかります。

  2. プログラムが長時間実行されても結果が得られません。
    デッドロックがこの問題の原因であると考えられます。jstack $pidコマンドを使用して分析することができます (またはjconsole)


3. ヒープ

ヒープ

new キーワードによって作成されたオブジェクトはヒープ内に置かれ、ヒープ メモリを使用します。
特徴: 1. スレッド共有、2. ガベージ コレクション メカニズムがあります。

ヒープ メモリ オーバーフロー (java.lang.OutOfMemoryError: Java Heap Space)
ヒープ内の使用済みオブジェクトの合計数がメモリを占有しており、ヒープ メモリの最大制限を超えています。
ヒープ メモリ診断ツールとコマンド:

  • jps ツールは、
    現在のシステムにどの Java プロセスがあるかをチェックします。jps
  • jmap ツールは
    この時点でヒープ メモリの使用量をチェックします。jmap -heap $pid
  • jconsole ツールは、
    ヒープ メモリの使用状況を動的に監視する多機能グラフィカル インターフェイス ツールです。jconsole
  • jvirsualvm ツールは
    、ヒープ メモリの使用状況を動的に監視する超多機能グラフィカル インターフェイス ツールです。jvirsualvm

次のコード ケースを監視オブジェクトとして使用できます。

public static void main(String[] args) throws InterruptedException {
    
    
	System.out.println("step 1");
	Thread.sleep(30000);
	byte[] megaArray = new byte[10 * 1024 * 1024]; // 10MB
	
	System.out.println("step 2");
	Thread.sleep(20000);
	array = null;
	System.gc();
	
	System.out.println("step 3");
	Thread.sleep(1000000L);
}

4.メソッドエリア

メソッドエリア

メソッド領域には、クラス構造、クラスのメンバ変数、メソッドデータ、メンバメソッド、コンストラクタメソッドのコード部分に関する情報が格納されます。これは論理的にはヒープの一部ですが、各 JVM の実装では必ずしもそうではありません。メソッド領域はスレッドで共有されます(メソッド領域は永続世代とメタスペースの仕様、永続世代は1.8以前のHotspotの実装、メタスペースは1.8以降の実装に属します)

JDK1.6、1.7、1.8 JMM の違い

JDK 1.6:プログラムカウンタ、Java仮想マシンスタック、ローカルメソッドスタック、ヒープ、メソッド領域[永続世代](文字列定数プール、静的変数、ランタイム定数プール、クラス定数プール) JDK 1.7:プログラムカウンタ、Java仮想マシンスタック
、ローカル メソッド スタック、ヒープ (文字列定数、静的変数)、メソッド領域 [永続世代] (ランタイム定数プール、クラス定数プール) JDK 1.8: プログラム カウンタ、Java 仮想マシン スタック、ローカル メソッド スタック、ヒープ (文字列定数)
、メタデータ (静的変数、ランタイム定数プール、クラス定数プール)
ここに画像の説明を挿入します

メソッド領域のメモリオーバーフロー

  • 1.8 より前には永続世代メモリ オーバーフローがありました: java.lang.OutOfMemoryError: PermGen Space
    -XX:MaxPermSize=8m

  • 1.8 以降ではメタスペース メモリ オーバーフローが発生します: java.lang.OutOfMemoryError: Metaspace
    -XX:MaxMetaspaceSize=8m

コード例:

/**
 * 由于方法区在1.8后的元空间实现使用的是系统内存,
 * 而我的PC使用的是16G内存,很难触发该问题,所以需添加如下参数,以指定最大元空间内存
 * -XX:MaxMetaspaceSize=8m(如果是1.8之前,则需指定-XX:MaxPermSize=8m)
 */
public class Demo extends Classloader {
    
    
	public static void main(String[] args) {
    
    
		int n = 0;
		try {
    
    
			Demo demo = new Demo();
			for (int i = 0; i < 10000; i ++, n ++) {
    
    
				// ClassWriter可用作生成类的二进制字节码
				ClassWriter cw = new ClassWriter(0);
				// 版本号,访问权限,类名,包名,父类,接口
				cw.visit(Opcodes.V1_8, Opcodes.ACC_PLUBLIC, "Class" + i, null, "java/lang/Object", null);
				// 返回byte数组
				byte[] code = cw.toByteArray();
				// 执行加载类
				demo.defineClass("Class" + i, code, 0, code.length);
			}
		} finally {
    
    
			System.out.println(n);
		}
	}
}

5. その他

5.1 定数プール

定数プールはテーブルとして理解でき、仮想マシンの命令はこのテーブルを使用して、実行するクラス名、メソッド名、パラメータの型、リテラルなどの情報を見つけます。

5.2 ランタイム定数プール

クラスがロードされると、その定数プール情報が実行時定数プールに入れられ、そのシンボリック アドレスが実際のアドレスに変更されます。

Java プログラム:

public class StringTableTest {
    
    
    public static void main(String[] args) {
    
    
        String a = "a";
        String b = "b";
        String ab = "ab";
    }
}

バイトコード ファイルに逆コンパイルします。

ziang.zhang@ziangzhangdeMacBook-Pro test % javap -v StringTableTest.class
Classfile /Users/ziang.zhang/dreamPointer/JavaSpace/JavaProjects/data-structures-and-algorithms/out/production/leetcode/test/StringTableTest.class
  Last modified 2022-10-3; size 496 bytes
  MD5 checksum 92426c26340f906a2a36f096f6c1fd33
  Compiled from "StringTableTest.java"
public class test.StringTableTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#24         // java/lang/Object."<init>":()V
   #2 = String             #18            // a
   #3 = String             #20            // b
   #4 = String             #21            // ab
   #5 = Class              #25            // test/StringTableTest
   #6 = Class              #26            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Ltest/StringTableTest;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               a
  #19 = Utf8               Ljava/lang/String;
  #20 = Utf8               b
  #21 = Utf8               ab
  #22 = Utf8               SourceFile
  #23 = Utf8               StringTableTest.java
  #24 = NameAndType        #7:#8          // "<init>":()V
  #25 = Utf8               test/StringTableTest
  #26 = Utf8               java/lang/Object
{
    
    
  public test.StringTableTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ltest/StringTableTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=4, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: return
      LineNumberTable:
        line 6: 0
        line 7: 3
        line 8: 6
        line 10: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  args   [Ljava/lang/String;
            3       7     1     a   Ljava/lang/String;
            6       4     2     b   Ljava/lang/String;
            9       1     3    ab   Ljava/lang/String;
}
SourceFile: "StringTableTest.java"

プログラムが実行されると、定数プールの情報が実行時定数プールにロードされますが、このとき定数プールには a、b、ab がシンボルとして存在します。バイトコード命令は、ldc # 2文字列定数プールにシンボルの文字列オブジェクトが含まれているかどうかを確認し、含まれている場合はそれを使用し、含まれていない場合は文字列オブジェクトを作成して格納します。可視文字列の作成は遅延します

5.3 文字列定数プール(StringTable)

特性

  • 定数プール内の文字列は単なるシンボルであり、初めて使用されるときはオブジェクトになります。
  • 文字列定数プールメカニズムを使用して、文字列オブジェクトの繰り返し作成を回避します。
  • 文字列変数のスプライシングは StringBuilder (1.8) を使用します
  • 文字列定数のスプライシングはコンパイル時の最適化に基づいています
  • intern() メソッドを使用すると、文字列定数プールにない文字列オブジェクトを積極的に入れることができます。1.8
    : 文字列オブジェクトが存在しない場合は、それを入れます。存在する場合は、入れません。 value はプール内のオブジェクトです
    1.6: 文字列オブジェクトが無い場合はコピーします あれば入れません 戻り値はプール内のオブジェクトです

位置

JDK1.6:定数プールに配置され、定数プールはメソッド領域(永続世代)に配置されます。
JDK1.8:ヒープに配置されます。
ここに画像の説明を挿入します

StringTable ガベージ コレクション

場合:

// 运行前添加JVM参数:-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {
    
    
	int i = 0;
	try {
    
    
		for (int j = 0; j < 10000; j ++) {
    
    
			String.valueOf(j).intern();
			i ++;
		}
	} catch (Exeception e) {
    
    
		e.printStackTrace();
	} finally {
    
    
		System.out.println(i);
	}
}

StringTable のパフォーマンス調整

  • -XX:StringTableSize=バケット数を調整
    StringTable は本質的にはハッシュ テーブルであり、バケットが多いほどハッシュ衝突の確率が小さくなり、検索速度が速くなります。このパラメータを使用して、テーブル内のバケットの数を調整できます。
  • 文字列をプールに入れるかどうかを検討してください
    。重複する文字列が多数ある場合、 intern() を適切に使用すると、重複した文字列オブジェクトの作成を防ぐことができ、メモリ使用量を削減できます。

Q&A

  1. 次のコードの実行結果を確認します。
    public static void main(String[] args) {
          
          
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString();
        String s5 = "a" + "b"; // javac在编译期的优化,在编译期就可以确定其值为"ab"
    
    	System.out.println(s3 == s4 + ", " + s3 == s5);
    }
    
    false、true
    は、文字列 s4 の作成は基本原則として StringBuilder に基づいているため、文字列 s5 の作成はコンパイル時の最適化に基づいているため、文字列オブジェクト "ab" は s3 の作成時に文字列定数プールに配置されているためです。 , s5 参照されるオブジェクトも文字列定数プール内にある「ab」です。
  2. 次のコードの実行結果を確認します。
    public static void main(String[] args) {
          
          
        // 动态拼接的字符串仅存在于堆中,字符串常量池中并不存在
        String s1 = new String("a") + new String("b");
        String s2 = "ab";
        // 尝试将此字符串对象放入字符串常量池,若已存在则不放入,不存在则放入。返回池中的对象
        String s3 = s1.intern();
        
        System.out.println(s1 == "ab" + ", " + s1 == s3 + ", " + s2 == s3);
    }
    
    false、false、true
    は、s1 がオブジェクトから動的に結合され、最終的に生成された「ab」はヒープ内に存在しますが、文字列定数プールには存在しないため、s2 はランタイムによって作成された文字列オブジェクトであり、プール。s3 は文字列定数プールから取得され、s2 オブジェクトと同じです

5.4 ダイレクトメモリ(DirectMemory)

BIO: Java プログラム自体はディスクの内容を直接読み取りませんが、オペレーティング システムの API に依存して実行します。このとき、CPU はユーザー モードからカーネル モードに切り替わり、ディスク ファイルをバッファするためにメモリ内にシステム バッファが開かれ、Java プログラムはバイト配列を使用してバッファを再度作成し、システム バッファ内のデータを読み取ります。
ここに画像の説明を挿入します

NIO: Java ヒープ メモリとシステム メモリ間の通信用にダイレクト メモリを作成し、このダイレクト メモリからディスク ファイルのデータを直接読み取ることができるため、効率が大幅に向上します。
ここに画像の説明を挿入します

特徴:

  • 一般的に NIO 操作で使用され、データ バッファーに使用されます。
  • 割り当てと回復のコストは高くなりますが、読み取りと書き込みのパフォーマンスは高くなります
  • これは JVM メモリのリサイクルによって管理されず、そのリリースは仮想参照メカニズムを借用します。

流通とリサイクルの原則

  • Unsafe オブジェクトを使用して、直接メモリの割り当てとリサイクルを完了します。リサイクルする場合は、Unsafe クラスの freeMemory メソッドを積極的に呼び出す必要があります。
  • ByteBuffer の実装クラスは Cleaner (仮想参照) を使用して ByteBuffer オブジェクトを監視します。ByteBuffer オブジェクトがガベージ コレクションされると、ReferenceHandler スレッドは Cleaner の clean メソッドを通じて freeMemory を呼び出し、ダイレクト メモリを解放します。

OutOfMemoryError: メモリがオーバーフローするとダイレクト バッファ メモリ エラーが発生します


3. ガベージコレクション

1. オブジェクトがリサイクル可能かどうかを判断する方法

1.1 参照カウント方法

参照カウントの方法は、オブジェクトに参照カウンタを追加し、追加のメモリ領域を使用して各オブジェクトが参照された回数を保存することです。オブジェクトが特定の場所から参照されると、オブジェクトの参照カウントが増加します。逆に、参照が失敗するたびに、オブジェクトの参照数は 1 ずつ減ります。オブジェクトへの参照数が 0 の場合、そのオブジェクトは再度使用されないと考えることができます。このようにして、これらのリサイクル可能な物体を迅速かつ直観的に見つけて洗浄することができます。

参照カウントの不完全性

  1. 循環参照の問題を解決できません。
    参照カウント方法は非常に直観的で効率的ですが、特殊なケースでは、参照カウント方法を使用して「リサイクル可能な」オブジェクトをスキャンすることは不可能です。この特殊なケースは、オブジェクトに循環参照がある場合です。参照 (オブジェクト A など)。B が参照され、B オブジェクトは A を参照します。さらに、これら 2 つは他のオブジェクトから参照されていません。実際、オブジェクトのこの部分も「リサイクル可能な」オブジェクトです。ただし、参照カウント方法では特定できません。
    ここに画像の説明を挿入します

  2. もう 1 つの側面は、参照カウント方法では、各オブジェクトが参照された回数を記録するために追加のスペースが必要であり、この参照数にも追加のメンテナンスが必要であることです。

1.2 到達可能性の分析

到達可能性分析手法は、すべての「GC ルート」オブジェクトを開始点とします。GC ルートの参照を通じてオブジェクトを追跡できない場合、これらのオブジェクトは再度使用されないと考えられます。現在主流のプログラミング言語到達可能性分析を通じてオブジェクトが生きているかどうかを判断します。

ここに画像の説明を挿入します
どのオブジェクトを「GC ルート」オブジェクトと呼びますか? もちろん、通常のオブジェクトは絶対に機能しません。それを GC ルート オブジェクトとして使用する場合は、条件を満たしている必要があります。つまり、それ自体が GC ルート オブジェクトとして使用される必要があります。長期間保存されているため、GC によってリサイクルされません。この条件を満たすオブジェクトのみを GC ルートとして使用できます。GC ルートの種類は大まかに次のとおりです。

  1. 仮想マシン スタック内のローカル変数によって参照されるオブジェクト。

  2. メソッド領域の静的プロパティによって参照されるオブジェクト。

  3. メソッド領域の定数によって参照されるオブジェクト。

  4. ネイティブメソッド(Nativeメソッド)で参照されるオブジェクト。

  5. 仮想マシン内の参照オブジェクト(クラスレコーダー、基本データに対応するクラスオブジェクト、例外オブジェクト)。

  6. 同期ロック (Synchronnized) によって保持されるすべてのオブジェクト。

  7. 仮想マシンの内部状態を記述するオブジェクト (JMXBean、JVMTI に登録されたコールバック、ローカル キャッシュ コードなど)。

  8. ガベージ コレクターによって参照されるオブジェクト

1.3 4 種類の参照

  1. 強力な参照
    • GC ルート オブジェクトが [強参照] を通じてオブジェクトを参照していない場合にのみ、オブジェクトをガベージ コレクションできます。
  2. ソフトリファレンス
    • ソフト参照のみがオブジェクトを参照している場合、ガベージ コレクション後、メモリがまだ不足している場合は、ソフト参照オブジェクトをリサイクルするために再度ガベージ コレクションがトリガーされます。
    • 参照キューを使用してソフト参照自体を解放できます。
  3. 弱い参照
    • 弱参照のみがオブジェクトを参照している場合、ガベージ コレクション中に、メモリが十分であるかどうかに関係なく、弱参照オブジェクトがリサイクルされます。
    • 参照キューと一緒に使用して、弱い参照自体を解放できます。
  4. ファントムリファレンス
    • 参照キュー、主に ByteBuffer で使用する必要があります。参照されたオブジェクトがリサイクルされると、仮想参照がキューに入れられ、参照ハンドラー スレッドが仮想参照関連のメソッドを呼び出してダイレクト メモリを解放します。
  5. 最終リファレンス
    • 手動コーディングは必要ありませんが、参照キューとともに内部で使用されます。ガベージ コレクション中に、ファイナライザー参照がキューに入れられ (参照されたオブジェクトはまだリサイクルされていません)、その後、ファイナライザー スレッドがファイナライザー参照を参照して参照されたオブジェクトを見つけ、がその Finalize メソッドを呼び出すと、参照されたオブジェクトは 2 回目の GC 中にのみリサイクルできます。

ソフトリファレンス例

/**
 * 演示软引用,配合引用队列
 * -Xmx20M -XX:+PrintGCDetails -verbose:gc
 */
public static void main(String[] args) {
    
    
	List<SoftReference<byte[]>> list = new ArrayList<>();

	// 引用队列
	ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

	for (int i = 0; i < 5; i ++) {
    
    
		// 关联引用队列:当软引用所引用的byte[]被回收后,该软引用会自动进入queue
		SoftReference<byte[]> softRef = new SoftReference<>(new byte[4 * 1024 * 1024], queue);
		System.out.println(softRef.get());
		list.add(softRef);
	}

	// 在queue中poll出无用的软引用对象,并在list中remove掉
	Reference<? extends byte[]> poll = queue.poll();
	while (poll != null) {
    
    
		list.remove(poll);
		poll = queue.poll();
	}

	for (SoftReference<byte[]> softRef : list) {
    
    
		System.out.println(softRef.get());
	}
}

弱参照の例

/**
 * 演示弱引用
 * -Xmx20M -XX:+PrintGCDetails -verbose:gc
 */
public static void main(String[] args) {
    
    
	List<WeakReference<byte[]>> list = new ArrayList<>();

	for (int i = 0; i < 5; i ++) {
    
    
		WeakReference<byte[]> weakRef = new WeakReference<>(new byte[4 * 1024 * 1024]);
		list.add(weakRef);
		// 输出每次add后的list内容
		for (WeakReference<byte[]> one : list) {
    
    
			System.out.println(one.get() + " ");
		}
		System.out.println();
	}

	System.out.println("running over, list size: " + list.size());
}

出力結果:

[B@7ea987ac 
[B@7ea987ac [B@12a3a380 
[B@7ea987ac [B@12a3a380 [B@29453f44 
[B@7ea987ac [B@12a3a380 [B@29453f44 [B@5cad8086 
[GC (Allocation Failure) --[PSYoungGen: 5632K->5632K(6144K)] 17920K->17957K(19968K), 0.0033787 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 5632K->0K(6144K)] [ParOldGen: 12325K->393K(8704K)] 17957K->393K(14848K), [Metaspace: 3153K->3153K(1056768K)], 0.0027499 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
null null null null [B@6e0be858 
null null null null [B@6e0be858 [B@61bbe9ba 
null null null null [B@6e0be858 [B@61bbe9ba [B@610455d6 
null null null null [B@6e0be858 [B@61bbe9ba [B@610455d6 [B@511d50c0 
[GC (Allocation Failure) --[PSYoungGen: 4380K->4380K(6144K)] 17061K->17061K(19968K), 0.0004614 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 4380K->0K(6144K)] [ParOldGen: 12681K->374K(13824K)] 17061K->374K(19968K), [Metaspace: 3158K->3158K(1056768K)], 0.0028986 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
null null null null null null null null [B@60e53b93 
null null null null null null null null [B@60e53b93 [B@5e2de80c 
running over, list size: 10
Heap
 PSYoungGen      total 6144K, used 4378K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 5632K, 77% used [0x00000007bf980000,0x00000007bfdc6828,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
  to   space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
 ParOldGen       total 13824K, used 4470K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)
  object space 13824K, 32% used [0x00000007bec00000,0x00000007bf05da48,0x00000007bf980000)
 Metaspace       used 3169K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 349K, capacity 388K, committed 512K, reserved 1048576K

2. ガベージコレクションアルゴリズム

到達可能性分析を使用してオブジェクトが生きているかどうかを判断する場合、何らかの戦略を使用してこれらの死んだオブジェクトをクリーンアップしてから、生き残っているオブジェクトを分類する必要があります。このプロセスには、マーク クリア法、マーク コピー法、マーク ソートという 3 つのアルゴリズムが含まれます。方法。

2.1 マークとクリアの方法

マークとクリアの方法は、まずメモリ内で生き残っているオブジェクトを見つけてマークし、その後マークされていないオブジェクトを均一にクリーンアップすることであり、プロセスは大まかに次のとおりです。
ここに画像の説明を挿入します
マーク アンド クリアの利点: マーク アンド クリア方式は、そのシンプルさ、直接性、および非常に高速な速度が特徴で、多くのオブジェクトが残存し、リサイクルする必要があるオブジェクトがほとんどないシナリオに適しています。

マークのクリアが不十分

  1. 会造成不连续的内存空间:就像上图清除后的内存区域一样,清除后内存会有很多不连续的空间,这也就是我们常说的空间碎片,这样的空间碎片太多不仅不利于我们下次分配,而且当有大对象创建的时候,我们明明有可以容纳的总空间,但是空间都不是连续的造成对象无法分配,从而不得不提前触发GC。

  2. 性能不稳定:内存中需要回收的对象,当内存中大量对象都是需要回收的时候,通常这些对象可能比较分散,所以清除的过程会比较耗时,这个时候清理的速度就会比较慢了。

2.2 标记复制法

标记清除法最大问题是会造成空间碎片,同时可回收对象如果太多也会影响其性能,而标记复制法则可以解决这两大问题。标记清除法的关注点在可回收的对象身上,而标记复制法的关注点则放在了存活的对象身上,通过把存活的对象转移到一个新的区域,然后对原区域的对象进行统一清理。

首先它把内存划分出三块区域,一块用于存放新创建的对象叫Eden区,另外两块则用于存放存活的对象分别叫 S1区和S2区。回收的时候会有两种情况,一种是把Eden和S1区的存活对象复制到S2区,第二种是把Eden和S2区的存活对象复制到S1区 ,也就是说S1区和S2这两块区域同时只会有一块使用,通过这种方式保证始终会有一块空白的区域用于下次GC时存放存活的对象,而且原来的区域不需要考虑保留存活的对象,所以可以直接一次性清除所有对象,这要既简单直接同时也保证了清除的内存区域的内存连续性。

ここに画像の説明を挿入します

标记复制法的优势
标记复制法解决了标记清除法的空间碎片问题,并且采用移动存活对象的方式,每次清除针对的都是一整块内存,所以清除可回收对象的效率也比较高,但因为要移动对象所以这里会耗费一部分时间,所以标记复制法效率还是会低于标记清除法。

标记复制法的不足

  1. スペースを無駄にします: 上の図から、使用できない空きメモリ領域が常に存在することがわかりますが、これもリソースの無駄になります。

  2. 多数のオブジェクトが存続すると、非常に時間がかかります。オブジェクトのコピーと移動のプロセスには時間がかかるため、オブジェクト自体を移動するだけでなく、これらのオブジェクトを使用して参照アドレスを変更する必要があるため、多数のオブジェクトが存続するシナリオでは、したがって、マーク付きコピー方法は、生き残るオブジェクトが少ないシナリオに適しています。

  3. 保証メカニズムが必要です。コピー領域には常に無駄なスペースが存在するため、無駄なスペースを減らすために、コピー領域のスペース割り当てを狭い範囲に制御しますが、スペースが小さすぎると、多くのオブジェクトが残っている場合、コピー領域のスペースがこれらのオブジェクトを収容するのに十分でない可能性があります。この場合、これらのオブジェクトが確実に収容されるようにスペースを借用する必要があります。この借用方法は、他の場所からのメモリは保証メカニズムと呼ばれます。

2.3 マーキングと分別方法

マーク コピー方法は、マーク クリア方法の欠点を完全に補完し、スペースの断片化の問題を解決するだけでなく、ほとんどのオブジェクトがリサイクル可能なシナリオでの使用にも適しています。ただし、マーク コピー方法にも欠点があり、オブジェクトを移動するためにメモリ領域を解放する必要がある一方で、多くのオブジェクトが残っているシーンにはあまり適していません。通常はマークの使用に適しています。クリア方式ですが、マークとクリア方式ではスペースの断片化が発生し、これは許容できない問題です。

したがって、スペースの断片化やメモリスペースの無駄を引き起こすことなく、多くの生き残ったオブジェクトを特にターゲットにするアルゴリズムが必要であり、これがマーキングおよびソート方法の本来の目的です。マーキングと分別の考え方は分かりやすく、部屋を片付けるときと同じように、使えるものと捨てるべきゴミを部屋の両側に移動し、ゴミ側を掃き出します。部屋全体として。

マーキングと分別方法は、マーキングと分別の 2 段階に分かれており、マーキング段階では、最初に残存オブジェクトとリサイクル可能なオブジェクトにマーキングが行われ、マーキング後に思い出オブジェクトが分別され、この段階で残存オブジェクトは移動されます。オブジェクトを移動した後、残っているオブジェクトの境界の外側にあるオブジェクトを消去します。

ここに画像の説明を挿入します

マーキングおよびソート方法の利点:
マーキングおよびソート方法は、スペースを無駄にし、複数のオブジェクトが生き残るシナリオには適さないというマーキングおよびコピー方法の欠点を解決し、スペースの断片化のマークおよび明確な方法の欠点も解決します。 , そのため、マークとコピーの方法が適していないシーンでは、同時に、マークとクリアの方法の領域の断片化の問題を許容できない場合は、マークとデフラグの方法を検討できます。

マーキングおよびソート方式の欠点:
全能のアルゴリズムはありません。マーキングおよびソート方式は多くの問題を解決するように見えますが、それ自体には重大なパフォーマンスの問題があります。マーキングおよびソート方式は 3 つのガベージ コレクション アルゴリズムの中で最もパフォーマンスが低くなります。マーキングおよびソート方法では、オブジェクトを移動するときにオブジェクトを移動する必要があるだけでなく、オブジェクトの参照アドレスも維持する必要があります。このプロセスを完了するには、メモリの数回のスキャンと位置決めが必要になる場合があります。やるべきことがたくさんあるので、どうしても時間がかかってしまいます。

2.4 さまざまなガベージ コレクション アルゴリズムの適用可能なシナリオ

3 つのガベージ コレクション アルゴリズムを理解すると、完璧なアルゴリズムは存在しないことがわかります。各アルゴリズムには独自の特性があるため、特定のシナリオに基づいて適切なガベージ コレクション アルゴリズムを選択するしかありません。

1. マークアンドスイープ方式
特徴: 簡単かつ高速に回収できるが、スペースデブリが存在するため、その後の GC 頻度が増加します。

シナリオに適しています: リサイクルする必要があるオブジェクトは少数であるため、マーク アンド スイープ方式は旧世代のガベージ コレクションに適しています。これは、一般に、旧世代にはリサイクルされたオブジェクトよりも生き残っているオブジェクトの方が多いためです。

2. マーク付きコピー方式
特徴: 収集速度が速く、領域の断片化を回避できるが、領域の無駄が発生し、多数のオブジェクトが残っている場合、オブジェクトのコピー処理に非常に時間がかかり、保証機構が必要となる。

適したシナリオ: 少数のオブジェクトのみが生き残るシナリオ これは新世代オブジェクトの特性でもあるため、一般に新世代のガベージ コレクターは基本的にマークされたコピー方法を選択します。

3. マークソート方式
特徴: マークコピー方式に比べてメモリ空間を無駄にしない 相対マーククリア方式は空間の断片化を回避できるが、他の 2 つのアルゴリズムに比べて速度が遅い。

適切なシナリオ: メモリが不足しており、領域の断片化を回避する必要があるシナリオ 高齢者が領域の断片化の問題を回避したい場合は、通常、マークのデフラグを使用します。


3. 世代別ガベージコレクション

3.1 世代別ガベージコレクション

実際の JVM ガベージ コレクションでは、特定の状況に応じて複数のガベージ コレクション アルゴリズムが調整され、その具体的な実装は世代別ガベージ コレクション メカニズムと呼ばれます。ヒープメモリ全体の広い領域は新世代と旧世代に分かれており、新世代はエデンの園、生存領域From、生存領域Toに分かれています。この分割の理由は、長期間使用する必要があるオブジェクトが古い世代に配置され、長期間使用する必要のないオブジェクトが新世代に配置されるためです。領域ごとに異なるガベージ コレクション アルゴリズムを使用します。
ここに画像の説明を挿入します

  • 新しいオブジェクトはエデンエリアにオブジェクトを作成します
  • Eden メモリが新しいオブジェクトを収容できなくなると、マイナー GC (到達可能性分析アルゴリズムを使用) がトリガーされ、その後、生き残ったオブジェクトがコピー アルゴリズムを使用して生存領域 Toにコピーされ、オブジェクトの年齢 + 1 が作成され、オブジェクトが交換されます。出発地と到着地のエリア。
  • マイナー GC はSTW (ストップ ザ ワールド)をトリガーします。他のユーザー スレッドは一時停止され、ガベージ コレクション スレッドのリサイクルが完了した後にのみユーザー スレッドが再開されます。
  • オブジェクトの寿命がしきい値を超えると、古い世代に昇格され、最大寿命は 15 (4 ビット、10 進数の最大値は 15) です。
  • 旧世代の領域が不足している場合は、まずマイナー GC が発生し、それでも不足している場合はフル GCが発生し、STW 時間が長くなります。
  • フル GC 後も古い世代のスペースがまだ不十分な場合、メモリ不足エラー: Java ヒープ スペースがトリガーされます。

3.2 関連する JVM パラメータ

ここに画像の説明を挿入します

3.3 GC分析

次のプログラムを実行して、GC 出力の詳細とヒープ メモリの
初期変更を観察します。

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:PrintGCDetails -verbose:gc
 */
public class Main {
    
    
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    public static void main(String[] args) {
    
    
    }
}
Heap
 def new generation   total 9216K, used 1731K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  21% used [0x00000007bec00000, 0x00000007bedb0f58, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 0K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,   0% used [0x00000007bf600000, 0x00000007bf600000, 0x00000007bf600200, 0x00000007c0000000)
 Metaspace       used 3159K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 348K, capacity 388K, committed 512K, reserved 1048576K
  • def new Generation: 新世代、-Xmx20M に設定、実際には 9216K。生存領域 To はデフォルトでは使用できない領域であるため、この領域が占有しているメモリを減算する必要があります。
  • 在職期間のある世代: 古い世代
  • メタスペース: メタスペース

小さなオブジェクトの作成

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:PrintGCDetails -verbose:gc
 */
public class Main {
    
    
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    public static void main(String[] args) {
    
    
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
        list.add(new byte[_512KB]);
    }
}
[GC (Allocation Failure) [DefNew: 1567K->388K(9216K), 0.0015099 secs] 1567K->388K(19456K), 0.0016601 secs] [Times: user=0.01 sys=0.01, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 8478K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  98% used [0x00000007bec00000, 0x00000007bf3e6838, 0x00000007bf400000)
  from space 1024K,  37% used [0x00000007bf500000, 0x00000007bf561160, 0x00000007bf600000)
  to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
 tenured generation   total 10240K, used 0K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,   0% used [0x00000007bf600000, 0x00000007bf600000, 0x00000007bf600200, 0x00000007c0000000)
 Metaspace       used 3153K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 347K, capacity 388K, committed 512K, reserved 1048576K

小さなオブジェクトの作成により GC がトリガーされる

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:PrintGCDetails -verbose:gc
 */
public class Main {
    
    
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    public static void main(String[] args) {
    
    
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_512KB]);
    }
}
[GC (Allocation Failure) [DefNew: 1567K->392K(9216K), 0.0012920 secs] 1567K->392K(19456K), 0.0014405 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 8400K->880K(9216K), 0.0028952 secs] 8400K->8048K(19456K), 0.0029182 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 1801K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  11% used [0x00000007bec00000, 0x00000007bece60f0, 0x00000007bf400000)
  from space 1024K,  86% used [0x00000007bf400000, 0x00000007bf4dc390, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 7168K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  70% used [0x00000007bf600000, 0x00000007bfd00010, 0x00000007bfd00200, 0x00000007c0000000)
 Metaspace       used 3160K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 348K, capacity 388K, committed 512K, reserved 1048576K

大きなオブジェクトの作成

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:PrintGCDetails -verbose:gc
 */
public class Main {
    
    
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    public static void main(String[] args) {
    
    
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
    }
}
Heap
 def new generation   total 9216K, used 1731K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  21% used [0x00000007bec00000, 0x00000007bedb0f58, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 8192K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  80% used [0x00000007bf600000, 0x00000007bfe00010, 0x00000007bfe00200, 0x00000007c0000000)
 Metaspace       used 3151K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 347K, capacity 388K, committed 512K, reserved 1048576K

大きなオブジェクトの作成により OOM がトリガーされ、メインスレッドが終了します

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:PrintGCDetails -verbose:gc
 */
public class Main {
    
    
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    public static void main(String[] args) {
    
    
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
        list.add(new byte[_8MB]);
    }
}
[GC (Allocation Failure) [DefNew: 1567K->388K(9216K), 0.0012293 secs][Tenured: 8192K->8579K(10240K), 0.0014159 secs] 9759K->8579K(19456K), [Metaspace: 3059K->3059K(1056768K)], 0.0028190 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [Tenured: 8579K->8562K(10240K), 0.0008542 secs] 8579K->8562K(19456K), [Metaspace: 3059K->3059K(1056768K)], 0.0008647 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 410K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,   5% used [0x00000007bec00000, 0x00000007bec66800, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
  to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
 tenured generation   total 10240K, used 8562K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  83% used [0x00000007bf600000, 0x00000007bfe5c8a8, 0x00000007bfe5ca00, 0x00000007c0000000)
 Metaspace       used 3179K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.tzuaness.jvm.Demo02GCPrintDetails.main(Demo02GCPrintDetails.java:19)

スレッドでラージ オブジェクトを作成すると OOM がトリガーされますが、メイン スレッドは終了しません。

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:PrintGCDetails -verbose:gc
 */
public class Demo02GCPrintDetails {
    
    
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(() -> {
    
    
            List<byte[]> list = new ArrayList<>();
            list.add(new byte[_8MB]);
            list.add(new byte[_8MB]);
        }).start();

        Thread.sleep(1000);
        System.out.println("over...");
    }
}
[GC (Allocation Failure) [DefNew: 3898K->623K(9216K), 0.0031331 secs][Tenured: 8192K->8813K(10240K), 0.0031949 secs] 12090K->8813K(19456K), [Metaspace: 4095K->4095K(1056768K)], 0.0065025 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [Tenured: 8813K->8757K(10240K), 0.0019078 secs] 8813K->8757K(19456K), [Metaspace: 4095K->4095K(1056768K)], 0.0019295 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
	at com.tzuaness.jvm.Demo02GCPrintDetails.lambda$main$0(Demo02GCPrintDetails.java:20)
	at com.tzuaness.jvm.Demo02GCPrintDetails$$Lambda$1/1480010240.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:750)
over...
Heap
 def new generation   total 9216K, used 546K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,   6% used [0x00000007bec00000, 0x00000007bec88b98, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
  to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
 tenured generation   total 10240K, used 8757K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  85% used [0x00000007bf600000, 0x00000007bfe8d668, 0x00000007bfe8d800, 0x00000007c0000000)
 Metaspace       used 4123K, capacity 4676K, committed 4864K, reserved 1056768K
  class space    used 461K, capacity 496K, committed 512K, reserved 1048576K

4. ガベージコレクター

4.1 シリアル

ここに画像の説明を挿入します

  • シングルスレッド
  • ヒープメモリが小さくパソコンに最適
  • シリアル ガベージ コレクションを有効にする: -XX:+UseSerialGC=Serial+SerialOld

4.2 スループットの優先順位

ここに画像の説明を挿入します

  • マルチスレッド化
  • 大容量ヒープメモリ、マルチコアCPU
  • 単位時間あたりの STW 時間を最小化するようにしてください (0.2 + 0.2 = 0.4)
  • 関連する JVM パラメータ: パラメータ 3 と 4 は相互に排他的であり、両方ともパラメータ 2 に関連しています。
    1. -XX:+UseParallelGC ~ -XX:+UseParallelOldGC (いずれかをオンにすると、もう一方もデフォルトでオンになります。1.8 ではデフォルトでオンになります)
    2. -XX:+UseAdaptiveSizePolicy、適応型サイズ調整戦略を使用して、ParallelGC が発生したときに Eden Survivor 領域を動的に調整します。
    3. -XX:GCTimeRatio=ratio、計算式は です1/(1+ratio)プログラムの実行時間の合計は合計です。ガベージ コレクション時間が を超える場合total/(1+ratio)、この目標値を達成する (スループットの向上) ために、ヒープ メモリが増加して GC の数が減ります。ヒープが増加した後は、合計 GC 時間は減少しますが、そのたびに GC 時間は増加します)。デフォルト値は 99 ですが、通常は 19 に設定されます。
    4. -XX:MaxGCPauseMillis=ms、各 GC の最大時間。この値はパラメータ 3 とは排他的です (この値を下げると、平均 GC 時間を短縮するにはヒープ メモリを小さくする必要があることになるため)。デフォルト値は 200 です
    5. -XX:ParallelGCThreads=n、並列ガベージ コレクションのスレッド数

4.3 応答時間の優先順位

ここに画像の説明を挿入します

  • マルチスレッド化
  • 大容量ヒープメモリ、マルチコアCPU
  • 各 STW の時間をできるだけ短くしてください (0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5)
  • 関連する JVM パラメータ:
    1. -XX:+UseConcMarkSoupGC ~ -XX:+UseParNewGC ~ SerialOld、GC スレッドの実行中にユーザー スレッドも実行できます
    2. -XX:ParallelGCThreads=n ~ -XX:ConcGCTreads=threads、並列 GC スレッドの数と同時 GC スレッドの数、通常は thread=n/4
    3. -XX:CMSInitiatingOccupancyFraction=percent、CMSGC をトリガーする古い世代のメモリ使用率を設定します。80% に設定すると、古い世代のメモリ使用率が 80% に達したときに CMS がトリガーされ、残りの 20% に達すると CMS がトリガーされます。浮遊ゴミ用に確保されています。デフォルト値は 65% です
    4. -XX:+CMSScavengeBeforeRemark

CMS ガベージ コレクタには 2 つの欠陥があります。1 つは、フローティング ガベージを処理できないことです。つまり、同時クリーニング プロセス中に、ユーザー スレッドは新しいガベージを生成します。このガベージにはマークを付けることができず、次の FullGC までクリーニングを待たなければなりません。そのため、浮遊ゴミを保持するために旧世代にある程度の領域を確保する必要があり、JDK1.6 バージョンでは、CMS の起動しきい値は 92% であり、旧世代のメモリが 92% を超えると CMS が起動されます。 。このプロセスにより、CMS の実行中にメモリ領域が満たされないという同時実行エラーが発生する可能性があり、このとき、仮想マシンはシリアル オールドを取り出し、STW を実行してシリアル ガベージ クリーニングを実行します。

4.4 G1

定義: ガベージファースト

歴史:

  • 2004 年の論文
  • 2009 JDK 6u14 の経験
  • 2012 JDK 7u4 正式サポート
  • 2017 JDK 9 のサポート

特徴:

  • スループットと低遅延の両方に注意してください。デフォルトの一時停止目標は 200 ミリ秒です。
  • 非常に大規模なヒープ メモリに適しています (ヒープは同じサイズの複数の領域に分割されます)
  • 全体はマーキング + ソート アルゴリズムであり、2 つの領域はコピー アルゴリズムです。

関連する JVM パラメータ:

  • -XX:+G1GC を使用する
  • -XX:G1HeapReginSize=サイズ
  • -XX:MaxGCPauseMillis=時間
4.4.1 G1 ガベージ コレクション フェーズ

ここに画像の説明を挿入します

4.4.2 若いコレクション

G1 ガベージ コレクターは、ヒープ メモリを同じサイズの複数の領域に分割します。ここで、E は、エデンの園に分割された領域エデンです。
ここに画像の説明を挿入します
Eden 領域のスペースがいっぱいになると、Young GC がトリガーされ、コピー アルゴリズムを使用して、生き残ったオブジェクトが Survivor 領域にコピーされます。Survivor 領域
ここに画像の説明を挿入します
のスペースがいっぱいになると、古いオブジェクトが Old 領域にコピーされます。エリアに追加され、若いオブジェクトはそのまま残り、その年齢は +1 されます。
ここに画像の説明を挿入します

4.4.3 ヤングコレクション + CM

GC ルートの初期マーキングは、ヤング GC 中に実行されます。
古い世代が占有するヒープ領域の割合がしきい値に達すると、同時マーキングが実行されます (STW なし)。しきい値は、この JVM パラメータによって決定されます: -XX:InitiatingHeapOccupancyPercent =パーセント (デフォルトは45%)
ここに画像の説明を挿入します

4.4.4 Mixed Collection 混合ガベージコレクション

E、S、O で包括的なガベージ コレクションが実行されます。

  • 最後のマーク (リマーク) は STW になります。これは、同時マーキング中に見逃されたガベージ オブジェクト (同時マーキング中に他のユーザー スレッドによって生成されたガベージ オブジェクト) をリサイクルすることを目的としています。
  • コピー生存(避難)はSTWになります

-XX:MaxGCPauseMillis=ms

ここに画像の説明を挿入します

4.4.5 フル GC 分析

SerialGC と ParallelGC

  • 新世代のメモリ不足により発生するガベージコレクション:minor gc
  • 旧世代のメモリ不足により発生するガベージコレクション:full gc

CMSとG1

  • 新世代のメモリ不足により発生するガベージコレクション:minor gc
  • 古い世代ではメモリが不足しています。同時マーキングと混合コレクションがトリガーされるときに、ガベージ コレクション速度がガベージ生成速度よりも高い場合、その時点ではフル gc ではなく、同時ガベージ コレクションが行われます。ガベージ コレクションが実行されたときのみ、ガベージ生成速度よりも遅いと同時ガベージコレクションが失敗するとシリアルガベージコレクションに縮退してフルGCが発生し、この時より長いSTWが実行されます。
4.4.6 Young Collection の世代間参照 (詳細)

新世代リサイクルにおける世代間参照の問題(旧世代が新世代を参照)

ここに画像の説明を挿入します

  • カードリストと思い出のセット
  • 参照変更時にポストライトバリア + ダーティカードキューを通過
  • 同時リファインメント スレッド更新記憶セット

ここに画像の説明を挿入します

4.4.7 備考(詳細)

プリライトバリア + satb_mark_queue

同時マーキングの終了直後、ガベージとしてマークされたオブジェクトが、他のユーザー スレッドの作業により再び GC ルート参照チェーンのメンバーになると、書き込みバリアがトリガーされ、オブジェクトはキューに追加され、オブジェクトとしてマークされます。生存オブジェクト。再マーキング (最終マーキング) する場合、キュー内のオブジェクトは STW の後に再チェックされ、ガベージ オブジェクトかどうかが確認されます。
ここに画像の説明を挿入します

4.4.8 JDK 8u20 文字列の重複排除

-XX:+UseStringDeduplication (デフォルトで有効)

String s1 = new String("hello"); // char[]{'h', 'e', 'l', 'l', 'o'}
String s2 = new String("hello"); // char[]{'h', 'e', 'l', 'l', 'o'}

新しく割り当てられたすべての文字列をキューに入れます。新しい世代でガベージ コレクションが発生すると、G1 は文字列の重複があるかどうかを同時にチェックし、重複した値を持つ文字列が同じ char[] を参照するようにします。

利点: メモリを大幅に節約できます。
欠点: CPU 時間がわずかに多く占有され、新世代のリサイクル時間がわずかに増加します。

: 最適化原理は String.inter() とは異なります。

  • String.intern() は文字列オブジェクトの非反復に焦点を当てています
  • この文字列の重複排除は、char[] の非重複に焦点を当てています。
  • JVM 内では異なる文字列テーブルが使用されます
4.4.9 JDK 8u40 同時マーククラスのアンロード

すべてのオブジェクトが同時実行用にマークされると、どのクラスが使用されなくなったかがわかります。クラス ローダーのすべてのクラスが使用されなくなると、そのクラス ローダーによってロードされたすべてのクラスがアンロードされます。

-XX:+ClassUnloadingWithConcurrentMark (デフォルトで有効)

4.4.10 JDK 8u60 は巨大オブジェクトをリサイクルします
  • オブジェクトが領域の半分より大きい場合、それは巨大オブジェクトと呼ばれます。
  • G1 は巨大なオブジェクトをコピーしません
  • リサイクルを優先
  • G1 は古い世代のすべての受信参照を追跡するため、古い世代の受信参照が 0 である巨大オブジェクトは、新世代のガベージ コレクション中に破棄できます。(たとえば、下の図の右上隅にある H は、O 領域のカード テーブルによって参照されなくなり、新世代のガベージ コレクション中にリサイクルされます)
    ここに画像の説明を挿入します
4.4.11 JDK 9によるコンカレントマーク開始時間の調整
  • 同時マーキングは、ヒープ領域がいっぱいになる前に完了する必要があります。完了しないと、フル GC に縮退します。
  • JDK 9 より前では、同時マークをトリガーする初期値を設定するには、-XX:InitiatingHeapOccupancyPercent が必要でした。
  • JDK 9 はデータ サンプリングを実行して値を動的に調整することができ、浮遊ガベージを収容する安全なギャップ スペースを追加し、フル GC への縮退を可能な限り回避します。
4.4.12 JDK 9 のより効率的なリサイクル
  • 250 以上の機能強化
  • 180 以上のバグ修正

5. ガベージコレクションのチューニング

コマンドを使用して、現在の JVM の GC 関連パラメータ構成を表示します。java -XX:+PrintFlagsFinal -version | grep GC

5.1 チューニング領域

  • メモリ
  • ロック競合
  • CPU使用率
  • IO

5.2 目標を決定する

要件が「低遅延」か「高スループット」かに応じて、適切なガベージ コレクターを選択します。

  • CMS、G1、ZGC
  • パラレルGC
  • ジン

5.3 最速の GC は GC を使用しないことです。

フル GC の前後でメモリ使用量を確認し、次の問題を考慮してください。

  • データが多すぎませんか?
    • resultSet = state.executeQuery(“SELECT * FROM big_table;”);
  • データ表現が肥大化しすぎていませんか?
    • オブジェクト グラフの例: クエリから取得されたオブジェクト グラフは、必要な実際のデータよりもはるかに多くなります。
    • オブジェクト サイズ: 例: Integer のオブジェクト ヘッダーは 16 バイトで、4 バイトの int 値はアライメント後に 32 バイトを占有しますが、int は 4 バイトしか必要としません。
  • メモリリークがあるのでしょうか?
    • 例: static Map マップ = new HashMap<>()、継続的にデータをそこに入力すると、OOM がトリガーされます。ソフトリファレンスと弱リファレンスの最適化が使用可能

5.4 新世代のチューニング

新世代の特徴:

  • オブジェクトの作成時に TLAB (スレッドローカル割り当てバッファ) が存在するため、すべての新しい操作に対するメモリ割り当ては非常に安価になります。
  • ガベージ コレクションでコピー アルゴリズムが使用される場合、Eden と Survivor From の生き残ったオブジェクトが Survivor To にコピーされるため、死んだオブジェクトのリサイクル コストはゼロです。残りのスペースはクリアされます
  • ほとんどのオブジェクトは使用後すぐに消滅します
  • マイナー GC の時間はフル GC の時間よりもはるかに短い

新しい世代 (-Xmn) は大きいほど優れていますか?

Oracle原文:
-Xmnはヒープ内の若い世代の初期値と最大値を設定しますが、若い世代領域は他の領域に比べてGCが発生しやすくなります。若い世代が小さすぎるとマイナー GC が頻繁に発生し、大きすぎるとフル GC のみになり、時間がかかります。若い世代がヒープ メモリ全体の 25% ~ 50% を占めることをお勧めします。

具体的な設定は、より適切です。理想的には、要求と応答中に生成されるオブジェクトと同時実行量の積です。1 つのリクエストで約 512KB のオブジェクトが生成され、同時リクエストが 1,000 あると仮定すると、512KB * 1000 ~ 512MB となります。

サバイバルゾーンの調整

  • 生存領域は、[現在アクティブなオブジェクト + 昇格する必要があるオブジェクト] を保持するのに十分な大きさです。
  • 有効期限の長いオブジェクトをできるだけ早くプロモートするように、プロモーションしきい値を適切に構成します (プロモートが遅すぎると、複数のオブジェクトのコピーとコピーに時間がかかり、マイナー GC で費やされる主な時間はオブジェクトのコピーです)。

最大プロモーションしきい値を調整します: -XX:MaxTenuringThreshold=threshold
サバイバー ゾーン オブジェクトの経過時間と占有の監視: -XX:+PrintTenuringDistribution
ここに画像の説明を挿入します
左の列は、この経過期間のオブジェクトによって占有されている合計メモリであり、右の列は、この期間のオブジェクトによって占有されている合計メモリで終わります。この年齢以下のオブジェクト。

5.5 旧世代のチューニング

CMS を例に挙げます。

  • CMS の古い世代のメモリは大きいほど良いです (CMS ガベージ コレクションが古いシリアルに縮退するのを防ぐため)
  • Full GC が発生しない場合は、古い世代が豊富で最適化の必要がないことを意味します。Full GC が発生した場合でも、新しい世代のチューニングを優先する必要があります。
  • フル GC が発生したときに旧世代のメモリ使用量を観察し、旧世代のメモリのデフォルトを 1/4 ~ 1/3 増加させます
    -XX:CMSInitiatingOccupancyFractioin=percent

5.6 ケース

1. 頻繁なマイナー GC とフル GC
の分析: 新しい世代が小さいため、マイナー GC が頻繁に発生し、さらに多くのオブジェクトが古い世代に入るようになり、結果として古い世代でフル GC が頻繁に発生することが考えられます。
解決策: 新世代のサイズを増やし、マイナー GC の頻度を減らし、新世代のオブジェクトを旧世代に昇格させるためのしきい値を増やして、旧世代の占有を減らすことができます。

2. フル GC はピーク要求期間中に発生し、単一の一時停止時間が特に長い (CMS)
分析: GC ログを確認すると、各 GC ステージに費やされた時間が表示されます。CMS ガベージ コレクタの再マーキング フェーズには時間がかかります。ログがこの状況を満たしているかどうかを確認してください。
解決策: 要件を満たしている場合は、-XX:+CMSScavengeBeforeRemark パラメータを使用して再マーキング前にガベージ コレクションを実行し、所要時間を短縮できます。指摘について。

3. 古い世代が多い場合、フル GC が発生します (CMS JDK1.7)
分析: メタスペース メモリが不足するとフル GC が発生します
解決策: メタスペース メモリの使用量を増やす

4. クラスローディングとバイトコード技術

1. クラスロード処理

ここに画像の説明を挿入します

1.1 クラスファイルの構造

1.2 読み込み

クラスのバイトコードをメソッド領域にロードします。C++ の instanceKlass は、Java クラスを記述するために内部的に使用されます。その重要なフィールドは次のとおりです。

  • _java_mirror は Java のクラス ミラーです。たとえば、String の場合は String.class です。その機能は、klass を
    Java に公開して使用できるようにすることです。
  • _super は親クラスです
  • _fields はメンバー変数です
  • _methods はメソッドです
  • _constants は定数プールです
  • _class_loader はクラスローダーです。
  • _vtable 仮想メソッド テーブル
  • _itable インターフェースメソッドテーブル

このクラスにロードされていない親クラスがある場合は、最初に親クラスをロードします。ロードとリンクは交互に実行される場合があります

知らせ:

  • 【メタデータ】instanceKlassなどはメソッド領域(1.8以降メタスペース)に格納されますが、_java_mirrorは
    ヒープに格納されます
  • 先ほど紹介した HSDB ツールを使用して表示できます。
  • instanceKlass とクラスの対応関係は次のとおりです。
    ここに画像の説明を挿入します

1.3 リンク

このフェーズには、検証、準備、分析が含まれます。

  • 検証:クラスがJVM仕様に準拠しているかどうかの検証、セキュリティチェック
  • 準備: 静的変数用のスペースを割り当て、デフォルト値を設定します。
    • 静的変数は、JDK 7 より前では、instanceKlass の最後に保存され、JDK 7 以降は、_java_mirror の最後に保存されます。
    • スペースの割り当てと静的変数への値の代入は 2 つのステップであり、スペースの割り当ては準備フェーズで完了し、代入は初期化フェーズで完了します。
    • 静的変数が最終的な基本型であり、文字列定数である場合、値はコンパイル段階で決定され、割り当ては準備段階で完了します。
    • 静的変数が最終参照型の場合、初期化フェーズ中に割り当ても完了します。
  • 解析: 定数プール内のシンボル参照を直接参照に解決します。
    /* 解析的含义 */ 
    public class Load2 {
          
           
    	public static void main(String[] args) throws ClassNotFoundException, IOException {
          
           
    		ClassLoader classloader = Load2.class.getClassLoader(); 
    		// loadClass() 只涉及类的加载,不会进行类的解析和初始化 
    		Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C"); 
    		
    		new C(); // 会进行类的初始化操作
    		System.in.read(); 
    	} 
    }
    class C {
          
           D d = new D(); }
    class D {
          
           }
    

1.4 初期化

初期化とは、<cinit>()V メソッドを呼び出すことを意味し、仮想マシンはこのクラスの「コンストラクター メソッド」のスレッド セーフを保証します。要約すると、クラスの初期化と
は、[遅延] クラスの初期化が発生することです。

  • main メソッドが配置されているクラスが常に最初に初期化されます。
  • このクラスの静的変数または静的メソッドに初めてアクセスする場合
  • サブクラスの初期化。親クラスが初期化されていない場合、トリガーされます。
  • サブクラスが親クラスの静的変数にアクセスすると、親クラスの初期化のみがトリガーされます。
  • Class.forName() メソッド
  • 新しいキーワード

クラスの初期化が行われない状況

  • クラスの静的な最終静的定数 (基本型と文字列) にアクセスしても初期化はトリガーされません
  • String.class などのクラス object.class は初期化をトリガーしません
  • このクラスの配列を作成しても、new String[0] などの初期化はトリガーされません。
  • クラスローダーのloadClassメソッド
  • Class.forNameの第2パラメータがfalseの場合

演習:シングルトン パターンの遅延読み込み

// 懒加载单例模式
class Singleton {
    
    
	private Singleton() {
    
    }

	private static class LazyHolder {
    
    
		private static final Singleton INSTANCE = new Singleton();
	}

	// 第一次调用该方法,才会导致内部类加载和初始化其静态成员
	public Singleton getInstance() {
    
    
		return LazyHolder.INSTANCE;
	}
}

2. クラスローダー

JDK 8 を例に挙げます。

名前 ロードディレクトリ 説明する
ブートストラップ クラスローダー JAVA_HOME/jre/lib C++ で実装されたクラス ローダーを開始します。ユーザーは直接アクセスできません
拡張クラスローダー JAVA_HOME/jre/lib/ext 拡張クラスローダー、上位はシステムクラスローダーです
アプリケーションクラスローダー クラスパス アプリケーション クラス ローダー、親は拡張クラス ローダー
カスタムクラスローダー カスタマイズ ユーザー定義ローダー、上位レベルはアプリケーションクラスローダー

一般に、パッケージ名が java で始まるクラス (lib ディレクトリにある) はスタートアップ クラス ローダーによってロードされ、自分で作成した Java コードはアプリケーション クラス ローダーによってロードされます。

2.1 クラスローダーの起動

スタートアップ クラス ローダーを使用してクラスをロードします。

// 使用此命令运行该类:java -Xbootclasspath/a:. BootstrapClassLoaderTest
public class BootstrapClassLoaderTest {
    
    
	public static void main(String[] args) {
    
    
		// 如果使用的是BootstrapClassLoader,打印结果为null
		System.out.println(BootstrapClassLoaderTest.class.getClassLoader());
	}
}
  • -Xbootclasspath はブートクラスパスを設定することを意味します
  • このうち、/a:. は現在のディレクトリをブートクラスパスに追加することを意味します。
  • このメソッドを使用してコアクラスを置き換えることができます
    • java -Xbootクラスパス:
    • java -Xbootclasspath/a:<追加パス>
    • java -Xbootclasspath/p:<追加パス>

2.2 拡張クラスローダー

拡張クラスローダーを使用してクラスをロードします。

1. 将字节码文件打成jar包:jar -cvf extclassloadertest.jar ExtensionClassloaderTest.class
2. 将jar包拷贝到JAVA_HOME/jre/lib/ext
3. 执行该文件,输出结果sun.misc.Launcher$ExtClassLoader@29453f44

2.3 親の委任メカニズム

いわゆる親委任とは、クラスローダーのloadClassメソッドを呼び出すときにクラスを検索するためのルールを指します。ClassLoader はクラスロード要求を受け取ると、クラス自体をロードするのではなく、上位のクラスローダにクラスローダのロードを委託し、最上位のクラスローダ (開始クラスローダ) まで階層ごとにロードします。ClassLoader がロード パスでクラスを見つけられない場合、そのクラスはロードのために下位レベルのクラス ローダーに渡されます。

関連する Java コードの実装:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    
    
	synchronized (getClassLoadingLock(name)) {
    
     
		// 1. 检查该类是否已经加载 
		Class<?> c = findLoadedClass(name); 
		if (c == null) {
    
     
			long t0 = System.nanoTime(); 
			try {
    
    
				if (parent != null) {
    
     
					// 2. 有上级的话,委派上级 
					loadClass c = parent.loadClass(name, false); 
				} else {
    
     
					// 3. 如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader
					c = findBootstrapClassOrNull(name); 
				} 
			} catch (ClassNotFoundException e) {
    
     
			}

			if (c == null) {
    
     
				long t1 = System.nanoTime(); 
				// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载 
				c = findClass(name); 
				// 5. 记录耗时 
				sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); 		
				sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); 
				sun.misc.PerfCounter.getFindClasses().increment(); 
			} 
		}

		if (resolve) {
    
     
			resolveClass(c); 
		}
	
		return c; 
	} 
}

例えば:

public class Load5_3 {
    
     
	public static void main(String[] args) throws ClassNotFoundException {
    
     
		Class<?> aClass = Load5_3.class.getClassLoader() .loadClass("com.ziang.jvm.t3.load.H"); 
		System.out.println(aClass.getClassLoader()); 
	} 
}

実行プロセスは次のとおりです。

  1. sun.misc.Launcher$AppClassLoader //1で、ロードされたクラスのチェックを開始しますが、結果はありません
  2. sun.misc.Launcher$AppClassLoader // 2 か所、上位の
    sun.misc.Launcher$ExtClassLoader.loadClass()に委任します
  3. sun.misc.Launcher$ExtClassLoader // 1 で、ロードされたクラスをチェックします。結果は no です。
  4. sun.misc.Launcher$ExtClassLoader // 3 で、上位が存在しない場合、
    検索はBootstrapClassLoader に委任されます。
  5. BootstrapClassLoader は JAVA_HOME/jre/lib の下で H クラスを探しますが、明らかに何もありません
  6. sun.misc.Launcher$ExtClassLoader // 4 番目の場所で、独自の findClass メソッドを呼び出し、
    JAVA_HOME/jre/lib/ext で H クラスを探します。明らかにそのようなクラスはありません。sun.misc.Launcher$AppClassLoader に戻ります。 2位で。
  7. sun.misc.Launcher$AppClassLoader // 4 への実行を続行し、独自の findClass メソッドを呼び出し、
    クラスパスの下を検索して見つけます。

2.4 スレッドコンテキストクラスローダー

Class.forName("com.mysql.jdbc.Driver")JDBC を使用するときはドライバーをロードする必要がありますが、 com.mysql.jdbc.Driver クラスは書かなくても正しくロードできることに気づいたでしょうか。その方法を知っていますか?

最初に他のものを見るのはやめて、DriverManager のクラス ローダーを見てみましょう。実行System.out.println(DriverManager.class.getClassLoader());、出力結果は null です。これは、そのクラス ローダーが Bootstrap ClassLoader であり、JAVA_HOME/jre/lib の下にあるクラスを検索することを意味しますが、明らかに JAVA_HOME/jre/lib の下に mysql-connector-java-5.1.47.jar パッケージがないため、問題はDriverManager で静的コード ブロックに com.mysql.jdbc.Driver を正しくロードするにはどうすればよいですか?

ソースコードをトレースしてみましょう。

public class DriverManager {
    
     
	// 注册驱动的集合 
	private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); 
	
	// 初始化驱动 
	static {
    
     
		loadInitialDrivers(); 
		println("JDBC DriverManager initialized"); 
	}

引き続き、loadInitialDrivers() メソッドを見てみましょう。

private static void loadInitialDrivers() {
    
     
	String drivers; 
	try {
    
    
		drivers = AccessController.doPrivileged(new PrivilegedAction<String> () {
    
     
			public String run() {
    
     
				return System.getProperty("jdbc.drivers"); 
			} 
		}); 
	} catch (Exception ex) {
    
     
		drivers = null; 
	}
	
	// 1)使用 ServiceLoader 机制加载驱动,即 SPI 
	AccessController.doPrivileged(new PrivilegedAction<Void>() {
    
     
		public Void run() {
    
     
			ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); 	
			Iterator<Driver> driversIterator = loadedDrivers.iterator(); 
			try{
    
    
				while (driversIterator.hasNext()) {
    
     
					driversIterator.next(); 
				} 
			} catch(Throwable t) {
    
     
				// Do nothing 
			}
			return null; 
		} 
	}); 
	
	println("DriverManager.initialize: jdbc.drivers = " + drivers); 
	
	// 2)使用 jdbc.drivers 定义的驱动名加载驱动 
	if (drivers == null || drivers.equals("")) {
    
     
		return; 
	}

	String[] driversList = drivers.split(":"); 
	println("number of Drivers:" + driversList.length); 
	for (String aDriver : driversList) {
    
     
		try {
    
    
			println("DriverManager.Initialize: loading " + aDriver); 
			// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器 
			Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); 
		} catch (Exception ex) {
    
     
			println("DriverManager.Initialize: load failed: " + ex); 
		} 
	} 
}

まず 2) を見て、最終的に Class.forName を使用してクラスのロードと初期化を完了していることがわかります。アプリケーションのクラス ローダーと関連付けられているため、クラスのロードは正常に完了できます。次に 1) を見てください。有名な Service Provider
Interface (SPI) の規則は次のとおりです。

jar パッケージの META-INF/services パッケージでは、インターフェイスの完全修飾名をファイル名として使用します。ファイルの内容は実装クラス名です。
ここに画像の説明を挿入します

このようにして、次のコードを使用して、[インターフェイス指向プログラミング + 分離] のアイデアを具体化する実装クラスを取得できます。

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class); // 接口类型:java.sql.Driver 
Iterator<接口类型> iter = allImpls.iterator(); 
while (iter.hasNext()) {
    
     
	iter.next(); 
}

このアイデアは、次のフレームワークの一部で使用されています。

  • JDBC
  • サーブレット初期化子
  • スプリングコンテナ
  • ダボ (拡張 SPI)

次に ServiceLoader.load メソッドを見てみましょう。

public static <S> ServiceLoader<S> load(Class<S> service) {
    
     
	// 获取线程上下文类加载器 
	ClassLoader cl = Thread.currentThread().getContextClassLoader(); 
	return ServiceLoader.load(service, cl); 
}

スレッド コンテキスト クラス ローダーは、現在のスレッドによって使用されるクラス ローダーです。デフォルトは、アプリケーション クラス ローダーです。ServiceLoader.load(service, cl) 内で、Class.forName() は、スレッド コンテキスト クラス ローダーを呼び出してロードを完了します。具体的には、コードは ServiceLoader の内部クラス LazyIterator にあります。

private S nextService() {
    
     
	if (!hasNextService()) 
		throw new NoSuchElementException(); 
	String cn = nextName; 
	nextName = null; 
	Class<?> c = null; 
	try {
    
    
		// 此处的loader就是上一步中的cl
		c = Class.forName(cn, false, loader); 	
	} catch (ClassNotFoundException x) {
    
     
		fail(service, "Provider " + cn + " not found"); 
	}
	
	if (!service.isAssignableFrom(c)) {
    
     
		fail(service, "Provider " + cn + " not a subtype"); 
	}
	try {
    
    
		S p = service.cast(c.newInstance()); 
		providers.put(cn, p); 
		return p; 
	} catch (Throwable x) {
    
     
		fail(service, "Provider " + cn + " could not be instantiated", x); 
	}

	throw new Error(); // This cannot happen 
}

2.5 カスタムクラスローダー

いつカスタム クラス ローダーが必要になるかを自問してください。

  1. クラスパス以外のパスにクラスファイルをロードしたい
  2. これらはすべてインターフェイスを介して実装されており、分離が必要な場合は、フレームワーク設計でよく使用されます。
  3. これらのクラスは分離されることが期待されます。異なるアプリケーションで同じ名前のクラスは競合することなくロードできます。これらは Tomcat コンテナーでは一般的です。

ステップ:

  1. ClassLoaderの親クラスを継承する
  2. 親の委任メカニズムに準拠するには、findClass メソッドをオーバーライドします (loadClass メソッドをオーバーライドしないことに注意してください。オーバーライドしないと、親の委任メカニズムは使用されません)。
  3. クラスファイルのバイトコードを読み取る
  4. 親クラスのdefineClassメソッドを呼び出してクラスをロードします。
  5. ユーザーは、クラスローダーのloadClassメソッドを呼び出します
    。例:
class MyClassloader extends Classloader {
    
    
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
    
    
		String path = "\\myclasspath\\" + name + ".class";

		try {
    
    
			ByteArrayOutputStream os = new ByteArrayOutputStream();
			Files.copy(Paths.get(path), os);
			// 得到字节数组
			byte[] bytes = os.toByteArray();
			return defineClass(name, bytes, 0, bytes.length);
		} catch (IOException e) {
    
    
			e.printStackTrace();
			throw new ClassNotFoundExeception("类文件未找到:", path);
		}
	}
}

5. 実行時の最適化

1. ジャストインタイムのコンパイル

レイヤードコンパイル:
まず例を見てみましょう

public class JIT1 {
    
     public static void main(String[] args) {
    
     for (int i = 0; i < 200; i++) {
    
     long start = System.nanoTime(); for (int j = 0; j < 1000; j++) {
    
     new Object(); }long end = System.nanoTime(); System.out.printf("%d\t%d\n",i,(end - start)); } } }

出力の時間間隔を観察すると、最初は5桁だったものが3桁になってしまうのですが、これは
なぜでしょうか?
JVM は実行ステータスを 5 つのレベルに分けます。

  • レベル0、通訳
  • レベル 1、C1 ジャストインタイム コンパイラを使用してコンパイルおよび実行 (プロファイリングなし)
  • レイヤ 2、C1 ジャストインタイム コンパイラを使用してコンパイルおよび実行 (基本プロファイリング付き)
  • レイヤ 3、C1 ジャストインタイム コンパイラを使用してコンパイルおよび実行 (完全なプロファイリング付き)
  • レベル 4、C2 ジャストインタイム コンパイラを使用してコンパイルおよび実行

プロファイリングとは、実行プロセス中のプログラムの実行状況に関するデータ ([メソッド呼び出しの数]、[
ループのループバック数] など) を収集することを指します。

ジャストインタイムコンパイラ (JIT) とインタープリタの違い

  • インタプリタはバイトコードを機械語に解釈し、次に同じバイトコードに遭遇した場合でも繰り返し解釈を行います。
  • JIT は一部のバイトコードをマシン コードにコンパイルし、コード キャッシュに保存します。次回同じコードに遭遇したときに、再度コンパイルせずに直接実行できます。
  • インタプリタはバイトコードをすべてのプラットフォームに共通のマシンコードに解釈します。
  • JIT は、プラットフォームの種類に基づいてプラットフォーム固有のマシンコードを生成します。

大部分を占める使用頻度の低いコードについては、マシンコードへのコンパイルに時間を費やす必要はなく、解釈と実行を通じて実行できますが、一方、ごく一部しか占めていないホットコードについては、これをマシンコードにコンパイルして、必要な実行速度を実現します。インタプリタ < C1 < C2 の実行効率の単純な比較 全体的な目標はホットスポット コード (ホットスポットの名前の由来) を発見することです 先ほどの最適化手法は [エスケープ分析] と呼ばれ、新しく作成されたコードが物体が逃げる。-XX:-DoEscapeAnalysis を使用してエスケープ分析をオフにし、先ほどの例を実行して結果を観察できます。

参照: https://docs.oracle.com/en/java/javase/12/vm/java-hotspot-virtual-machineperformance-enhancements.html#GUID-D2E3DC58-D18B-4A6C-8167-4A1DFB4888E4

インラインメソッド

public static void main(String[] args) {
    
     
	int x = 0; 
	for (int i = 0; i < 500; i++) {
    
     
		long start = System.nanoTime(); 
		for (int j = 0; j < 1000; j++) {
    
     
			x = square(9); 
		}
		long end = System.nanoTime(); 
		System.out.printf("%d\t%d\t%d\n",i,x,(end - start)); 
	} 
}

private static int square(final int i) {
    
     
	return i * i; 
}

square がホットスポット メソッドであり、長さが長すぎないことが判明した場合は、インライン化されます。いわゆるインライン化とは、メソッド内のコードをコピーして呼び出し元の場所に貼り付けることです。

System.out.println(9 * 9);

定数フォールディングの最適化も実行できます

System.out.println(81);

2. その他の最適化

フィールド最適化や反射最適化などのその他の最適化。最適化の詳細についてはオンラインで参照できますが、詳細は説明しません。


おすすめ

転載: blog.csdn.net/qq_45867699/article/details/126336909