Java仮想マシン-クイックガイド

JVMは仕様であり、仕様に準拠している限り、さまざまな実装を行うことができます。スペックは以下のリンクで見つけることができます-https://docs.oracle.com

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

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

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

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

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

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

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

JVMは、クラスとインターフェースのロード、リンク、および初期化のプロセスを動的に管理します。ロードプロセス中に、JVM finds the binary representation of a class and creates it.

リンクプロセス中に、 loaded classes are combined into the run-time state of the JVM so that they can be executed during the initialization phase。JVMは基本的に、リンクプロセスのために実行時定数プールに格納されているシンボルテーブルを使用します。初期化は実際に構成されていますexecuting the linked classes

ローダーの種類

ザ・ BootStrapクラスローダーは、クラスローダー階層の最上位にあります。JREのlibディレクトリに標準のJDKクラスをロードします。

ザ・ Extension クラスローダーはクラスローダー階層の中間にあり、ブートストラップクラスローダーの直接の子であり、JREのlib \ extディレクトリにクラスをロードします。

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

リンク

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

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

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

Resolution−すべてのシンボリックメモリ参照は元の参照に置き換えられます。これを実現するために、クラスのメソッド領域の実行時定数メモリ内のシンボルテーブルが使用されます。

初期化

これは、クラスのロードプロセスの最終フェーズです。静的変数には元の値が割り当てられ、静的ブロックが実行されます。

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

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

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

スタック

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

ヒープ

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

メソッドエリア

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

実行時定数プール

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

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

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

ガベージコレクション

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

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

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

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

コンパイルされた言語と解釈された言語

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

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

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

このような最適化の例を以下で調べます。

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

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

Javaはコンパイルまたはインタープリターされていますか?

Javaは中間点を見つけようとしました。JVMはjavacコンパイラと基盤となるハードウェアの間に位置するため、javac(またはその他のコンパイラ)コンパイラは、プラットフォーム固有のJVMによって理解されるバイトコードでJavaコードをコンパイルします。次に、JVMは、コードの実行時にJIT(ジャストインタイム)コンパイルを使用してバイトコードをバイナリでコンパイルします。

ホットスポット

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

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

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

次のコードを考えてみましょう-

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

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

一方、実際に発生するのは、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コンパイラによって実行されるいくつかの一般的な最適化です-

  • メソッドのインライン化
  • デッドコードの除去
  • コールサイトを最適化するためのヒューリスティック
  • 定数畳み込み

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

  • Interpreter
  • 完全に最適化された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 and 64b。C2は64b

階層型コンパイル

Javaの古いバージョンでは、ユーザーは次のオプションのいずれかを選択できた可能性があります-

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

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

コンパイルの5つの層のうち、層状のコンパイルは 1 -> 4 -> 5

32bマシンでは、32bバージョンのJVMのみをインストールできます。64bマシンでは、ユーザーは32bバージョンと64bバージョンのどちらかを選択できます。ただし、これには、Javaアプリケーションのパフォーマンスに影響を与える可能性のある特定のニュアンスがあります。

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

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

この章では、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'メソッドがサブクラスでオーバーライドされ、メソッドを含むオブジェクトのタイプが実行時までわからない場合にどうなるかを考えてみてください。この場合、コンパイラはインライン化するメソッドを認識しません。ただし、メソッドが「final」としてマークされている場合、コンパイラーは、サブクラスによってオーバーライドできないため、インラインにできることを簡単に認識できます。最終的なメソッドが常にインライン化されるという保証はまったくないことに注意してください。

到達不能でデッドコードの除去

到達不能コードとは、実行フローによって到達できないコードです。次の例を考えてみましょう-

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

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

定数畳み込み

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

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

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の空き容量があった後でも発生します。

ほとんどのJVMは、ヒープを3世代に分割します- the young generation (YG), the old generation (OG) and permanent generation (also called tenured generation)。そのような考えの背後にある理由は何ですか?

経験的研究によると、作成されるオブジェクトのほとんどは非常に短い寿命を持っています-

ソース

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);
}

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

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

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

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

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

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

シリアルGC

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

スループットGC

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

スループットGCはYGとOGを収集します。エデンがいっぱいになると、コレクターは生きているオブジェクトをOGまたはサバイバースペースの1つ(下の図のSS0とSS1)に排出します。死んだオブジェクトは、それらが占めていたスペースを解放するために破棄されます。

YGのGC前

YGのGC後

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

CMSコレクター

'concurrentmark-sweep'の略です。その機能は、いくつかのバックグラウンドスレッドを使用して古い世代を定期的にスキャンし、死んだオブジェクトを取り除くことです。ただし、マイナーGCの間、アプリケーションスレッドは停止します。ただし、一時停止は非常に小さいです。これにより、CMSは一時停止の少ないコレクターになります。

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

“XX:+UseConcMarkSweepGC -XX:+UseParNewGC” CMSコレクターを使用するように指示するJVM引数として。

GC前

GC後

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

G1 GC

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

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

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

前の章では、さまざまな世代別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のサイズを大きくしすぎると、収集と圧縮に時間がかかりすぎて、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のサイズは上記の式を使用して計算され続けます。

パーマゲンとメタスペース

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

次のフラグを使用して、permagenのサイズを制御できます。 -XX: PermSize=N そして -XX:MaxPermSize=N。Metaspaceのサイズは、以下を使用して制御できます。-XX:Metaspace- Size=N そして -XX:MaxMetaspaceSize=N

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

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

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

この章では、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がトリガーされる前に開いたままになり、接続オブジェクトが到達不能であると見なされます。これで、接続オブジェクトのfinalメソッドが呼び出されますが、実装されていない可能性があります。したがって、このサイクルでオブジェクトがガベージコレクションされることはありません。

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