javaメモリの割り当てと回収戦略を深く理解する


一、導論
java技術体系で言及されているメモリ自動化管理は、所詮メモリの割り当てと回収の二つの問題であり、以前はすでにjavaの回収に関する知識についてお話ししました。一般的には、対象メモリの割り当てはヒープ上の割り当てであり、対象は主に新生代のEdenに割り当てられています(対象はメモリ上の世代についてはゴミ回収中に補っています。知りたいのは「java仮想マシンを深く理解する」を参照してください。)。ローカルスレッドの割り当てバッファが起動されたら、スレッド別にTLABに優先的に割り当てられます。少数の場合も直接に古い年代に配分します。
二、経典の配分策略
1、対象は優先的にEdenに割り付ける
一般にオブジェクトは優先的にEdenに割り当てられ、Edenが十分な空間で割り当てられていない場合、jvmはMinor GCを開始する。もしまだ十分な空間配分がないなら、後にまた別の措置があります。
仮想マシンのログパラメータを設定します。XX:+PrintGCDetailsは、ゴミ回収時にメモリの回収ログを印刷し、プロセス終了時に現在のメモリの各エリアの割り当て状況を出力します。具体的な例を見ると、まずjvmのパラメータ-Xms 20 m-Xmx 20 m-Xmn 10 mを設定する必要があります。これらの3つのパラメータは、javaスタックのサイズが20 Mであり、拡張できません。10 Mは新生代に割り当てられ、残りの10 Mは古い世代に割り当てられます。XX:SurvivorRatio=8はjvmデフォルトの新生代EdenとSurvivorの割合で、デフォルトは8:1です。新生代の対象の98%が次のGCの時に回収されるため、複製アルゴリズムを使ってゴミ回収に適しているため、新生代10 Mのメモリの中で、8 MはEden、1 MはSurvivor、もう1 Mはコピーアルゴリズムを使用していないメモリブロックであり、Survivorでもあります。

public class ReflectTest {

  private static final int _1MB = 1024*1024;
  
  public static void testAllocation(){
    byte[] allocation1 , allocation2 , allocation3 , allocation4;
    allocation1 = new byte[2 * _1MB];
    allocation2 = new byte[2 * _1MB];
    allocation3 = new byte[2 * _1MB];
    allocation4 = new byte[6 * _1MB];
  }
  
  public static void main(String[] args) {
    ReflectTest.testAllocation();
  }
  
}
出力は以下の通りです

Heap
 PSYoungGen   total 9216K, used 6651K [0x000000000b520000, 0x000000000bf20000, 0x000000000bf20000)
 eden space 8192K, 81% used [0x000000000b520000,0x000000000bb9ef28,0x000000000bd20000)
 from space 1024K, 0% used [0x000000000be20000,0x000000000be20000,0x000000000bf20000)
 to  space 1024K, 0% used [0x000000000bd20000,0x000000000bd20000,0x000000000be20000)
 PSOldGen    total 10240K, used 6144K [0x000000000ab20000, 0x000000000b520000, 0x000000000b520000)
 object space 10240K, 60% used [0x000000000ab20000,0x000000000b120018,0x000000000b520000)
 PSPermGen    total 21248K, used 2973K [0x0000000005720000, 0x0000000006be0000, 0x000000000ab20000)
 object space 21248K, 13% used [0x0000000005720000,0x0000000005a07498,0x0000000006be0000)
edenは81%を占めています。allocation 1、allocation 2、allocation 3は新生代Edenに割り当てられています。
2、大きい対象は直接に古い年代に割り当てられます。
大きなオブジェクトとは、大量の連続メモリ空間を必要とするオブジェクトで、そのような長い文字列と配列に類似しています。大きなオブジェクトは仮想マシンのメモリ分布にとっては良いことではなく、1ラウンドだけ生きている多くのオブジェクトjvmが扱いにくいので、コードを書くときはこのような問題を避けるべきです。仮想マシンでは-XX:PretenseSizeThresoldパラメータを提供しています。また、この値より大きいオブジェクトは古い年代に直接割り当てられています。これはEdenエリアとSurvivorエリアの間で大量のメモリcopyが発生しないようにするためです。前に言ったゴミ回収アルゴリズムのコピーアルゴリズムが提案されています。

public class ReflectTestBig {

  private static final int _1MB = 1024*1024;
  
  public static void testAllocation(){
    byte[] allocation2 , allocation3 , allocation4;
    allocation2 = new byte[2 * _1MB];
    allocation3 = new byte[2 * _1MB];
    allocation4 = new byte[6 * _1MB];
  }
  
  public static void main(String[] args) {
    ReflectTestBig.testAllocation();
  }
  
}
出力は以下の通りです

Heap
 PSYoungGen   total 8960K, used 4597K [0x000000000b510000, 0x000000000bf10000, 0x000000000bf10000)
 eden space 7680K, 59% used [0x000000000b510000,0x000000000b98d458,0x000000000bc90000)
 from space 1280K, 0% used [0x000000000bdd0000,0x000000000bdd0000,0x000000000bf10000)
 to  space 1280K, 0% used [0x000000000bc90000,0x000000000bc90000,0x000000000bdd0000)
 PSOldGen    total 10240K, used 6144K [0x000000000ab10000, 0x000000000b510000, 0x000000000b510000)
 object space 10240K, 60% used [0x000000000ab10000,0x000000000b110018,0x000000000b510000)
 PSPermGen    total 21248K, used 2973K [0x0000000005710000, 0x0000000006bd0000, 0x000000000ab10000)
 object space 21248K, 13% used [0x0000000005710000,0x00000000059f7460,0x0000000006bd0000)
allocation 4はすでに設定されている-XX:PrtentureSizeTheshold=3145728を超えています。適当なallocation 4は直接に古い年代に割り当てられています。古い年代の占有率は60%です。ここに-XXをセットしてください。PrtentureSizeTheshold=3145728は-XX:Prtensure SizeThrehold=3 mと書くことができません。そうしないと、jvmは識別できません。
3、長期的な生存の対象は古い時代に入ります。
仮想マシンでは、メモリを管理するために収集されたアイデアを使用して、メモリのリサイクルは、どのオブジェクトが新しい世代に置かれるべきかを識別する必要があります。目的を達成するために、jvmは各オブジェクトに年齢カウンタ(Age)を定義します。オブジェクトがEdenで生まれ、初めてMinor GCを通過した後も生存し、Survivorに預けられると、Survivorに移動し、オブジェクトの年齢を1に設定します。オブジェクトはMinor GCを避けるたびに年齢が1つずつ加算されます。彼の年齢が1年の閾値を超えると、その対象は古い年代に昇進します。このしきい値jvmはデフォルトでは15です。-XX:MaxTenuringThreholdで設定できます。

public class JavaTest { 
 
  static int m = 1024 * 1024; 
 
  public static void main(String[] args) { 
    byte[] a1 = new byte[1 * m / 4]; 

     byte[] a2 = new byte[7 * m]; 

     byte[] a3 = new byte[3 * m]; //GC 
  } 
}
出力は以下の通りです

[GC [DefNew: 7767K->403K(9216K), 0.0062209 secs] 7767K->7571K(19456K), 0.0062482 secs]  
[Times: user=0.00 sys=0.00, real=0.01 secs]  
a3 ok 
Heap 
 def new generation  total 9216K, used 3639K [0x331d0000, 0x33bd0000, 0x33bd0000) 
 eden space 8192K, 39% used [0x331d0000, 0x334f9040, 0x339d0000) 
 from space 1024K, 39% used [0x33ad0000, 0x33b34de8, 0x33bd0000) 
 to  space 1024K,  0% used [0x339d0000, 0x339d0000, 0x33ad0000) 
 tenured generation  total 10240K, used 7168K [0x33bd0000, 0x345d0000, 0x345d0000) 
  the space 10240K, 70% used [0x33bd0000, 0x342d0010, 0x342d0200, 0x345d0000) 
 compacting perm gen total 12288K, used 381K [0x345d0000, 0x351d0000, 0x385d0000) 
  the space 12288K,  3% used [0x345d0000, 0x3462f548, 0x3462f600, 0x351d0000) 
  ro space 10240K, 55% used [0x385d0000, 0x38b51140, 0x38b51200, 0x38fd0000) 
  rw space 12288K, 55% used [0x38fd0000, 0x396744c8, 0x39674600, 0x39bd0000)
a 2はもう一回生存しました。年齢は1で、設定されたXX:MaxTenuringThrehold=1を満たしています。だからa 2は古い時代に入りました。a 3は新生代に入りました。
4、動態対象年齢判定
各プログラムのメモリ状態に適応するためには、仮想マシンは常にオブジェクトの年齢を必要としない-XX:MaxTenuringThresoldに設定された値を古い年代に昇格させることができます。Survivor空間では、同じ年齢のオブジェクトサイズの合計がSurvivor空間の半分以上であれば、年齢がその年齢のオブジェクトより直接に入ることができます。-XX:MaxTenuringThreholdの設定値に達する必要はありません。
5、空間配分担保
Minor GCが発生した場合、仮想機会は、古い年代の平均サイズに昇進するたびに、古い年代の残りの空間より大きいかどうかを検出し、それより大きい場合は直接にFull GCを行う。もし小さいなら、HandlerPromotionFailyreの設定が担保失敗を許可するかどうかを確認します。許可されればMinor GCだけ行います。許可されないなら、Full GCも改善します。つまり、新生代のEdenは、オブジェクトを変えられない時は、そのオブジェクトを古い時代に預けます。
三、よく使うjvmパラメータの設定
1、-Xms:初期ヒープの大きさは、デフォルト(MinHeapheeRatioパラメータは調整できます)空きヒープのメモリが40%未満の場合、JVMは-Xmxの最大制限まで増加します。
2、Xmx:最大ヒープサイズ、デフォルト(MaxHeapperRatioパラメータは調整できます)空きヒープメモリが70%以上の場合、JVMは-Xmsの最小制限までヒープを減らします。
3、-Xmn:若い世代のサイズ(1.4 or lator)ここのサイズは(eden+2 survivor space)です。jmap-heapに表示されているNew genとは違います。
全体の大きさ=若い世代の大きさ+若い世代の大きさ+長い世代の大きさ。
若い世代を大きくすると、若い世代のサイズが小さくなります。この値はシステムの性能に大きな影響を与えます。Sun公式の推奨配置は全体の3/8です。
4、-XX:NewSize:若い世代のサイズを設定します(for 1.3/1.4)。
5、-XX:MaxNewSize:若い世代最大値(for 1.3/1.4)。
6、-XX:PermSize:耐久代(perm gen)初期値を設定します。
7、-XX:MaxPermSize:耐久最大値を設定します。
8、-Xss:各スレッドのスタックサイズは、JDK 5.0以降の各スレッドスタックのサイズは1 Mであり、以前は各スレッドスタックのサイズは256 Kであった。アプリケーションのスレッドに必要なメモリサイズを調整した。同じ物理メモリでは、この値を小さくすると、より多くのスレッドが生成される。しかし、オペレーティングシステムは、プロセス内のスレッド数に制限があり、無限に生成できない。経験値は3000~5000ぐらいです。
9、-XX:NewRatio:若い世代(Edenと二つのSurvivor区を含む)と古い世代の比率(持久代を除く)、-XX:NewRatio=4は若い世代と古い世代の比率が1:4で、若い世代が全体の倉庫の1/5を占めることを表します。Xms=XmxでXmnが設定されている場合、このパラメータは設定する必要がありません。
10、-XX:SurvivorRatio:EdenエリアとSurvivorエリアのサイズ比は8に設定されています。二つのSurvivorエリアと一つのEdenエリアの比率は2:8で、一つのSurvivorエリアは若い世代の1/10を占めています。
11、-XX:LargePageSize InByttes:メモリページのサイズは大きすぎて設定できません。Permのサイズに影響します。
12、-XX:+Dispable ExplicitGC:System.gc()をクローズします。
13、-XX:MaxTenuringThresold:ごみの最大年齢は0に設定すれば、若い世代の対象はSurvivor区を経ずに、直接老年世代に入ります。若い世代の多くの応用に対して、効率を高めます。この値を大きな値に設定すれば、若い世代の対象はSurvivor区で何度もコピーされます。このように、対象の若い世代の生存時間を増やします。若い世代で回収される確率を増やすには、シリアルGCでのみ有効です。
14、-XX:PretentureSizeThrehold:対象がどれぐらい大きいかは直接旧世代に割り当てられ、単位バイトの新生世代はParalel Scaavenge GCを採用した時、もう一つの直接的な旧世代に割り当てられた場合は大きな配列オブジェクトであり、配列中に外部参照オブジェクトがない。
15、-XX:TLABWasteTarget Percent:TLABはedenエリアのパーセンテージを占めています。
四、補充
Minor GCとFull GCの違い:
新生代GC(Minor GC):新生代に発生したごみ収集動作を指します。javaの対象は対数が大きいので、第1ラウンドのGCに逃げられないです。だから、Minor GCは頻繁に使われています。一般的な回収速度も比較的速いです。
旧世代GC(FULL GC/Majer GC):旧世代のGCで、Majer GCが出現し、少なくとも一回のMinor GCを伴うことが多い(ただし、絶対ではなく、ParalelScanceコレクタの収集戦略の中でMajor GCを直接選択する過程がある)。Major GCの速度は一般的にMinor GCより10倍以上遅くなります。
以上の記事はjavaメモリの割り当てと回収戦略を深く理解しました。つまり、小編集は皆さんに全部の内容を共有しました。