Java 仮想マシンのクイック スタート | JVM の概要、JVM メモリ構造、ダイレクト メモリ

目次

1: JVM の概要

1.JVMとは何ですか?

2. 一般的な JVM

3. 学習ルート

2: JVM メモリ構造

1. プログラムカウンタ(PCレジスタ)

2. 仮想マシン スタック (JVM スタック)

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

4. ヒープ

5. メソッドエリア

3: 直接記憶


ヒント:まず、2 つの便利なフリー ソフトウェアを皆さんにお勧めします。GIF 取得ソフトウェア: ScreenToGif と画面録画ツール: oCam (毎日の GIF 作成と画面録画に使用できます)、ネットワーク ディスク リンク: quark ネットワーク ディスク共有

1: JVM の概要

1.JVMとは何ですか?

定義: Java 仮想マシン - Java プログラムの実行環境 (Java バイナリ バイトコードの実行環境)

利点:

① 一度書けばどこでも実行可能。 

② 自動メモリ管理、ガベージコレクション機能付き。

③配列の添え字が範囲外をチェックし、例外をスローします。

④ オブジェクト指向の基礎となるポリモーフィズム。

比較: JVM、JRE、JDK

この図からも、段階的に上向きで包括的な関係であることがわかります。

2. 一般的な JVM

最も一般的に使用されるものは、HotSpot、Oracle JDK エディション、Eclipse OPenJ9 です。以下の説明は HotSpot! に基づいています。

3. 学習ルート

主に、クラスローダー ClassLoaderJVM メモリ構造、および実行エンジンの3 つの部分に分かれています

学習順序:最初に JVM メモリ構造を学習し、次に GC ガベージ コレクション メカニズムを学習し、次に JavaClass バイトコードを学習し、次にクラス ローダー ClassLoader を学習し、最後に実行エンジンのその他の内容を学習します。

2: JVM メモリ構造

1. プログラムカウンタ(PCレジスタ)

(1) 定義

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

特徴: スレッドプライベートなのでメモリオーバーフローがありません!

(2) 機能

実行プロセス: Java ソース コード ---「バイナリ バイトコードを生成するためにコンパイル (一部の JVM 命令)---」インタプリタ後 ---「マシン コードに解釈される ---」最終的に実行のために CPU に渡されます。

プログラムカウンターの機能:プログラム実行中の次の JVM 命令の実行アドレスを記憶します(前の数値は実行アドレスとして理解できます)。例: 最初の getstatic 命令を取得してインタプリタに渡すと、インタプリタはマシンコードになり、マシンコードが CPU に渡され、同時に次の命令 (astore_1) のアドレス (3) が取得されます。最初の命令が実行された後、インタプリタはプログラム カウンタから次の命令 (astore_1) のアドレス (3) をフェッチし、順番に繰り返します。

考えてみましょう: プログラム カウンタがなかったら何が問題になるでしょうか?

次にどのコマンドを実行すればよいか分からなくなります。実際、プログラム カウンタはレジスタを通じて物理的に実装されています。

2. 仮想マシン スタック (JVM スタック)

スタック: 先入れ先出しまたは後入れ先出しのデータ構造であり、スレッドにはスタックがあります。

(1) 定義

Java 仮想マシン スタック (Java 仮想マシン スタック)

① 各スレッドの実行に必要なメモリは、仮想マシン スタックと呼ばれます。

② 各スタックは、各メソッド呼び出しが占有するメモリに対応する複数のスタック フレーム (フレーム) で構成されます。

③各スレッドは、現在実行中のメソッドに対応するアクティブなスタック フレームを 1 つだけ持つことができます。

要約:

スタック---」は、スレッドの実行に必要なメモリ空間に対応します。

スタック フレーム---」は、各メソッドの実行に必要なメモリ スペースに対応します。

次のコード部分をデバッグ モードで実行すると、スタックとスタック フレームを理解できます。

問題分析:

(1) ガベージ コレクションにはスタック メモリが関係しますか?

回答:いいえ、スタック フレーム メモリはメソッドが終了するたびにポップアップされ、スタックは自動的に解放されてリサイクルされます。ガベージ コレクション メカニズムはヒープ メモリ内の不要なオブジェクトのみを再利用できることがわかっています。

(2) スタックメモリの割り当ては大きいほど良いのでしょうか?

回答:いいえ、物理メモリのサイズは固定されているため、スタック メモリが大きいほどスレッドの数は少なくなります。たとえば、スレッドに 1M が割り当てられ、物理メモリの合計が 500M の場合、理論的には 500 個のスレッドのみが割り当てられますが、スレッドに 2M が割り当てられた場合、理論的には 250 個のスレッドのみが割り当てられます。

注:スタック メモリ分割は大きくなりますが、より多くのメソッド呼び出しが行われるため、動作効率は向上しません。

注:実行時に、-Xss size を使用して、割り当てられるスタック メモリのサイズを指定できます。デフォルトでは、Linux と macOS は 1024KB を割り当て、Windows は仮想メモリのサイズに応じて割り当てられます。

(3) メソッド内のローカル変数はスレッドセーフですか?

例 1: 複数のスレッド呼び出しを分析すると、変数 x の値が台無しになりますか?

1 つのスレッドには 1 つのスタックがあり、別のスレッドを呼び出すと新しいスタック フレームが生成され、各スレッドには独自のプライベート変数 x があることがわかっています。

例 2: 複数のスレッドによって呼び出されるメソッドを分析する場合、スレッドの安全性は保証できますか?

①M1 メソッド、ローカル変数はメソッドのスコープをエスケープせず、変数はメソッドの終了時に解放されるため、スレッドセーフです。

②m2メソッド、メソッド内の変数(データ型を参照することが前提)、他のスレッドがこのメソッドを介して呼び出すことができますが、スレッドセーフではありません。

③m3 メソッド、このローカル変数 (参照データ型の場合) が return を通じて返され、他のスレッドがそれを受信して​​、他の操作を実行できます。

要約:

① メソッド内のローカル変数がメソッドのスコープをエスケープしない場合、それはスレッドセーフです。

②ローカル変数が参照型 (基本データ型は依然としてスレッドセーフである必要がある) であり、メソッドのスコープをエスケープする場合、スレッドセーフを考慮する必要があります。

(2) スタックメモリのオーバーフロー

ケース 1: スタック フレームが多すぎるとスタック メモリ オーバーフローが発生する

スタックのサイズは固定されています。スタック フレームが継続的にスタックにプッシュされるように呼び出しを続けると、最終的にはスタック メモリ オーバーフローが発生します。例: 再帰呼び出し、終了条件なし、最終的には StackOverflowError 例外投げられる

ケース 2: スタック フレームが大きすぎてスタック メモリ オーバーフローが発生する

スタック フレームが大きすぎて、一度にスタックのサイズを超えています。まれです。

(3) スレッド実行診断

ケース 1: CPU 使用率が高すぎる (おそらく無限ループ)

位置付け: Linux 環境では、Java コード、nohub java class&を実行します。

nohub: 電話を切らないという意味です。Xshell などの Linux クライアント ツールを使用して Linux スクリプトをリモートで実行すると、ネットワークの問題により、クライアントの接続が失われ、ターミナルが切断され、スクリプトが途中で予期せず終了することがあります。この場合、nohup コマンドを使用してコマンドを実行できます。クライアントがサーバーから切断されても、サーバー上のスクリプトは引き続き実行できます。

&: バックグラウンドで実行することを意味します。

まず、top コマンドを使用して、どのプロセスがCPU を過剰に占有しているかを特定します。

ps H -eo pid,tid,%cpu | grep process id (使用率の高さを引き起こしたスレッドをさらに特定するには、ps コマンドを使用します)

H:プロセス間の関係を示すツリー構造を表示します。

-eo:対象となるコンテンツの出力を指定します。例: プロセス ID (pid)、スレッド ID (tid)、CPU 使用率 (%cpu)

|:はパイプ文字を表し、grep フィルター コマンドと一緒によく使用されます。

jstack プロセス ID:スレッド ID に従って問題のあるスレッドを見つけ、さらに問題のあるコードのソース コード行番号を特定できます

注:上に表示される 32665 スレッド番号は 10 進数であり、jstack によって表示される 16 進数は 7F99 に対応します。

ケース 2: プログラムが長時間実行されても結果が得られない (スレッドのデッドロックが発生している可能性があります)

最初に Java プログラム、nohub java class & を実行すると、プロセス ID が表示されます

 jstack プロセス ID : 現時点ではスレッド ID を知ることができません。最後の実行結果のプロンプトを参照してください。

スレッドのデッドロックはどのようなときに発生しますか?

属性 a と b を持つクラスの場合、スレッド t1 では、最初に a をロックし、次に b をロックします。スレッド t2 では、最初に b をロックし、次に a をロックします。この場合、プログラムはデッドロックになり、例外はスローされません。このケースのトラブルシューティングは非常に困難です。

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

定義: JVM がネイティブ メソッドを呼び出すときは、これらのネイティブ メソッドにメモリ領域を提供する必要があります。

ネイティブ メソッドの説明 (ネイティブ メソッド): Java コードで記述されていないメソッドを指します。たとえば、C および C++ で記述されたネイティブ メソッドを使用してオペレーティング システムを処理し、Java コードはこれらのネイティブ メソッドを通じてこれらの基礎となる関数を呼び出すことができます。ネイティブ メソッドの作成に使用されるメモリはネイティブ メソッド スタックのみです。

例: Object クラスの clone メソッド

4. ヒープ

先ほど学習したスタック Stack はスレッドに対してプライベートであり、ヒープ Heap はスレッドによって共有されます。

ヒープ: 新しいキーワードを使用すると、オブジェクトの作成にヒープ メモリが使用されます。

特徴:

①それはスレッドによって共有され、ヒープ内のすべてのオブジェクトはスレッドの安全性の問題を考慮する必要があります。

②ガベージコレクションメカニズムがある。

(1) ヒープメモリオーバーフロー

まず次のコードを見てください。

まず ArrayList コレクションを作成し、無限ループを記述して文字列を連続的に結合してから、List コレクションに入れます。

public class Demo1_5 {

    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "hello";
            while (true) {
                list.add(a); // hello, hellohello, hellohellohellohello ...
                a = a + a;  // hellohellohellohello
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

実行結果:メモリオーバーフロー、OutOfMemoryErrorがスローされる

-Xmx size を使用して、割り当てられたヒープ領域のサイズを指定できます。

(2) ヒープメモリ診断

①jpsツール:現在のシステムにどのJavaプロセスが存在するかを確認します。

②jmapツール:ある瞬間のヒープメモリ使用量を確認する;  jmap -heap process id

③jconsoleツール:継続的に監視できるグラフィカルインターフェース、多機能監視ツール

ケース 1:

まずバイト配列を作成し、ヒープ メモリ内に 10M のスペースを空けます。次に、配列の参照 arr を null に設定し、リサイクルのためにガベージ コレクション メカニズムをオンにします。中間のスリープ スリープは、次の実行を容易にするためのものです。モニタリングの指示。

public class Demo1_4 {

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

IDEA を使用してこのプログラムを実行し、組み込みの dos ウィンドウを開いて、次のコマンドを入力します。

①まずjpsコマンドを入力し、どのJavaプロセスが存在するかを確認します。

② 検出にはjmapを使用する

ステップ 1: jmap -heap 18756 を使用して、コンソールが 1 を出力するとき、つまり 10M メモリ空間が作成されないときを検出します。

ステップ 2: コンソールに 2 を出力し、 jmap -heap 18756 を使用して検出します (この時点で 10M のスペースを作成します)

ステップ 3: コンソールに 3 を出力し、jmap -heap 18756 を使用して再度確認し (この時点では参照は null に設定されています)、リサイクルのためのガベージ コレクション メカニズムを有効にします。

 ③検出にはjconsoleを使用します(表示用グラフィカルインターフェース)

手順: jconsole に直接入力します ---> グラフィカル インターフェイスを表示し、検出するクラスを見つけます --- 「安全でない接続を選択します。検出効果が各瞬間に動的に表示されます。

ケース 2: ガベージ コレクションを呼び出した後もメモリ使用量が依然として高い

まずはコードです。コードの具体的な実装がわからない場合、どのように段階的に確認すればよいでしょうか?

import java.util.ArrayList;
import java.util.List;

public class ClazzMapperTest {

    public static void main(String[] args) throws InterruptedException {
        List<Student> students = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            students.add(new Student());
        }
        Thread.sleep(1000000000L);
    }
}
class Student {
    private byte[] big = new byte[1024*1024];
}

最初のステップ: jps を使用してプロセスの ID を表示します

ステップ 2: jmap -head process id を使用してメモリ使用量を表示します。これは 2 つの部分に分かれています。

エデン地区: 

Old Generation区: 

ステップ 3: jconsole ツールを使用してガベージ コレクション メカニズム GC を実行すると、確かにメモリの一部が元の状態に比べて回復しましたが、まだ 200 M 以上が回復されていないことがわかります。

ステップ 4: 実際、200 M を超える Eden 領域は確かに多くリサイクルされていますが、Old Generation 領域はリサイクルされていません。より便利な検出ツールjvisualvm を使用してください (JDK9 以降は使用できなくなります。プラグインをダウンロードしてください)検出のために

①ヒープダンプを見つけるということは、現在のヒープのスナップショットを取得することを意味します

② ヒープメモリを占有しているオブジェクトの上位 20 個を見つける

③ 最大のヒープメモリを占有しているオブジェクトは ArrayList オブジェクトであることがわかります

④クリックして ArrayList に移動し、そのプロパティがすべて Student オブジェクトであることを確認します。 アイテムは合計 244 個あり、そのうち 200 個が Student アイテムで、残りは Object オブジェクト (公開済み)です。Student オブジェクトは約 1M を占めます。 200 200 メガバイトを超えるため、チェックアウトできます。

⑤ソースコード解析と組み合わせると、メインメソッド main の実行が終了する(sleep メソッドが呼び出されてスリープする)前に、解放できない大量の Student オブジェクトが ArrrayList コレクションに格納され、最終的にガベージ コレクション後に、メモリ使用量はまだ高いです。

5. メソッドエリア

(1) 定義

(1) メソッド領域は、従来の言語でコンパイルされたコードに使用される記憶領域、またはオペレーティング システム プロセスの「テキスト」セグメントに似ています。ランタイム定数プール、フィールドとメソッドのデータ、クラスとインスタンスの初期化およびインターフェイスの初期化で使用される特別なメソッドを含むメソッドとコンストラクターのコードなど、クラスごとの構造が保存されますメソッド領域は、仮想マシンの起動時に作成されますメソッド領域は、論理的にはヒープの一部です。メソッド領域内のメモリは割り当て要求を満たすことができず、Java 仮想マシンは OutOfMemoryError をスローします。

(2) 特徴:

①メソッド領域はスレッドで共有されるため、複数のスレッドが同じクラスを使用する場合、クラスがロードされていない場合、この時点でクラスをロードできるのは 1 つのスレッドだけであり、他のスレッドは待機する必要があります。

②メソッド領域のサイズは固定ではなく、アプリケーションのニーズに応じて jvm を動的に調整でき、ユーザーとプログラムによるメソッド領域の初期サイズの指定もサポートされます。

③ メソッド領域にはガベージコレクション機構があり、一部のクラスが使用されなくなった場合はガベージとなり、クリーンアップする必要があります。

(2) 構成

JVM1.6バージョンのメモリ構造:

PermGen 永続世代をメソッド領域の実装として使用します。この永続世代には次の情報が含まれます: Class クラス情報、ClassLoader クラスローダー情報、StringTable (文字列テーブル) ランタイム定数プール

JVM1.8バージョンのメモリ構造:

Metaspace メタスペースをメソッド領域の実装として使用し、次の情報を格納します: Class クラス情報、ClassLoader クラスローダー情報、定数プール (上記とは異なります);ヒープ メモリは占有されません。 JVM のメモリ構造を管理し、ローカル メモリ (OS メモリ) に移動

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

①JDK1.8以前では永続世代メモリオーバーフローが発生する

メモリの上限は設定しませんでした。10000 クラスすべてがメモリにロードされます。パラメータを使用して設定し、ソース空間メモリのサイズを指定できます: -XX:MaxPermSize= 8m

package cn.itcast.jvm.t1.metaspace;

import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

// 把下面10000个类加载到内存当中
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 20000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号, public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}

-XX:MaxPermSize=8m を追加すると 19314 回ループするだけで、永続的な世代のオーバーフロー例外がスローされます。

②JDK1.8ではメタスペースでメモリオーバーフローが発生する

同じコードで、パラメータを使用して設定し、ソース空間メモリのサイズを指定します: -XX:MaxMetaspaceSize=8m

-XX:MaxMetaspaceSize=8m を追加すると 5411 回ループするだけで、メタスペース メモリ オーバーフローがスローされます

(4) ランタイム定数プール

①まずはコンスタントプールを理解する

バイナリ バイトコード (以下を含む) の場合:基本クラス情報、定数プール、クラス メソッド定義 (仮想マシン命令を含む)。まず次のコードを確認し、HelloWorld.class ファイルをコンパイルして生成し、逆コンパイルするにはjavap -v HelloWorld.classを使用します。

package cn.itcast.jvm.t5;
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

定数プールはテーブルです。仮想マシン命令は、この定数テーブルに従ってクラス名、メソッド名、パラメータのタイプ、リテラル値、および実行されるその他の情報を見つけます。次に例を示します。  

②ランタイム定数プール

定数プールは *.class ファイル内にあり、クラスがロードされると、その定数プール情報が実行時定数プールに入れられ、内部のシンボル アドレスが実際のアドレスに変更されます。

(5)文字列テーブル

文字列テーブルの機能:

① 定数プール内の文字列は単なるシンボルであり、初めて使用されるときはオブジェクトになります

② 文字列プールメカニズムを使用して、文字列オブジェクトの繰り返し作成を回避します

③ 文字列変数の結合原理はStringBuilder (1.8) です。

④ 文字列定数スプライシングの原理はコンパイル時の最適化です。

intern メソッドを使用すると、文字列プールにない文字列オブジェクトを文字列プールに積極的に入れることができます

1.8 の場合:この文字列オブジェクトを文字列プールに入れてみます。存在する場合は文字列プールに入れられません。存在しない場合は文字列プールに入れられ、文字列プール内のオブジェクトが返されます。

1.6 の場合: この文字列オブジェクトを文字列プールに入れようとします。存在する場合は、文字列プールには入れられません。存在しない場合は、このオブジェクトのコピーを作成し、文字列プールに入れて、オブジェクトを返します。文字列プール。

上記のプロパティを確認します。

String s1 = "a";
String s2 = "b";
String s3 = "ab";

逆コンパイルするには:

#2 は文字列 a に対応し、#3 は文字列 b に対応し、#4 は文字列 ab に対応します。

 astore_1 は、ロードされた文字列オブジェクトを 1 番のローカル変数 s1 に格納します。以下同様です。

定数プールはバイトコード ファイル .class に存在し、実行時にランタイム定数プールに配置されますが、ランタイム定数プールにロードされるとき、それを参照するために特別に実行されるまでは、まだ Java 文字列オブジェクトにはなりません。そのコード行; 例: String s1 = "a" が実行されると、ldc #2 は a シンボルを "a" 文字列オブジェクトに変更します; このとき、スペース StringTable が準備され、"a"文字列オブジェクトが配置されます (オブジェクトが存在しない場合) に移動します。これは実際には遅延読み込み (遅延) 動作です。文字列プールにオブジェクトが存在する場合は、それが直接使用されます。 1部になってください!

したがって、s1、s2、および s3 が指す "a"、"b"、および "ab" は、文字列定数プール StringTable と StringTable の最下層に配置されます [ "a", "b" , "ab " ] はハッシュテーブル構造であるため、展開できません。

String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;

s4=s1+s2、変数スプライシング、s4 参照は、最初に StringBuilder オブジェクトを作成し、次に append メソッドを呼び出し、「a」と「b」を結合してから、toString メソッドを呼び出します。StringBuilder の toString メソッドの基礎となるソース コードを確認します。検出では、新しい文字列オブジェクト new String("ab") を作成します。

String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
// 问
System.out.println(s3 == s4);

s3 == s4の結果?

s3 に対応する "ab" は文字列定数プール内のオブジェクトですが、s4 は新しく作成された文字列オブジェクトです。値は同じですが、s3 が文字列プール内にあり、s4 が最初に作成されます。 = = 比較は次のようになります。アドレスは異なる必要があり、結果は false になります。

String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // 变量拼接
String s5 = "a" + "b"; // 常量拼接
// 问
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true

s5 = "a" + "b" は、結合された "ab" オブジェクトを直接検索します。これは、コンパイラでの javac の最適化です。コンパイル中に、それが "ab" オブジェクトであることを確認できます。今度は定数プールにこのオブジェクトがすでに存在するため、s3 == s5 の結果は true になります。

注: s4=s1+s2 は動作中にのみ決定できるため、ダイナミック スプライシングを使用してください。

JDK1.8: この文字列オブジェクトを文字列プールに入れようとします。存在する場合は文字列プールに入れられません。存在しない場合は文字列プールに入れられ、文字列プール内のオブジェクトは返される!

String s = new String("a") + new String("b");
// 使用JDK1.8,会将这个字符串对象尝试放入串池,
// 如果有则不会放入,如果没有则放入串池,并把串池中的对象返回
String s2 = s.intern();
System.out.println(s2 == "ab"); // true
System.out.println(s == "ab"); // true

s = new String("a") + new String("b"); まず、"a"と"b"は定数プールに入れられますが、s="ab"は変数なので入れられませんスプライシング, 文字列オブジェクトが作成され、ヒープに保存されます。定数プールに「ab」を入れたい場合は、intern メソッドを呼び出して、文字列定数プールに「ab」を入れることができます。この時点で、s のオブジェクトを定数プールに入れ、s2 は文字列プール内のオブジェクトの戻り値であるため、両方とも true になります。

String s = new String("a") + new String("b");

String x = "ab";

String s2 = s.intern();
System.out.println(s2 == x); // true
System.out.println(s == x); // false

このとき、 String x = "ab' は "ab" を文字列プールに置きます。このとき、"ab" オブジェクトは文字列プールにすでに存在しており、この時点では s.intern() は " ab" オブジェクトを文字列プールに入力します。また、s2 = s.intern の戻り値は文字列プール内のオブジェクトである必要があります。したがって、この時点では、s2 == x は true、s == x は false です。

JDK1.6: この文字列オブジェクトを文字列プールに入れようとします。存在する場合は入れられません。存在しない場合は、このオブジェクトをコピーして文字列プールに入れ、文字列プール内のオブジェクトを返します。 !

String s = new String("a") + new String("b");
// 使用JDK1.6,会将这个字符串对象尝试放入串池,
// 如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
String s2 = s.intern();
System.out.println(s2 == "ab"); // true
System.out.println(s == "ab"); // false

JDK1.6 を使用する主な違いは s.intern です。このとき、s のオブジェクト自体を文字列プールに置くのではなく、コピーが文字列プールに置かれます。s はまだヒープ内のオブジェクトです。この時点では、s はまだオブジェクトです。時間、s == " ab" は false です。

典型的な面接の質問:

String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; // ab
String s4 = s1 + s2; // new String("ab")
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d"); // new String("cd")
String x1 = "cd"; // "cd"
x2.intern();
System.out.println(x1 == x2); // false

// 问,如果调换了x1,x2的位置呢?如果是jdk1.6呢?
String x2 = new String("c") + new String("d"); // new String("cd")
x2.intern(); //先把“ab”入常量池
String x1 = "cd"; 
System.out.println(x1 == x2); 
// 此时对于JDK1.8-true,对于JDK1.6-false

(6) StringTableの場所

JDK1.6の場合

JDK1.8の場合

 では、StringTable の位置をコードを通じて直感的に反映できるでしょうか?

import java.util.ArrayList;
import java.util.List;

public class Demo1_6 {

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        int i = 0;
        try {
            for (int j = 0; j < 260000; j++) {
                list.add(String.valueOf(j).intern());
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

JDK1.6 の場合、永続世代メモリをより小さく設定します: -XX:MaxPermSize=10m

JDK1.8 の場合、ヒープ メモリをより小さく設定します: -Xmx10m。現時点では、ヒープ メモリ エラーは発生しません。次のプロンプトは、エネルギーの 98% がリサイクルに使用されているが、値は 2 によって回収されることを示しています。 %、このエラー メッセージが報告されます。

現時点では、このプロンプトを閉じるためのパラメータ -Xmx10m -XX:-UseGCOverheadLimitを追加する必要があります。

(7) StringTableのガベージコレクション機構

StringTable はガベージ コレクション機構によっても管理されており、メモリ領域が不足した場合、StringTable 内の参照されていない文字列定数はガベージ コレクションによってリサイクルされます。

設定パラメータ: -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc

-Xmx10m: 仮想マシンのヒープ メモリの最大値を設定します。

-XX:+PrintStringTableStatistics: 文字列テーブルの統計情報を出力します。

-XX:+PrintGCDetails -verbose:gc: ガベージ コレクションに関する情報を出力します (ガベージ コレクションが発生した場合)。

package cn.itcast.jvm.t1.stringtable;

import java.util.ArrayList;
import java.util.List;

public class Demo1_7 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 100000; j++) { // j=100, j=10000
                String.valueOf(j).intern(); // 入池
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}

①先にループコードを追加せず、この時点でStringTableの格納を確認してください。

②サイクルが100回の場合、この時点ではヒープメモリのサイズを超えていないため、ガベージコレクション機構はトリガーされません

③サイクルが10,000回の場合、その時点でヒープメモリのサイズを超えており、リサイクルのためにガベージコレクションメカニズムがトリガーされます。

 ガベージ コレクション メカニズムの情報の出力が開始されます。

(8) StringTableのパフォーマンスチューニング

方法 1: -XX:StringTableSize = バケット数を調整する

StringTable の基礎となる層はハッシュ テーブル (配列 + リンク リスト) です。ハッシュ テーブルのパフォーマンスはそのサイズに密接に関係しており、ハッシュ テーブル内のバケットの数が大きい場合、要素は分散し、確率は高くなります。ハッシュ衝突が軽減され、検索速度が速くなります。バケットの数が少ない場合、ハッシュ衝突の可能性が増加し、リンクされたリストが長くなり、検索速度に影響します。

package cn.itcast.jvm.t1.stringtable;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * 演示串池大小对性能的影响
 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
 */
public class Demo1_24 {

    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
            String line = null;
            long start = System.nanoTime();
            while (true) {
                line = reader.readLine();
                if (line == null) {
                    break;
                }
                line.intern(); // 入串池
            }
            System.out.println("cost:" + (System.nanoTime() - start) / 1000000); // 毫秒
        }


    }
}

いわゆるチューニングで、最も重要なことはバケットの数を調整することです-XX:StringTableSize = バケットの数、仮想マシンのメモリの最大値を設定せずに、40,000 を超えるデータを簡単に仮想マシンに入力できます。プール!

-XX:StringTableSize = 200000、バケット数を 200000 に調整します。

-XX:StringTableSizeパラメータを指定しない場合、使用されるデフォルトのバケット サイズは 60013 です。

結論: バケットの数が少ないほど時間はかかります。バケットの最小数は 1009 です。

方法 2: 文字列オブジェクトをプールに入れるかどうかを検討する

たとえば、現在大量の文字列オブジェクトが作成されているとします。linux.words ファイルには 48,000 個の文字列があり、サイクルが 10 回中断され、プールと下位プールのメモリ使用量が比較されます。

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

public class Demo1_25 {

    public static void main(String[] args) throws IOException {

        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i = 0; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    // 不入池
                    address.add(line);
                    // 入池
                    address.add(line.intern());
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();


    }
}

①address.add(line)はプールに入りませんが、この時点でListコレクションに48万件のデータが追加されます。

jvisualvm ツールを使用して、サンプラーを選択します。

メモリ使用量のグラフィック表示:

読み取り前、この時点で文字列が占有するメモリは約 1M です。

 読み取り後、この時点で文字列が占有するメモリは約 110M です。

②Address.add(line.intern()) がプールに入ります プールに入った後、次の 9 サイクルのデータが繰り返され、最初にプールに入ったデータのみが直接使用できます。 time, String+ 作成される char 配列はわずか 40M 未満であり、ヒープ メモリ領域を大幅に節約できます。

3: 直接記憶

(1) 定義

直接メモリは Java 仮想マシンのメモリには属しません。それはオペレーティング システムのメモリです。

ダイレクトメモリ: DirectMemory

① NIO 操作で一般的に使用され、データ バッファーに使用されます。 

② 割り当てと回復のコストは高いが、読み取りと書き込みのパフォーマンスは高い。 

③ JVM メモリ回復管理の対象外。

 ケース: 従来の IO ストリームとダイレクト メモリを使用した比較

package cn.itcast.jvm.t1.direct;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 演示 ByteBuffer 作用
 */
public class Demo1_9 {
    static final String FROM = "E:\\编程资料\\text.txt";
    static final String TO = "E:\\a.txt";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // io 用时:3秒左右
        directBuffer(); // directBuffer 用时:1秒左右
    }
    // 使用直接内存的方式
    private static void directBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
             FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
            while (true) {
                int len = from.read(bb);
                if (len == -1) {
                    break;
                }
                bb.flip();
                to.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
    }
    // 传统的IO流
    private static void io() {
        long start = System.nanoTime();
        try (FileInputStream from = new FileInputStream(FROM);
             FileOutputStream to = new FileOutputStream(TO);
        ) {
            byte[] buf = new byte[_1Mb];
            while (true) {
                int len = from.read(buf);
                if (len == -1) {
                    break;
                }
                to.write(buf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io 用时:" + (end - start) / 1000_000.0);
    }
}

ダイレクト メモリ ByteBuffer を使用したファイル (特に大きなファイル) のコピー速度が従来の IO ストリームよりも大幅に高速であることがわかり、ファイルの読み取りと書き込みのプロセスから分析します。

従来の IO ストリームの場合:

Java 自体にはディスクの読み取りおよび書き込み機能がなく、オペレーティング システムによって提供される関数を呼び出す必要があります。つまり、Java メソッドからローカル メソッドを呼び出す必要があります。このとき、CPU はユーザー モードからカーネル モードに切り替わります。このとき、ディスク ファイルを読み取ることができます。このとき、オペレーティング システムにバッファー層 (システム バッファー) が描画され、ディスクの内容が最初にこのシステム バッファーに読み込まれます (個別の読み取りのため、Java コードは読み取ることができません)。システム バッファ (システム バッファの内容) を読み取ります)。このとき、Java はヒープ メモリに Java バッファも割り当てます。Java がデータを読み込む場合は、まずシステム キャッシュから Java バッファにデータを読み取る必要があります。2 つのバッファ、同等です。読み取り時に 2 つのコピーを保存する必要があり (不必要なデータの重複が発生する)、効率が低下します。

ダイレクトメモリの場合:

ByteBuffer が assignDirect メソッドを呼び出すと、オペレーティング システム間にバッファ (ダイレクト メモリ) が描画され、この領域の Java コードに直接アクセスできるようになり、このメモリにはオペレーティング システムと Java コードの両方から直接アクセスできます。エリア; バッファ読み取りは 1 つだけなので、より効率的です。

これは JVM メモリ回復によって管理されないため、次のようなダイレクト メモリもメモリ オーバーフローを引き起こします。

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;


// 演示直接内存溢出
public class Demo1_10 {
    static int _100Mb = 1024 * 1024 * 100;

    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
        }
        // 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
        //                  jdk8 对方法区的实现称为元空间
    }
}

ダイレクト メモリを不適切に使用すると、メモリ オーバーフローが発生する可能性もあります。

(2) 配分と回収の原則

例:

public class Test {
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb); // 分配1G的空间
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null; // 空引用
        System.gc(); // 启动垃圾回收
        System.in.read();
    }
}

タスクマネージャーを表示し、1G を割り当てます。

タスク マネージャーを確認して null に設定し、ガベージ コレクション メカニズムを開始すると、それがリサイクルされていることがわかります。直接メモリは JVM メモリのリサイクルによって管理されないと言われませんでしたか? ガベージ コレクション後にダイレクト メモリが再利用され、解放されるのはなぜですか?

これには、直接メモリの解放原理の説明が必要です。まず、何らかの方法で安全でないオブジェクトを取得します。その後、安全でないオブジェクトを通じて直接メモリの割り当てとリサイクルを完了できます。

注: 直接メモリ監視の場合、IDEA ではこれらの監視ツールを使用できません。タスク マネージャーでプロセスを確認する必要があります。

メモリを割り当てる:

long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (byte) 0);

空きメモリ:

unsafe.freeMemory(base);

ByteBuffer.allocateDirect メソッドの最下層は、ByteBuffer を使用する実装クラス DirectByteBuffer です。

DirectByteBuffer のコンストラクターで、安全でない assignMemory メソッドが呼び出され、ダイレクト メモリが割り当てられます。

DirectByteBuffer のコンストラクターは、Cleaner (仮想参照) を使用して ByteBuffer オブジェクトを監視します。ByteBufferオブジェクトがガベージ コレクションされると、ReferenceHandler スレッドは Cleaner の clean メソッドを通じてタスク オブジェクト Deallocator を実行します。タスク オブジェクトは安全でないオブジェクトを呼び出します。 freeMemory : 直接メモリを解放する

-XX:+DisableExplicitGC:ダイレクト メモリに対する明示的なリサイクルの影響を無効にします。これにより System.gc() が無効になりますが、現時点ではダイレクト メモリの解放に影響します。安全でないオブジェクトを使用してダイレクト メモリを手動で解放できます。 !

おすすめ

転載: blog.csdn.net/m0_61933976/article/details/129228283