Works by

Ren's blog

@rennnosuke_rk 技術ブログです

【Java】JVMのヒープメモリ管理

JVMとヒープ

オブジェクト指向言語として有名なJavaプログラムがコンパイルされて中間言語としてJVM上で動く時、多くのオブジェクトの生成と破棄が繰り返され、それに伴ってヒープ領域の確保や生成が行われます。そのような激しいメモリアロケーションにも関わらず、今日のシステムの多くはJavaによって構成されています。今回はJVM上で行われるヒープメモリ上の管理、特にガーベジコレクタについて調べたので、紹介します。

ガーベジコレクタ

JavaScript実行モデルの記事でも触れたとおり、ガーベジコレクタはオブジェクトを扱う高級言語にとって現在なくてはならないプログラムです。ガーベジコレクション(以下GC)によって不要なオブジェクトの開放作業を自動的に行い、プログラマが煩雑なメモリ管理・開放作業に悩まされることなく、透過的にオブジェクトを使用することを助けてくれます。

上記記事では基本的な二種類のGCの方法について紹介しました。

  • Reference Pointer
  • Mark & Sweep

Reference Pointerはオブジェクトへの参照数をカウントし、その値が0になるオブジェクトを破棄の対象とするのでした。またMark & Sweepはrootオブジェクトから参照可能なオブジェクトを辿り、参照不可能なオブジェクトを破棄する方法を採っていました。Mark & Sweepは循環参照問題を回避し、非コレクション時はReference Pointerよりもパフォーマンスに優れる特徴を持ちます。一方で、Mark & Sweep はコレクション時のコストが大きいという欠点を持ち、積極的な「ゴミ集め」をしてしまうとアプリのパフォーマンスを大きく下げる原因となってしまいます。

また、メモリの確保と開放の繰り返しによって、ヒープは小さな空き領域が点在する穴だらけの状態になってしまいます。これでは大きなひとかたまりのオブジェクトが入る隙間がなくなってしまうので、デフラグメンテーション(以下デフラグ)を行いヒープを整頓する必要がありますが、デフラグ自体にもパフォーマンスコストを要します。

コピーGC

コピーGCフラグメンテーションの問題を解決します。このアルゴリズムでは同じ要領のメモリ領域を二つ用意するのですが、ヒープ領域として用いるのは片方の領域Aだけです。Aに対してGCを実施する場合、必要なオブジェクトだけを別の領域Bへ整列させながらコピーし、Aの領域をすべてクリアすることでGCを実現します。このアルゴリズムでは、GCデフラグを同時に行えるという利点がありますが、前述の通り用意したヒープ領域の半分しか使えなくなるというデメリットがあります。

一般に、この手法は他のアルゴリズムと組み合わせて使用します。例えばMark & Sweepと組み合わせて使う場合、SweepのタイミングでコピーGCによるデフラグの恩恵をうけることができます(Mark & Copyと呼ばれるそうです)。

世代別GC

Mark & Sweep アルゴリズムGCとして採用するのであれば、コレクションを実施するタイミングについて慎重にならなければなりません。

世代別GCは、コレクションすべきオブジェクトを分別してそのタイミングを調整してくれる、より賢いアルゴリズムです。「多くのGCにさらされながらも生き残っているオブジェクトは、以降も参照される率が高い」というコンセプトに基づいて、オブジェクトの管理を行います。

このアルゴリズムでは、ヒープ領域を第一世代第二世代の二つの領域に分割します。

f:id:rennnosukesann:20180328002551p:plain:w400

第一世代では、高速なコピーGC(後述)を頻繁に行っていきます。こちらの領域は小さく、一時オブジェクトは即座に回収されます。

f:id:rennnosukesann:20180328002946p:plain:w400

第二世代では基本的にGCが行われず、システム全体でメモリ不足が発生して初めてMark & SweepによってGCが行われます。

f:id:rennnosukesann:20180328003241p:plain:w400

最後に重要なルールですが、第一世代で一定回数のGCを生き残ったオブジェクトは、第二世代に移動するという決まりがあります。

f:id:rennnosukesann:20180328003923p:plain:w400

この仕組によって、一時オブジェクトのようなすぐに使われなくなるオブジェクトは第一世代ですぐに破棄され、暫く参照されているオブジェクトは第二世代で生き延びます。第二世代ではMark & SweepによるGCが実施されますが、頻度は少ないです。

世代別GCはバランスに優れたGCとして認知されており、多くのオブジェクト指向でサポートされています。Javaのヒープ領域の管理にも、世代別GCが採用されています。

JVMにおけるGC

現在のJava(10がリリースになりました!)では厳密にこの通りかどうか不明ですが、Javaも典型的な世代別GCの仕組みを採用しています。

JVMの世代別GC

ヒープはNew領域とOld領域に別れ、それぞれが第一世代と第二世代の領域に該当します。New領域はさらに「Eden」「From」「To」の三領域に分割されます。一方でJVMにロードされたクラス情報を格納する領域も用意されているようなのですが、ここでは割愛します(Pernament領域と呼ばれるそうです)。

f:id:rennnosukesann:20180328013552p:plain:w500

新しいオブジェクトが生成された時、そのオブジェクトはEden領域に格納されます。Eden領域がいっぱいになると、To領域へのコピーGCが走ります。このとき、コピーに成功した被参照オブジェクトはデフラグされた状態でTo領域に格納されます。このタイミングでEden領域はクリアされますが、不要なオブジェクトのみが残っている状態なので問題ありません。

f:id:rennnosukesann:20180328014313p:plain:w500

f:id:rennnosukesann:20180328014458p:plain:w500

To領域にオブジェクトが存在する状態で再びEden領域がいっぱいになると、同じようにTo領域へのコピーGCが行われます。このとき、元々To領域に存在したデータはどうなるのでしょうか。実は、To領域とFrom領域がこのタイミングで入れ替わり、入れ替わったあとのFrom領域とEden領域からTo領域へとコピーが開始されます。この繰り返しにより、To領域にはGCの対象外となったオブジェクトが蓄積されていきます。

f:id:rennnosukesann:20180328015120p:plain:w500

OLD領域への移動

To領域に蓄積されていくオブジェクトは、いずれOLD領域へ移さなければいけません。そのため、To領域に存在するオブジェクトには、コピーGCによってTo領域に移動した回数が割り当てられています。回数が閾値を上回るオブジェクトが発生した場合、OLD領域への移動が開始されます。

まとめ

簡単にですが、GC手法のおさらいと新しい手法、そしてJVMにおけるメモリ管理の概要について紹介しました。C/C++のような言語でなければメモリアロケーションは普段意識しにくいところですが、例えばサーバミドルウェアの管理などでメモリ障害が起きたときなどに対応できるよう、基本的な動作だけでも理解しておきたいところです。

参考文献

参照カウント - Wikipedia

マーク・アンド・スイープ - Wikipedia

世代別ガベージコレクション - Wikipedia

Javaのヒープ・メモリ管理の仕組み:Javaパフォーマンスチューニング(3) - @IT