Java 8メモリモデル

6969 ワード

一、JVMメモリモデル
メモリ空間(Runtime Data Area)では、スレッド共有が2つに分かれていますが、スレッド共有は、メソッドエリア(Method Area)とヒープ(Heap)、スレッド独自に楽しむのはJava仮想マシンスタック(Java Stock)、ローカル方法スタック(Native Method Stock)とPCレジスタ(Program Counter Register)です。具体的には下図を参照してください。
1.仮想マシンスタック:
各スレッドにはプライベートスタックがあり、スレッドの作成に伴って作成されます。スタックには「スタックフレーム」というものが格納されており、各方法は、ローカル変数テーブル(基本データタイプとオブジェクト参照)、オペランド、方法出口などの情報を格納するスタックフレームを作成する。スタックのサイズは固定可能であり、また動的に拡張可能である。スタックの呼び出し深さがJVMの許容範囲より大きい場合、StockOverflowErrerのエラーを投げますが、この深さ範囲は一定の値ではないので、次の手順でこの結果をテストします。
//        

package com.paddx.test.memory;

/**
 * Created by root on 2/28/17.
 */
public class StackErrorMock {
    private static int index = 1;

    public void call() {
        index++;
        call();
    }

    public static void main(String[] args) {
        StackErrorMock mock = new StackErrorMock();
        try {
            mock.call();
        } catch(Throwable e) {
            System.out.println("Stack deep: " + index);
            e.printStackTrace();
        }
    }
}
3回の運転で、スタックの深さはそれぞれ違っていることが分かります。出力結果は以下の通りです。
三枚の結果図を見ると、毎回のStock deep値が違います。その原因を追究して、JVMのソースコードの中に深く入り込んでようやく探求することができなければならなくて、ここは贅を要しません。
仮想マシンスタックには上記のエラー以外にもう一つのエラーがあります。それは空間を申請できない時にOutOfMemoryErrorを投げます。ここでは小さな注意が必要です。catchはExceptionではなく、Throwableをキャプチャしています。これはStockOverflowErrorとOutOfMemoryErrorはすべてExceptionのサブクラスに属していないからです。
2.ローカル方法スタック:
この部分は主に仮想マシンで使うNative方法と関連していますが、一般的にJavaアプリケーションプログラマはこの部分に関係する必要がありません。
3.PCレジスタ:
PCレジスタは、プログラムカウンタとも呼ばれる。JVMは複数のスレッドが同時に動作し、各スレッドには独自のプログラムカウンタがあります。現在実行されているのがJVM方法である場合、レジスタには現在実行されている命令のアドレスが保存される。native方法が実行されると、PCレジスタは空となります。
4.ヒープ:
ヒープメモリはJVMのすべてのスレッドが共有されている部分で、仮想マシンが起動する時に作成されました。すべてのオブジェクトと配列は山の上で割り当てられます。この部分のスペースはGCで回収できます。空間を申請できない時、OutOfMemoryErrを投げます。次のように簡単にメモリが溢れている状況をシミュレーションします。
package com.paddx.test.memory;

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

/**
 * Created by root on 2/28/17.
 */
public class HeapOomMock {
    public static void main(String[] args) {
        List list = new ArrayList();
        int i = 0;
        boolean flag = true;
        while(flag) {
            try {
                i++;
                list.add(new byte[1024 * 1024]); //     1M       
            }catch(Throwable e) {
                e.printStackTrace();
                flag = false;
                System.out.println("Count = " + i); //        
            }
        }
    }
}
まず実行時の仮想マシンの起動パラメータを設定します。
コードを実行し、出力結果は以下の通りです。
なお、ここではメモリのサイズを16 Mと指定していますので、ここに表示されているCount=13(この数字は固定されていません)は、なぜ13または他の数字なのかはGCログで判断する必要があります。
5.方法エリア:
メソッドエリアもすべてのスレッドが共有されています。主にクラスの情報、定数、メソッドデータ、メソッドコードなどを保存するために使用されます。メソッドエリアの論理上はヒープの一部ですが、ヒープと区別するために、通常は「非ヒープ」とも呼ばれます。メソッドエリアのメモリオーバーフローについては以下で詳細に議論します。
二、PermGen(永久代)
ほとんどのJavaプログラマは「java.lang.OutOfMemoryErr:PremGen space」の異常を見たはずです。ここの「PermGen space」とは、実は方法エリアのことです。しかし、方法エリアと「PermGen space」はまた本質的な違いがあります。前者はJVMの仕様であり、後者はJVM仕様の一つの実現であり、HotSpotだけが「PermGen space」であり、他のタイプの仮想マシンにはJRockit(Oracle)、J 9(IBM)は「PermGen space」ではない。メソッドエリアは主にクラスの関連情報を格納しているので、ダイナミックジェネレーションクラスの場合は永続的なメモリオーバーが発生しやすいです。最も典型的なシーンは、JSPページが多い場合、永久メモリオーバーフローが発生しやすくなります。私たちは現在、動的生成クラスを通じて「PermGen space」のメモリオーバーフローをシミュレーションしています。
package com.paddx.test.memory;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

public class PermGenOomMock {
	public static void main(String[] args) {
		URL url = null;
		List classLoaderList = new ArrayList();
		try {
			url = new File("/tmp").toURI().toURL();
			URL[] urls = {url};
			while(true) {
				ClassLoader loader = new URLClassLoader(urls);
				classLoaderList.add(loader);
				loader.loadClass("com.paddx.test.memory.Test");
			}
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
}
package com.paddx.test.memory;

public class Test {}
運転結果は以下の通りです。
本例で使用されるJDKバージョンは1.7で、指定されたPermGenエリアのサイズは8 Mです。毎回異なるURLClassLoaderオブジェクトを生成してTestクラスをロードすることによって、異なる種類のオブジェクトを生成することで、私たちがよく知っている「java.lang.OutOfMemoryErr:PermGen space」が見られます。ここでJDK 1.7を採用したのは、JDK 1.8において、HotSpotには既に「PermGen space」という区間がないからです。代わりに、Metaspaceというものがあります。次に、MetaspaceとPermGen spaceの違いを見てみましょう。
三、メタスペース(元空間)
実は、恒久代除去の作業はJDK 1.7から始まりました。JDK 1.7では、恒久的に記憶されている部分のデータは、Java HeapまたはNative Heapに転送されました。しかし、永久世代はJDK 1.7に存在し、完全に除去されていません。例えば、記号参照がnative heapに移行しました。文字の量はJava heapに移行しました。クラスの静的変数がJava heapに移行しました。JDK 1.6、JDK 1.7とJDK 1.8の違いを、あるプログラムで比較できます。文字列の定数を例にします。
package com.paddx.test.memory;

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

public class StringOomMock {
	static String base = "string";
	public static void main(String[] args) {
		List list = new ArrayList();
		for (int i = 0; i < Integer.MAX_VALUE; i++) {
			String str = base + base;
			base = str;
			list.add(str.intern());
		}
	}
}
このプログラムは2の指数レベルで連続的に新しい文字列を生成しています。これにより、メモリの消費が速くなります。私たちはJDK 1.6、JDK 1.7、JDK 1.8を通してそれぞれ運行します。
JDK 1.6の運転結果:
JDK 1.7の運転結果:
JDK 1.8の運転結果:
上記の結果から、JDK 1.6では「PermGen space」のメモリオーバーフローが発生しますが、JDK 1.7とJDK 1.8では、ヒープメモリオーバーが発生し、JDK 1.8ではパラメータPermSizeとMaxPermSizeは失効しました。したがって、JDK 1.7とJDK 1.8において文字列の定数を永久代数でスタックに移行させ、JDK 1.8には永久世代が存在しないという結論を大まかに検証することができる。今私達は元の空間を見に来ました。一体何ですか?
JDK 1.8はJVMアーキテクチャを改造して、クラスのメタデータをローカルメモリに入れます。また、定数プールと静的変数をJavaスタックに入れます。HotSpot VMは、クラスのメタデータにローカルメモリを明確に割り当て、リリースします。このようなアーキテクチャでは、元の-XX:MaxPermSizeの制限を突破し、現在はより多くのローカルメモリを使用することができます。このように、運転時に反射、代理などを使用して、常にフルGCを発生させる大量の種類をある程度解決しました。だからアップグレードしたらJavaヒープの空間が増えるかもしれません。
元空間の本質は永久世代と類似しており、いずれもJVM仕様における方法領域の実現である。しかし、元空間と永久世代の最大の違いは、元空間は仮想マシンではなく、ローカルメモリを使用することである。したがって、デフォルトの場合、元空間の大きさはローカルメモリに限定されますが、以下のパラメータで元空間の大きさを指定できます。
-XX:Metaspace Sizeでは、初期のスペースの大きさはこの値に達するとごみ収集をトリガしてタイプアンインストールを行います。同時にGCは修正値を調整します。大量の空間を開放したら、その値を適当に下げます。少しの空間が解放されたら、MaxMetaspaceSizeを超えないうちに、この値を適切に高めます。
-XX:MaxMetaspaceSize、最大スペース、デフォルトは制限なしです。
上の二つの指定サイズのオプションのほかに、GCに関する二つの属性があります。
-XX:MinMetaspace Free Ratioは、GCの後、最小のMetaspaceの空き容量の割合を減らし、分配空間によるゴミ収集を減少させます。
-XX:MaxMetaspace FreeRatioは、GCの後、最大のMetaspaceの空き容量のパーセンテージを減らし、放出空間によるゴミ収集を減らします。
今はJDK 1.8で上の第二部分のコードを再起動しますが、今回はPermSizeとMaxPermSizeを指定しません。Metaspace SizeとMaxMetaspaceSizeのサイズを指定します。出力結果は以下の通りです。
アウトプットの結果から、今回は永久的なオーバーフローではなく、元空間のオーバーフローであることが分かります。
四、まとめ
上の分析を通して、JVMのメモリ区分を大体理解したはずです。JDK 1.8の中で永久的なメタ空間への転換も分かりました。しかし、なぜこの転換をするのかという疑問があります。いくつかの原因をまとめます。
  • 文字列は永久世代において、性能問題とメモリオーバーフローが発生しやすい。
  • 類や方法の情報などは大きさを決めるのが難しいため、永久世代の大きさ指定は難しく、小さすぎると永久世代オーバーフローが出やすく、大きすぎると古い世代オーバーが出やすくなります。
  • 永久世代は、GCに必要以上の複雑さをもたらし、回収効率が低い。
  • OracleはHotSpotとJRockitを一つにするかもしれません。
  • 五、参考:
    1.JVMメモリモデル——パーペチュアルとメタスペース(Metaspace)(http://www.cnblogs.com/paddix/p/5309550.html)
    2.阿里電面の試験問題のまとめ(http://www.deanwangpro.com/2017/01/31/ali-interview/)