ガベージコレクション

最近、JVM構成の問題についてクライアントと協力しています。 すべての開発者と管理者がガベージコレクションの仕組みとJVMがメモリを使用する方法を知っているわけではないという事実に見えます。 したがって、このトピックの概要を明確な例で紹介することにしました。 この投稿は、ガベージコレクションやJVMのセットアップに関するすべての知識を網羅するふりをしているわけではありません(巨大です)。そして、最終的に、これについてはすでにWeb上で多くの良いことが書かれています。



この投稿は、HotSpot JVM、Oracleの「通常の」JVM(以前のSun)、Windowsで使用する可能性が最も高いJVMに関するものです。 Linuxの場合は、オープンソースJVMである可能性があります。 JVMは、WebLogicなどの他のソフトウェアにバンドルされているか、OracleのJrockit JVM(以前のBEA)を使用することもできます。 または、IBM、Appleなどの他のJVM。これらの「他の」JVMのほとんどは、メモリ管理が他と異なり、たとえば専用の永続世代を持たないJrockitを除き、HotSpotと同様の方法で動作します(以下を参照)。



JVMがメモリを使用する方法から始めましょう。 JVMでは、メモリは2つのセグメント(ヒープと永続的な世代)に分割されます。 図では、Permanent Generationは緑色で示され、残りはヒープです。







永久世代





永続的な生成は、作成されたオブジェクトに関するメタデータを含む必要なデータを格納するためにJVMによってのみ使用されます。 JVMが作成されるたびに、一部のデータセットがPGに「配置」されます。 したがって、さまざまなタイプのオブジェクトを作成すればするほど、PGにはより多くの「リビングスペース」が必要になります。

PGサイズは、2つのJVMパラメーターで指定できます。- XX:PermSize -PGの最小または初期サイズを設定し、 -XX:MaxPermSize-最大サイズを設定します。 大きなJavaアプリケーションを起動するとき、これらのパラメーターに同じ値を設定することがよくあります。そのため、PGサイズを変更するとコストがかかる(時間のかかる)操作であるため、PGはすぐに「最大」サイズで作成され、生産性が向上します。 これらの2つのパラメーターに同じ値を定義すると、PGのサイズを変更する必要があるかどうかを確認するなど、追加の操作をJVMで実行する必要がなくなります。



ヒープ





ヒープは、すべてのオブジェクトが保存されるメインメモリセグメントです。 ヒープは、Old GenerationとNew Generationの2つのサブセグメントに分割されます。 次に、新世代は、EdenとSurvivorの2つのセグメントに分割されます。

ヒープサイズは、パラメータで指定することもできます。 図では、これらはXms(最小)および-Xmx(最大)です。 追加のパラメーターは、ヒープセグメントのサイズを制御します。 そのうちの1つを後で確認し、残りはこの投稿の範囲外です。

オブジェクトを作成するときに、 byte [] data = new byte [1024]のようなものを書き込むと、このオブジェクトはEdenセグメントに作成されます。 Edenに新しいオブジェクトが作成されます。 バイト配列の実際のデータに加えて、このデータへのリンク(ポインター)もあります。

詳細な説明は簡略化されています。 新しいオブジェクトを作成したいが、Edenにスペースがない場合、JVMはガベージコレクションを実行します。つまり、JVMは不要になったすべてのオブジェクトをメモリ内で検索し、それらを削除します。



ガベージコレクションはクールです! CやObjective-Cなどの言語でプログラミングしたことがある場合、手動のメモリ管理は退屈な作業であり、エラーの原因になることもあります。 未使用のオブジェクトを自動的に処理するJVMを使用すると、開発が容易になり、デバッグ時間が短縮されます。 そのような言語で書いたことがない場合は、Cを使ってプログラムを書き、あなたの言語が完全に無料で提供することの価値を感じてください。



JVMがガベージコレクションを実行するために使用できる多くのアルゴリズムがあります。 パラメーターを使用して、JVMが使用するものを指定できます。

例を見てみましょう。 次のコードがあるとしましょう:



String a = "hello"; String b = "apple"; String c = "banana"; String d = "apricot"; String e = "pear"; // //  - // a = null; b = null; c = null; e = null;
      
      







図の5つの黄色い四角で示されるように、5つのオブジェクトがEdenに作成(配置)されます。 「何か」の後、a、b、c、eを解放し、リンクを無効にします。 それらへのリンクがもうないことを念頭に置いて、それらはもはや必要ではなく、2番目の図に赤で示されています。 ただし、ストリングd(緑色で表示)が引き続き必要です。







別のオブジェクトを配置しようとすると、JVMはEdenがいっぱいであり、クリーンアップする必要があることを検出します。 ガベージコレクションの最も単純なアルゴリズムはコピーコレクションと呼ばれ、図に示すように機能します。 最初の段階で、マークは未使用オブジェクト(赤)をマークします。 2番目の(コピー)では、まだ必要なオブジェクト(d)がサバイバーセグメント(右側の正方形)にコピーされます。 Survivorセグメントは2つあり、Edenよりも小さくなっています。 これで、保存したいすべてのオブジェクトがSurvivorにコピーされ、JVMはEdenからすべてを削除します。 それだけです

このアルゴリズムは、「世界が停止した瞬間」と呼ばれるものを作成します。 GCの実行中は、JVM内の他のすべてのスレッドが一時停止されるため、そこからすべてをコピーした後、メモリにアクセスしようとするスレッドはありません。 小規模なアプリケーションの場合、これは大きな問題ではありませんが、たとえば8ギガバイトのヒープを備えた深刻なプログラムがある場合、GCには多くの時間がかかります(数秒から数分)。 当然、毎回アプリケーションを停止することはオプションではありません。 したがって、他のアルゴリズムがあり、頻繁に使用されます。 ガベージが多く有用なオブジェクトがほとんどない場合も、コレクションのコピーはうまく機能します。

この投稿では、2つの一般的なアルゴリズムについて説明します。 興味のある人のために、ウェブ上の多くの情報といくつかの良い本があります。

次のアルゴリズムは、マークスイープコンパクトコレクションと呼ばれます。 アルゴリズムには3つの段階があります。



1)「マーク」:未使用のオブジェクト(赤)がマークされます。



2)「スイープ」:これらのオブジェクトはメモリから削除されます。 図の空のスロットに注意してください。



3)「コンパクト」:オブジェクトは空きスロットを占有して配置され、「大きな」オブジェクトを作成する必要がある場合にスペースを解放します。







しかし、これはすべて理論なので、例を使用して、それがどのように機能するかを見てみましょう。 幸いなことに、JDKにはJVMをリアルタイムで監視するための視覚的なツール、つまりjvisualvmがあります。 bin JDKにあります。 少し後で使用します。最初にアプリケーションを処理します。

開発、ビルド、および依存関係のために、私はMavenを使用しましたが、必要ありません。必要に応じて、アプリケーションをコンパイルして実行するだけです。

 mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.redstack -DartifactId=memoryTool
      
      







単純なJAR(98)を選択しましたが、残りはすべてデフォルトです。 次に、memoryToolディレクトリに切り替えてpom.xmlを編集しました(プラグインブロックを追加しました)。 これにより、Mavenからアプリケーションを直接実行し、必要なパラメーターを渡すことができました。



 <project xmlns = "http://maven.apache.org/POM/4.0.0" 
   xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion> 4.0.0 </ modelVersion>
   <groupId> com.redstack </ groupId>
   <artifactId> memoryTool </ artifactId>
   <バージョン> 1.0-SNAPSHOT </バージョン>
   <packaging> jar </ packaging>
   <name> memoryTool </ name>
   <url> http://maven.apache.org </ url>
   <プロパティ>
     <project.build.sourceEncoding> UTF-8 </project.build.sourceEncoding>
   </ properties>
   <ビルド>
     <プラグイン>
       <プラグイン>
         <artifactId> maven-compiler-plugin </ artifactId>
         <バージョン> 2.0.2 </バージョン>
         <構成>
           <source> 1.6 </ source>
           <target> 1.6 </ target>
         </構成>
       </ plugin>
 ---------------------
       <プラグイン>
         <groupId> org.codehaus.mojo </ groupId>
         <artifactId> exec-maven-plugin </ artifactId>
         <構成>
           <executable> java </ executable>
           <引数>
             <argument> -Xms512m </ argument>
             <argument> -Xmx512m </ argument>
             <argument> -XX:NewRatio = 3 </ argument>
             <argument> -XX:+ PrintGCTimeStamps </ argument>
             <argument> -XX:+ PrintGCDetails </ argument>
             <argument> -Xloggc:gc.log </ argument>
             <argument> -classpath </ argument>
             <クラスパス/>
             <argument> com.redstack.App </ argument>
           </ arguments>
         </構成>
       </ plugin>
 ----------------------
     </ plugins>
   </ build>
   <依存関係>
     <依存性>
       <groupId> junit </ groupId>
       <artifactId> junit </ artifactId>
       <バージョン> 3.8.1 </バージョン>
       <scope>テスト</ scope>
     </ dependency>
   </依存関係>
 </ project>


Mavenを使用しない場合は、次のコマンドでアプリケーションを起動できます。

 java -Xms512m -Xmx512m -XX:NewRatio=3 -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:gc.log -classpath <whatever> com.redstack.App
      
      





この場合:

-Xmsは、512 MBの初期/最小ヒープサイズを決定します

-Xmxは、512 MBの最大ヒープサイズを決定します

-XX:NewRatioは、古い世代のサイズが新しい世代のサイズの3倍であることを決定します

-XX:+ PrintGCTimeStamps、-XX:+ PrintGCDetailsおよび-Xloggc:gc.log JVMは、ガベージコレクションに関する追加情報をgc.logファイルに出力します

-classpathはクラスパスを定義します

com.redstack.Appメインクラス



以下は、メインクラスのコードです。 オブジェクトを作成してから破棄する単純なプログラムです。したがって、使用されているメモリ量が明確であり、JVMで何が起こるかを確認できます。

 package com.redstack; import java.io.*; import java.util.*; public class App { private static List objects = new ArrayList(); private static boolean cont = true; private static String input; private static BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); public static void main(String[] args) throws Exception { System.out.println("Welcome to Memory Tool!"); while (cont) { System.out.println( "\n\nI have " + objects.size() + " objects in use, about " + (objects.size() * 10) + " MB." + "\nWhat would you like me to do?\n" + "1. Create some objects\n" + "2. Remove some objects\n" + "0. Quit"); input = in.readLine(); if ((input != null) && (input.length() >= 1)) { if (input.startsWith("0")) cont = false; if (input.startsWith("1")) createObjects(); if (input.startsWith("2")) removeObjects(); } } System.out.println("Bye!"); } private static void createObjects() { System.out.println("Creating objects..."); for (int i = 0; i < 2; i++) { objects.add(new byte[10*1024*1024]); } } private static void removeObjects() { System.out.println("Removing objects..."); int start = objects.size() - 1; int end = start - 2; for (int i = start; ((i >= 0) && (i > end)); i--) { objects.remove(i); } } }
      
      







コードをビルドして実行するには、次のMavenコマンドを使用します。

mvn package exec:exec



コンパイルして次のアクションの準備ができたらすぐに、それとjvisualvmを実行します。 jvisualvmを使用したことがない場合は、VisualGCプラグインをインストールする必要があります。[ツール]メニューの[プラグイン]を選択し、[利用可能なプラグイン]タブを選択します。 Visual GCを選択し、[インストール]をクリックします。

JVMプロセスのリストが表示されます。 アプリケーションが実行されているもの(この例ではcom.redstack.App)をダブルクリックし、Visual GCタブを開きます。 下のスクリーンショットのようなものが表示されます。







永久世代、旧世代、エデン、および生存者セグメント(S0およびS1)の状態を視覚的に観察できることに注意してください。 色付きの列は使用済みメモリを示します。 右側は、JVMがガベージコレクションを実行した時期と各セグメントのメモリ量を示す履歴ビューです。



アプリケーションウィンドウで、オブジェクトの作成を開始し(オプション1)、Visual GCで何が起こるかを確認します。 新しいオブジェクトは常にedenで作成されることに注意してください。 次に、いくつかのオブジェクトを不要にします(オプション2)。 Visual GCで変更が表示されない場合があります。 これは、ガベージコレクション手順が完了するまで、JVMがこのスペースをクリーンアップしないためです。



ガベージコレクションを開始するには、Edenに入力してさらにオブジェクトを作成します。 充填時に何が起こるかに注意してください。 Edenに大量のゴミがある場合、Edenからオブジェクトがサバイバーに「移動」する方法がわかります。 ただし、Edenにごみがほとんどない場合は、古い世代でオブジェクトがどのように「移動」するかがわかります。 これは、残す必要があるオブジェクトが生存以上のものである場合に発生します。

永久世代の漸進的な増加も観察してください。

エデンを埋めようとしますが、最後までではなく、ほとんどすべてのオブジェクトを捨てて、20 mbだけ残します。 ほとんどの場合、エデンはゴミでいっぱいです。 その後、さらにオブジェクトを作成します。 今回は、EdenからのオブジェクトがSurvivorに移動していることがわかります。



次に、メモリが不足した場合に何が起こるかを見てみましょう。 460 mbにオブジェクトがなくなるまでオブジェクトを作成します。 エデンとオールドジェネレーションの両方がほぼ満杯であることに注意してください。 さらにいくつかのオブジェクトを作成します。 メモリーがなくなると、アプリケーションはOutOfMemoryException例外でクラッシュします。 あなたはすでにこの振る舞いを経験し、なぜこれが起こったのかを考えたかもしれません-特にあなたのコンピューターに大量の物理メモリがあり、どうしてこれが起こるのだろうと思ったら、十分なメモリがないと思います-今あなたはその理由を知っています。 Permanent Generationがいっぱいになった場合(この例の場合、これを達成するのはかなり困難です)、PermGenがいっぱいであることを通知する別の例外をスローします。



最後に、何が起こっていたかを確認する別の方法は、ログにアクセスすることです。 ここに私のものから少し:



 13.373:[GC 13.373:[ParNew:96871K-> 11646K(118016K)、0.1215535秒] 96871K-> 73088K(511232K)、0.1216535秒] [時間
 :ユーザー= 0.11 sys = 0.07、実数= 0.12秒]
 16.267:[GC 16.267:[ParNew:111290K-> 11461K(118016K)、0.1581621秒] 172732K-> 166597K(511232K)、0.1582428秒] [Ti
 mes:ユーザー= 0.16 sys = 0.08、実数= 0.16秒]
 19.177:[GC 19.177:[ParNew:107162K-> 10546K(118016K)、0.1494799秒] 262297K-> 257845K(511232K)、0.1495659秒] [Ti
 mes:ユーザー= 0.15 sys = 0.07、実数= 0.15秒]
 19.331:[GC [1 CMS-initial-mark:247299K(393216K)] 268085K(511232K)、0.0007000秒] [時間:ユーザー= 0.00 sys = 0.00、実数
 = 0.00秒]
 19.332:[CMS-concurrent-mark-start]
 19.355:[CMS-concurrent-mark:0.023 / 0.023秒] [時間:ユーザー= 0.01 sys = 0.01、実数= 0.02秒]
 19.355:[CMS-concurrent-preclean-start]
 19.356:[CMS-concurrent-preclean:0.001 / 0.001秒] [時間:ユーザー= 0.00 sys = 0.00、実数= 0.00秒]
 19.356:[CMS-concurrent-abortable-preclean-start]
  CMS:時間24.417による事前クリーニングの中止:[CMS-concurrent-abortable-preclean:0.050 / 5.061 secs] [時間:ユーザー= 0.10 sys =
 0.01、実数= 5.06秒]
 24.417:[GC [YG占有率:23579 K(118016 K)] 24.417:[再スキャン(並列)、0.0015049秒] 24.419:[弱いrefs processin
 g、0.0000064秒] [1 CMS-注釈:247299K(393216K)] 270878K(511232K)、0.0016149秒] [時間:ユーザー= 0.00 sys = 0.00、レア
 l = 0.00秒]
 24.419:[CMS-concurrent-sweep-start]
 24.420:[CMS-concurrent-sweep:0.001 / 0.001秒] [時間:ユーザー= 0.00 sys = 0.00、実数= 0.00秒]
 24.420:[CMS-concurrent-reset-start]
 24.422:[CMS同時リセット:0.002 / 0.002秒] [時間:ユーザー= 0.00 sys = 0.00、実数= 0.00秒]
 24.711:[GC [1 CMS-initial-mark:247298K(393216K)] 291358K(511232K)、0.0017944秒] [時間:ユーザー= 0.00 sys = 0.00、実数
 = 0.01秒]
 24.713:[CMS-concurrent-mark-start]
 24.755:[CMS-concurrent-mark:0.040 / 0.043秒] [時間:ユーザー= 0.08 sys = 0.00、実数= 0.04秒]
 24.755:[CMS-concurrent-preclean-start]
 24.756:[CMS-concurrent-preclean:0.001 / 0.001秒] [時間:ユーザー= 0.00 sys = 0.00、実数= 0.00秒]
 24.756:[CMS-concurrent-abortable-preclean-start]
 25.882:[GC 25.882:[ParNew:105499K-> 10319K(118016K)、0.1209086秒] 352798K-> 329314K(511232K)、0.1209842秒] [Ti
 mes:ユーザー= 0.12 sys = 0.06、実数= 0.12秒]
 26.711:[CMS-concurrent-abortable-preclean:0.018 / 1.955秒] [時間:ユーザー= 0.22 sys = 0.06、実数= 1.95秒]
 26.711:[GC [YG占有率:72983 K(118016 K)] 26.711:[再スキャン(並列)、0.0008802秒] 26.712:[弱いrefs processin
 g、0.0000046秒] [1 CMS-注釈:318994K(393216K)] 391978K(511232K)、0.0009480秒] [時間:ユーザー= 0.00 sys = 0.00、rea
 l = 0.01秒]



ログでは、JVMで何が起こったのかを確認できます。ConcurrentMark Sweep Compact Collectionアルゴリズム(CMS)が使用されたことに注意してください。ステージとYG-Young Generationの説明があります。



「プロダクション」でこれらの設定を使用できます。 ログを視覚化するツールもあります。



これで、JVMガベージコレクションの理論と実践について簡単に紹介しました。 この例のアプリケーションが、アプリケーションの実行時にJVMで何が起こるかを明確に理解するのに役立つことを願っています。

JVM設定とガベージコレクションについて教えてくれたRupesh Ramachandranに感謝します。






All Articles