Java-virtual-machine-quick-guide

提供:Dev Guides
移動先:案内検索

Java仮想マシン-はじめに

JVMは仕様であり、仕様に準拠している限り、異なる実装を持つことができます。 仕様は以下のリンクにあります-https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2l[https://docs.oracle.com]

Oracleには独自のJVM実装(HotSpot JVMと呼ばれる)があり、IBMには独自のJVM実装(たとえば、J9 JVM)があります。

仕様内で定義されている操作を以下に示します(ソース-Oracle JVM仕様、上記のリンクを参照)-

  • 「クラス」ファイル形式
  • データ型
  • プリミティブ型と値
  • 参照タイプと値
  • 実行時データ領域
  • フレーム
  • オブジェクトの表現
  • 浮動小数点演算
  • 特別な方法
  • 例外
  • 命令セットの概要
  • クラスライブラリ
  • パブリックデザイン、プライベート実装

JVMは仮想マシンであり、独自のISA、独自のメモリ、スタック、ヒープなどを備えた抽象的なコンピューターです。 ホストOS上で実行され、リソースに対する要求をホストOSに送信します。

Java仮想マシン-アーキテクチャ

HotSpot JVM 3のアーキテクチャを以下に示します-

アーキテクチャ

実行エンジンは、ガベージコレクタとJITコンパイラで構成されます。 JVMには、*クライアントとサーバー*の2つのフレーバーがあります。 これらは両方とも同じランタイムコードを共有しますが、使用されるJITが異なります。 これについては後で詳しく説明します。 ユーザーは、JVMフラグ_-client_または_-server_を指定して、使用するフレーバーを制御できます。 サーバーJVMは、サーバー上で長時間実行されるJavaアプリケーション用に設計されています。

JVMには32bバージョンと64bバージョンがあります。 ユーザーは、VM引数で-d32または-d64を使用して、使用するバージョンを指定できます。 32bバージョンでは、最大4Gのメモリしかアドレスできませんでした。 重要なアプリケーションがメモリ内に大きなデータセットを保持しているため、64bバージョンはそのニーズを満たします。

Java仮想マシン-クラスローダー

JVMは、クラスとインターフェースのロード、リンク、および初期化のプロセスを動的に管理します。 ロードプロセス中に、* JVMはクラスのバイナリ表現を見つけて作成します。*

リンクプロセス中に、読み込まれたクラスはJVMの実行時状態に結合され、初期化フェーズ中に実行できるようになります。 JVMは基本的に、リンクプロセスの実行時定数プールに格納されているシンボルテーブルを使用します。 初期化は、実際には*リンクされたクラスの実行*で構成されます。

ローダーの種類

*BootStrap* クラスローダーは、クラスローダー階層の最上位にあります。 JREの_lib_ディレクトリに標準のJDKクラスをロードします。
*Extension* クラスローダーはクラスローダー階層の中央にあり、ブートストラップクラスローダーの直接の子であり、JREのlib \ extディレクトリにクラスをロードします。

アプリケーション*クラスローダーは、クラスローダー階層の最下部にあり、アプリケーションクラスローダーの直接の子です。 *CLASSPATH ENV 変数で指定されたjarおよびクラスをロードします。

リンク

リンクプロセスは、次の3つのステップで構成されています-

検証-これは、生成された.classファイル(バイトコード)が有効であることを確認するために、バイトコード検証によって行われます。 そうでない場合、エラーがスローされ、リンクプロセスが停止します。

準備-メモリはクラスのすべての静的変数に割り当てられ、デフォルト値で初期化されます。

解像度-すべてのシンボリックメモリ参照が元の参照に置き換えられます。 これを達成するために、クラスのメソッド領域のランタイム定数メモリ内のシンボルテーブルが使用されます。

初期化

これは、クラス読み込みプロセスの最終段階です。 静的変数には元の値が割り当てられ、静的ブロックが実行されます。

Java仮想マシン-ランタイムデータ領域

JVM仕様は、プログラムの実行中に必要な特定の実行時データ領域を定義しています。 それらの一部は、JVMの起動時に作成されます。 その他はスレッドに対してローカルであり、スレッドが作成されたときにのみ作成されます(スレッドが破棄されると破棄されます)。 これらは以下にリストされています-

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

各スレッドに対してローカルであり、スレッドが現在実行しているJVM命令のアドレスが含まれています。

スタック

各スレッドに対してローカルであり、メソッド呼び出し中にパラメーター、ローカル変数、およびリターンアドレスを格納します。 スレッドが許可されているよりも多くのスタックスペースを必要とする場合、StackOverflowエラーが発生する可能性があります。 スタックが動的に拡張可能な場合でも、OutOfMemoryErrorをスローできます。

Heap

すべてのスレッド間で共有され、実行時に作成されるオブジェクト、クラスのメタデータ、配列などが含まれます。 JVMの起動時に作成され、JVMのシャットダウン時に破棄されます。 特定のフラグを使用して、JVMがOSに要求するヒープの量を制御できます(これについては後で説明します)。 重要なパフォーマンスへの影響があるため、必要なメモリが少なすぎたり多すぎたりしないように注意する必要があります。 さらに、GCはこのスペースを管理し、デッドオブジェクトを継続的に削除してスペースを解放します。

メソッドエリア

この実行時領域はすべてのスレッドに共通であり、JVMの起動時に作成されます。 定数プール(詳細は後述)、コンストラクターとメソッドのコード、メソッドデータなどのクラスごとの構造を格納します。 JLSは、この領域をガベージコレクションする必要があるかどうかを指定しないため、JVMの実装はGCを無視することを選択できます。 さらに、これはアプリケーションのニーズに応じて拡張される場合とされない場合があります。 JLSはこれに関して何も強制しません。

実行時定数プール

JVMは、ロードされたクラスをリンクするときにシンボルテーブル(多くのロールの1つ)として機能するクラスごと/タイプごとのデータ構造を維持します。

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

スレッドがネイティブメソッドを呼び出すと、Java仮想マシンの構造とセキュリティ制限がその自由を妨げることのない新しい世界に入ります。 ネイティブメソッドは、仮想マシンのランタイムデータ領域にアクセスする可能性があります(ネイティブメソッドインターフェイスに依存します)が、必要な他のすべての操作も実行できます。

ガベージコレクション

JVMは、Javaのオブジェクトのライフサイクル全体を管理します。 オブジェクトが作成されると、開発者はそれを心配する必要がなくなります。 オブジェクトが死んだ場合(つまり、オブジェクトへの参照がなくなった場合)、シリアルGC、CMS、G1などの多くのアルゴリズムのいずれかを使用して、GCによってヒープから排出されます。

GCプロセス中に、オブジェクトはメモリ内で移動されます。 したがって、プロセスが進行している間、これらのオブジェクトは使用できません。 プロセス全体の間、アプリケーション全体を停止する必要があります。 このような一時停止は「世界を停止する」一時停止と呼ばれ、大きなオーバーヘッドです。 GCアルゴリズムは、主にこの時間を短縮することを目的としています。 これについては、次の章で詳しく説明します。

GCのおかげで、メモリリークはJavaでは非常にまれですが、発生する可能性があります。 Javaでメモリリークを作成する方法については、後の章で説明します。

Java仮想マシン-JITコンパイラー

この章では、JITコンパイラーと、コンパイル言語とインタープリター言語の違いについて学習します。

コンパイル済みvs. 通訳言語

C、C ++、FORTRANなどの言語はコンパイルされた言語です。 それらのコードは、基礎となるマシンを対象としたバイナリコードとして配信されます。 つまり、高レベルのコードは、基礎となるアーキテクチャ専用に作成された静的コンパイラーによってバイナリコードに一度にコンパイルされます。 生成されたバイナリは、他のアーキテクチャでは実行されません。

一方、PythonやPerlなどのインタープリター言語は、有効なインタープリターがあれば、どのマシンでも実行できます。 高レベルのコードを1行ずつ調べ、それをバイナリコードに変換します。

通常、解釈されたコードはコンパイルされたコードよりも遅くなります。 たとえば、ループを考えます。 インタプリタは、ループの各反復に対応するコードを変換します。 一方、コンパイルされたコードは、翻訳を1つだけにします。 さらに、インタープリターは一度に1行しか表示しないため、コンパイラーなどのステートメントの実行順序を変更するなど、重要なコードを実行できません。

私たちは、以下のそのような最適化の例を見ていきます-

メモリに保存された2つの数字を追加。 メモリへのアクセスは複数のCPUサイクルを消費する可能性があるため、優れたコンパイラはメモリからデータをフェッチし、データが利用可能な場合にのみ追加を実行する命令を発行します。 待機せず、その間に他の命令を実行します。 一方、インタープリターはコード全体を常に認識していないため、解釈中にそのような最適化は不可能です。

ただし、その場合、インタープリター言語は、その言語の有効なインタープリターを備えたマシンで実行できます。

Javaはコンパイルまたは解釈されますか?

Javaは妥協点を見つけようとしました。 JVMはjavacコンパイラと基礎となるハードウェアの間に位置するため、javac(またはその他のコンパイラ)コンパイラは、プラットフォーム固有のJVMが認識するバイトコードでJavaコードをコンパイルします。 JVMは、コードの実行時に、JIT(Just-in-time)コンパイルを使用して、バイトコードをバイナリでコンパイルします。

ホットスポット

典型的なプログラムでは、頻繁に実行されるコードはごくわずかです。多くの場合、このコードがアプリケーション全体のパフォーマンスに大きな影響を与えます。 このようなコードのセクションは、 HotSpots と呼ばれます。

コードの一部が一度しか実行されない場合、それをコンパイルするのは労力の無駄であり、代わりにバイトコードを解釈する方が速いでしょう。 しかし、セクションがホットセクションであり、複数回実行される場合、JVMは代わりにコンパイルします。 たとえば、メソッドが複数回呼び出された場合、コードのコンパイルにかかる余分なサイクルは、生成される高速なバイナリによって相殺されます。

さらに、JVMが特定のメソッドまたはループを実行するほど、より高速なバイナリが生成されるようにさまざまな情報が収集され、さまざまな最適化が行われます。

私たちは次のコードを考えてみましょう-

for(int i = 0 ; I <= 100; i++) {
   System.out.println(obj1.equals(obj2));//two objects
}

このコードが解釈されると、インタープリターは反復ごとにobj1のクラスを推測します。 これは、Javaの各クラスに.equals()メソッドがあり、Objectクラスから拡張されており、オーバーライドできるためです。 したがって、obj1が各反復の文字列であっても、演deは行われます。

一方、実際に発生することは、JVMが各反復でobj1がStringクラスであることに気付くため、Stringクラスの.equals()メソッドに対応するコードを直接生成することです。 したがって、ルックアップは不要であり、コンパイルされたコードはより高速に実行されます。

この種の動作は、JVMがコードの動作を知っている場合にのみ可能です。 したがって、コードの特定のセクションをコンパイルする前に待機します。

以下は別の例です-

int sum = 7;
for(int i = 0 ; i <= 100; i++) {
   sum += i;
}

インタープリターは、ループごとに、「sum」の値をメモリから取得し、「I」を追加して、メモリに保存します。 メモリアクセスは高価な操作であり、通常は複数のCPUサイクルがかかります。 このコードは複数回実行されるため、HotSpotです。 JITはこのコードをコンパイルし、次の最適化を行います。

「sum」のローカルコピーは、特定のスレッドに固有のレジスタに保存されます。 すべての操作はレジスタ内の値に対して実行され、ループが完了すると、値がメモリに書き戻されます。

他のスレッドが変数にアクセスしている場合はどうなりますか? 更新は他のスレッドによって変数のローカルコピーに対して行われているため、古い値が表示されます。 このような場合、スレッドの同期が必要です。 非常に基本的な同期プリミティブは、「sum」を揮発性として宣言することです。 現在、変数にアクセスする前に、スレッドはローカルレジスタをフラッシュし、メモリから値をフェッチします。 アクセスすると、値はすぐにメモリに書き込まれます。

以下は、JITコンパイラによって行われるいくつかの一般的な最適化です-

  • メソッドのインライン化
  • デッドコード除去
  • 呼び出しサイトを最適化するためのヒューリスティック
  • 一定の折りたたみ

Java仮想マシン-コンパイルレベル

JVMは5つのコンパイルレベルをサポートしています-

  • 通訳
  • 完全に最適化されたC1(プロファイリングなし)
  • 呼び出しおよびバックエッジカウンターを備えたC1(ライトプロファイリング)
  • 完全なプロファイリングを備えたC1
  • C2(前の手順のプロファイリングデータを使用)

すべてのJITコンパイラを無効にし、インタープリターのみを使用する場合は、-Xintを使用します。

クライアント対 サーバーJIT

-clientおよび-serverを使用して、それぞれのモードをアクティブにします。

クライアントコンパイラ(C1)は、サーバーコンパイラ(C2)よりも早くコードのコンパイルを開始します。 そのため、C2がコンパイルを開始するまでに、C1はすでにコードのセクションをコンパイルしていました。

しかし、待機中、C2はコードをプロファイリングして、C1よりもコードを認識します。 したがって、最適化によるオフセットを使用してはるかに高速なバイナリを生成できる場合に待機する時間。 ユーザーの観点から見ると、トレードオフはプログラムの起動時間とプログラムの実行にかかる時間の間です。 起動時間が重要な場合は、C1を使用する必要があります。 アプリケーションが長時間実行されることが予想される場合(通常、サーバーにデプロイされるアプリケーションの場合)、C2を使用すると、余分な起動時間を大幅に相殺するはるかに高速なコードが生成されるため、より適切です。

IDE(NetBeans、Eclipse)などのプログラムやその他のGUIプログラムでは、起動時間が非常に重要です。 NetBeansの起動には1分以上かかる場合があります。 NetBeansなどのプログラムを起動すると、数百のクラスがコンパイルされます。 このような場合、C1コンパイラーが最適です。

C1には2つのバージョンがあることに注意してください- 32bおよび64b 。 C2は 64b でのみ提供されます。

階層型コンパイル

Javaの古いバージョンでは、ユーザーは次のオプションのいずれかを選択できました-

  • 通訳者(-Xint)
  • C1(-クライアント)
  • C2(-サーバー)

Java 7で提供されました。 C1コンパイラを使用して起動し、コードが熱くなるとC2に切り替えます。 JVMオプション-XX:+ TieredCompilationでアクティブ化できます。 デフォルト値は* Java 7ではfalseに設定され、Java 8ではtrueに設定されます*。

5段階のコンパイルのうち、段階コンパイルでは 1→ 4→ 5 を使用します。

Java仮想マシン-32bと 64b

32bマシンでは、32bバージョンのJVMのみをインストールできます。 64bマシンでは、ユーザーは32bバージョンと64bバージョンを選択できます。 しかし、Javaアプリケーションのパフォーマンスに影響を与える可能性のある微妙な違いがあります。

Javaアプリケーションが使用するメモリが4G未満の場合、64bマシンでも32b JVMを使用する必要があります。 これは、この場合のメモリ参照は32bのみであり、それらの操作は64bアドレスの操作よりも安価であるためです。 この場合、OOPS(通常のオブジェクトポインター)を使用していても、64b JVMのパフォーマンスは低下します。 OOPSを使用すると、JVMは64b JVMで32bアドレスを使用できます。 ただし、基礎となるネイティブ参照は依然として64bであるため、それらの操作は実際の32b参照よりも遅くなります。

アプリケーションが4Gを超えるメモリを消費する場合、32b参照は4Gを超えるメモリをアドレス指定できないため、64bバージョンを使用する必要があります。 同じマシンに両方のバージョンをインストールし、PATH変数を使用してそれらを切り替えることができます。

Java仮想マシン-JITの最適化

この章では、JIT最適化について学習します。

メソッドのインライン化

この最適化手法では、コンパイラーは関数呼び出しを関数本体に置き換えることを決定します。 以下は同じものの例です-

int sum3;

static int add(int a, int b) {
   return a + b;
}

public static void main(String…args) {
   sum3 = add(5,7) + add(4,2);
}

//after method inlining
public static void main(String…args) {
   sum3 = 5+ 7 + 4 + 2;
}

この手法を使用すると、コンパイラーは関数呼び出しのオーバーヘッドからマシンを節約します(パラメーターをスタックにプッシュおよびポップする必要があります)。 したがって、生成されたコードはより高速に実行されます。

メソッドのインライン化は、非仮想関数(オーバーライドされない関数)に対してのみ実行できます。 「add」メソッドがサブクラスでオーバーライドされ、メソッドを含むオブジェクトのタイプが実行時までわからない場合はどうなるかを考えてください。 この場合、コンパイラはインライン化するメソッドを認識しません。 しかし、メソッドが「最終」としてマークされている場合、コンパイラは、サブクラスでオーバーライドできないため、インラインにできることを容易に認識します。 最終メソッドが常にインライン化されることはまったく保証されないことに注意してください。

到達不能およびデッドコードの排除

到達不能コードとは、実行フローによって到達できないコードです。 私たちは次の例を検討します-

void foo() {
   if (a) return;
   else return;
   foobar(a,b);//unreachable code, compile time error
}

デッドコードも到達不能コードですが、この場合、コンパイラはエラーを吐き出します。 代わりに、警告が表示されます。 コンストラクター、関数、try、catch、if、whileなどの各コードブロックには、JLS(Java Language Specification)で定義された到達不能コードに関する独自のルールがあります。

一定の折りたたみ

定数の折りたたみの概念を理解するには、以下の例を参照してください。

final int num = 5;
int b = num * 6;//compile-time constant, num never changes
//compiler would assign b a value of 30.

Java仮想マシン-ガベージコレクション

Javaオブジェクトのライフサイクルは、JVMによって管理されます。 プログラマがオブジェクトを作成したら、残りのライフサイクルについて心配する必要はありません。 JVMは、使用されなくなったオブジェクトを自動的に検出し、ヒープからメモリを回収します。

ガベージコレクションはJVMが行う主要な操作であり、ニーズに合わせてチューニングすると、アプリケーションのパフォーマンスが大幅に向上します。 最新のJVMによって提供されるガベージコレクションアルゴリズムにはさまざまなものがあります。 どのアルゴリズムを使用するかを決定するために、アプリケーションのニーズを認識する必要があります。

CやC ++などのGC以外の言語で実行できるように、Javaでプログラムでオブジェクトの割り当てを解除することはできません。 そのため、Javaでぶら下がり参照を使用することはできません。 ただし、null参照(JVMがオブジェクトを保存しないメモリ領域を参照する参照)がある場合があります。 null参照が使用されるたびに、JVMはNullPointerExceptionをスローします。

GCのおかげでJavaプログラムでメモリリークを見つけることはまれですが、実際に発生することに注意してください。 この章の最後でメモリリークを作成します。

次のGCは、最新のJVMで使用されます

  • シリアルコレクター
  • スループットコレクター
  • CMSコレクター
  • G1コレクター

上記の各アルゴリズムは同じタスクを実行します。使用されなくなったオブジェクトを検索し、ヒープで占有しているメモリを回収します。 これに対する単純なアプローチの1つは、各オブジェクトが持っている参照の数をカウントし、参照の数が0になったらすぐに解放することです(これは参照カウントとも呼ばれます)。 なぜこれがナイーブなのですか? 循環リンクリストを検討してください。 各ノードにはそれへの参照がありますが、オブジェクト全体はどこからも参照されていないため、理想的には解放する必要があります。

JVMはメモリを解放するだけでなく、小さなメモリチャックをより大きなメモリチャックに結合します。 これは、メモリの断片化を防ぐために行われます。

簡単なメモでは、典型的なGCアルゴリズムは次のアクティビティを行います-

  • 未使用のオブジェクトを見つける
  • ヒープで占有しているメモリを解放する
  • フラグメントの合体

GCは、実行中にアプリケーションスレッドを停止する必要があります。 これは、実行時にオブジェクトを移動させるため、これらのオブジェクトを使用できないためです。 このような停止は「世界の停止」と呼ばれ、GCの調整中にこれらの停止の頻度と期間を最小化することを目指しています。

メモリ結合

メモリ合体の簡単なデモを以下に示します

メモリ結合

網掛け部分は、解放する必要があるオブジェクトです。 すべてのスペースが再利用された後でも、最大サイズ= 75Kbのオブジェクトのみを割り当てることができます。 これは、以下に示すように200Kbの空き容量があった後でもです

影付き部分

Java仮想マシン-世代別GC

ほとんどのJVMは、ヒープを3つの世代に分割します-若い世代(YG)、古い世代(OG)、および永続的な世代(テニュア世代とも呼ばれます)。 そのような考えの背後にある理由は何ですか?

実証研究は、作成されたオブジェクトのほとんどが非常に短い寿命を持っていることを示しています-

実証研究

ソース

https://www.oracle.com

時間とともに割り当てられるオブジェクトが増えるにつれて、生き残るバイト数が少なくなることがわかります(一般的に)。 Javaオブジェクトの死亡率は高いです。

簡単な例を見てみましょう。 JavaのStringクラスは不変です。 つまり、Stringオブジェクトのコンテンツを変更する必要があるたびに、新しいオブジェクトを作成する必要があります。 以下のコードに示すように、ループ内で文字列を1000回変更するとします-

String str = “G11 GC”;

for(int i = 0 ; i < 1000; i++) {
   str = str + String.valueOf(i);
}

各ループで、新しい文字列オブジェクトを作成し、前の反復で作成された文字列は役に立たなくなります(つまり、参照によって参照されません)。 そのオブジェクトのTライフタイムは1回の繰り返しでした。GCによってすぐに収集されます。 このような短命のオブジェクトは、ヒープの若い世代の領域に保持されます。 若い世代からオブジェクトを収集するプロセスは、マイナーガベージコレクションと呼ばれ、常に「世界を止める」一時停止を引き起こします。

若い世代がいっぱいになると、GCはマイナーガベージコレクションを実行します。 デッドオブジェクトは破棄され、ライブオブジェクトは古い世代に移動します。 このプロセスの間、アプリケーションスレッドは停止します。

ここでは、このような世代設計が提供する利点を見ることができます。 若い世代はヒープのごく一部であり、すぐにいっぱいになります。 しかし、それを処理するのにかかる時間は、ヒープ全体を処理するのにかかる時間よりもはるかに短いです。 そのため、この場合の「世界停止」の一時停止は非常に短くなりますが、より頻繁に行われます。 より長い休止時間よりも短い休止時間を目標にする必要があります。 これについては、このチュートリアルの後のセクションで詳しく説明します。

若い世代は、 edenとsurvivor space の2つのスペースに分かれています。 エデンの収集中に生き残ったオブジェクトはサバイバースペースに移動し、サバイバースペースを生き残ったオブジェクトは古い世代に移動します。 若い世代は、収集中に圧縮されます。

オブジェクトが古い世代に移動すると、オブジェクトは最終的にいっぱいになり、収集して圧縮する必要があります。 アルゴリズムが異なれば、これに対するアプローチも異なります。 それらのいくつかは、アプリケーションスレッドを停止します(若い世代に比べて古い世代は非常に大きいため、長い「世界の停止」につながります)。 このプロセスはフルGCと呼ばれます。 このような2つのコレクターは、 CMSおよびG1 です。

これらのアルゴリズムを詳細に分析してみましょう。

シリアルGC

これは、クライアントクラスのマシン(シングルプロセッサマシンまたは32b JVM、Windows)のデフォルトGCです。 通常、GCは非常にマルチスレッド化されていますが、シリアルGCはそうではありません。 ヒープを処理する単一のスレッドがあり、マイナーGCまたはメジャーGCを実行するたびにアプリケーションスレッドを停止します。 フラグ -XX:+ UseSerialGC を指定することで、このGCを使用するようにJVMに命令できます。 別のアルゴリズムを使用する場合は、アルゴリズム名を指定します。 古い世代は、メジャーGCの間に完全に圧縮されることに注意してください。

スループットGC

このGCは、64b JVMおよびマルチCPUマシンのデフォルトです。 シリアルGCとは異なり、複数のスレッドを使用して、若い世代と古い世代を処理します。 このため、GCは parallel collector とも呼ばれます。 -XX:+ UseParallelOldGC または -XX:+ UseParallelGC (JDK 8以降の場合)フラグを使用して、このコレクターを使用するようにJVMに命令できます。 メジャーガベージコレクションまたはマイナーガベージコレクションを実行している間、アプリケーションスレッドは停止します。 シリアルコレクターと同様に、メジャーGC中に若い世代を完全に圧縮します。

スループットGCは、YGとOGを収集します。 エデンがいっぱいになると、コレクターはライブオブジェクトをそこからOGまたはサバイバースペースのいずれかに排出します(下図のSS0およびSS1)。 デッドオブジェクトは破棄され、占有されていたスペースが解放されます。

YGのGCの前

YGのGCの前

YGのGC後

YGのGC後

フルGC中、スループットコレクターはYG、SS0、SS1全体を空にします。 操作後、OGにはライブオブジェクトのみが含まれます。 上記の両方のコレクターは、ヒープの処理中にアプリケーションスレッドを停止することに注意してください。 これは、メジャーGCの間に長い「stopthe-world」が一時停止することを意味します。 次の2つのアルゴリズムは、より多くのハードウェアリソースを犠牲にしてそれらを排除することを目的としています-

CMSコレクター

これは「同時マークスイープ」の略です。 その機能は、バックグラウンドスレッドを使用して古い世代を定期的にスキャンし、無効なオブジェクトを取り除くことです。 ただし、マイナーGCの間、アプリケーションスレッドは停止します。 ただし、一時停止はごくわずかです。 これにより、CMSは一時停止コレクターになります。

このコレクターは、アプリケーションスレッドの実行中にヒープをスキャンするために追加のCPU時間を必要とします。 さらに、バックグラウンドスレッドはヒープを収集するだけで、圧縮は実行しません。 ヒープが断片化する可能性があります。 これが継続して行われると、特定の時点の後、CMSはすべてのアプリケーションスレッドを停止し、単一のスレッドを使用してヒープを圧縮します。 次のJVM引数を使用して、CMSコレクターを使用するようJVMに指示します-

  • 「XX:+ UseConcMarkSweepGC -XX:+ UseParNewGC」*をJVM引数として使用して、CMSコレクターを使用するように指示します。

GCの前

GCの前

GC後

GC後

収集は同時に行われていることに注意してください。

G1 GC

このアルゴリズムは、ヒープをいくつかの領域に分割することで機能します。 CMSコレクターと同様に、マイナーGCを実行中にアプリケーションスレッドを停止し、バックグラウンドスレッドを使用して、アプリケーションスレッドを維持しながら古い世代を処理します。 古い世代を領域に分割したため、オブジェクトをある領域から別の領域に移動しながらそれらを圧縮し続けます。 したがって、断片化は最小限です。 フラグ XX:+ UseG1GC を使用して、このアルゴリズムを使用するようJVMに指示できます。 CMSと同様に、ヒープの処理とアプリケーションスレッドの同時実行には、より多くのCPU時間も必要です。

このアルゴリズムは、より大きなヒープ(> 4G)を処理するように設計されており、多数の異なる領域に分割されます。 これらの地域のいくつかは若い世代を構成し、残りは古い世代を構成します。 YGは従来の方法でクリアされます。すべてのアプリケーションスレッドが停止し、古い世代またはサバイバースペースに対してまだ生きているすべてのオブジェクトです。

すべてのGCアルゴリズムがヒープをYGとOGに分割し、STWPを使用してYGをクリアすることに注意してください。 このプロセスは通常非常に高速です。

Java仮想マシン-GCのチューニング

前の章では、さまざまな世代別Gcsについて学びました。 この章では、GCの調整方法について説明します。

ヒープサイズ

ヒープサイズは、Javaアプリケーションのパフォーマンスにおける重要な要素です。 小さすぎる場合は、頻繁にいっぱいになるため、GCによって頻繁に収集する必要があります。 一方、ヒープのサイズを増やすだけでは、収集の頻度を減らす必要がありますが、一時停止の長さが長くなります。

さらに、ヒープサイズを増やすと、基盤となるOSに重大なペナルティが生じます。 OSは、ページングを使用して、アプリケーションプログラムに実際に利用可能なメモリよりもはるかに多くのメモリを認識させます。 OSは、ディスク上の一部のスワップスペースを使用してこれを管理し、プログラムの非アクティブ部分をそこにコピーします。 これらの部分が必要になると、OSはそれらをディスクからメモリにコピーします。

マシンに8Gのメモリがあり、JVMが16Gの仮想メモリを認識している場合、JVMはシステムで実際に8Gしか使用できないことを知りません。 OSに16Gを要求するだけで、そのメモリを取得すると、それを使用し続けます。 OSは大量のデータを入出力する必要があり、これはシステムのパフォーマンスを大幅に低下させます。

そして、そのような仮想メモリのフルGC中に発生する一時停止が発生します。 GCは、収集と圧縮のためにヒープ全体に作用するため、仮想メモリがディスクからスワップアウトされるまでかなり待たなければなりません。 コンカレントコレクターの場合、バックグラウンドスレッドは、スワップスペースからメモリにデータがコピーされるのをかなり待たなければなりません。

そのため、最適なヒープサイズをどのように決定すべきかという問題が生じます。 最初のルールは、実際に存在する以上のメモリをOSに要求しないことです。 これにより、頻繁なスワッピングの問題を完全に防ぐことができます。 マシンに複数のJVMがインストールされて実行されている場合、それらすべてを組み合わせた合計メモリ要求は、システムに存在する実際のRAMよりも少なくなります。

あなたは2つのフラグを使用して、JVMによるメモリ要求のサイズを制御することができます-

  • -XmsN -要求された初期メモリを制御します。
  • -XmxN -要求できる最大メモリを制御します。

これら両方のフラグのデフォルト値は、基盤となるOSによって異なります。 たとえば、MacOS上で実行されている64b JVMの場合、-XmsN = 64Mおよび-XmxN =最小1Gまたは合計物理メモリの1/4です。

JVMは2つの値を自動的に調整できることに注意してください。 たとえば、GCが多すぎることに気付いた場合、-XmxN未満であり、目的のパフォーマンス目標が満たされている限り、メモリサイズを増やし続けます。

アプリケーションに必要なメモリ量が正確にわかっている場合は、-XmsN = -XmxNを設定できます。 この場合、JVMはヒープの「最適な」値を把握する必要がないため、GCプロセスの効率が少し向上します。

世代サイズ

YGに割り当てるヒープの量と、OGに割り当てるヒープの量を決定できます。 これらの値は両方とも、次のようにアプリケーションのパフォーマンスに影響します。

YGのサイズが非常に大きい場合、収集される頻度は少なくなります。 これにより、OGに昇格されるオブジェクトの数が少なくなります。 一方、OGのサイズを大きくしすぎると、OGの収集と圧縮に時間がかかりすぎ、STWの一時停止が長くなります。 したがって、ユーザーはこれら2つの値のバランスを見つける必要があります。

以下は、これらの値を設定するために使用できるフラグです-

  • * -XX:NewRatio = N:* YGとOGの比率(デフォルト値= 2)
  • * -XX:NewSize = N:* YGの初期サイズ
  • * -XX:MaxNewSize = N:* YGの最大サイズ
  • * -XmnN:*このフラグを使用して、NewSizeとMaxNewSizeを同じ値に設定します

YGの初期サイズは、指定された式によるNewRatioの値によって決定されます-

(total heap size)/(newRatio + 1)

newRatioの初期値は2であるため、上記の式はYGの初期値を合計ヒープサイズの1/3に設定します。 NewSizeフラグを使用してYGのサイズを明示的に指定することにより、いつでもこの値をオーバーライドできます。 このフラグにはデフォルト値はありません。明示的に設定されていない場合、YGのサイズは上記の式を使用して計算され続けます。

パーマージェンとメタスペース

パーマゲンとメタスペースは、JVMがクラスのメタデータを保持するヒープ領域です。 Java 7ではスペースは「パーマジェン」と呼ばれ、Java 8では「メタスペース」と呼ばれます。 この情報は、コンパイラとランタイムによって使用されます。

次のフラグを使用して、パーマジェンのサイズを制御できます: -XX:PermSize = N および -XX:MaxPermSize = N 。 メタスペースのサイズは、-XX:Metaspace- Size = N *および *-XX:MaxMetaspaceSize = N を使用して制御できます。

フラグ値が設定されていない場合のパーマゲンとメタスペースの管理方法にはいくつかの違いがあります。 デフォルトでは、両方にデフォルトの初期サイズがあります。 ただし、メタスペースは必要なだけヒープを占有できますが、パーマジェンはデフォルトの初期値を超えて占有することはできません。 たとえば、64b JVMには最大永久磁石サイズとして82Mのヒープスペースがあります。

メタスペースは、指定しない限り無制限のメモリを占有できるため、メモリ不足エラーが発生する可能性があることに注意してください。 これらの領域のサイズが変更されるたびに、フルGCが実行されます。 したがって、起動中にロードされるクラスが多数ある場合、メタスペースはサイズ変更を続けることができ、その結果毎回フルGCになります。 したがって、初期メタスペースのサイズが小さすぎる場合、大規模なアプリケーションの起動には多くの時間がかかります。 起動時間を短縮するため、初期サイズを増やすことをお勧めします。

パーマゲンとメタスペースはクラスのメタデータを保持しますが、永続的ではなく、オブジェクトの場合と同様に、スペースはGCによって回収されます。 これは通常、サーバーアプリケーションの場合です。 サーバーに新しい展開を行うたびに、新しいクラスローダーにはスペースが必要になるため、古いメタデータをクリーンアップする必要があります。 このスペースはGCによって解放されます。

Java仮想マシン-Javaのメモリリーク

この章では、Javaのメモリリークの概念について説明します。

次のコードは、Javaでメモリリークを作成します-

void queryDB() {
   try{
      Connection conn = ConnectionFactory.getConnection();
      PreparedStatement ps = conn.preparedStatement("query");//executes a
      SQL
      ResultSet rs = ps.executeQuery();
      while(rs.hasNext()) {
        //process the record
      }
   } catch(SQLException sqlEx) {
     //print stack trace
   }
}

上記のコードでは、メソッドが終了するとき、接続オブジェクトを閉じていません。 したがって、GCがトリガーされる前に物理接続は開いたままになり、接続オブジェクトを到達不能と見なします。 これで、接続オブジェクトの最終メソッドが呼び出されますが、実装されない場合があります。 したがって、このサイクルではオブジェクトはガベージコレクションされません。

リモートサーバーが接続が長時間開いていることを確認し、強制的に終了するまで、同じことが次に起こります。 したがって、参照のないオブジェクトはメモリに長時間保持され、リークが発生します。