はじめまして!バックエンドエンジニアのnnhkrnkと申します!
先日組み込み系エンジニアの友人と話していた際、C言語ではメモリをユーザが意図的に開放しないとメモリリークが起きるということを初めて知りました。 自分は普段Javaを書いてますが、メモリを意識してコーディングをしたことはほとんどありません。どのような仕組みになっているのか気になったため、Javaにおけるメモリ解放について調べてみました!
メモリ解放は誰がやっているのか
CやC++ではユーザが意図的にメモリ管理をする必要がありました。JavaではJVMがその役割を担っています。メモリの使用や解放を自動で行ってくれるため、ユーザがメモリを意識せずにコーディングできるというわけです。
メモリ解放の仕組み
メモリ解放はJVMのガベージコレクション(GC)という機能で行われています。 ガベージコレクションの処理を知る前に、JVMが管理するメモリ領域について整理していきましょう。
4種類のメモリ領域
Javaプログラムが実行されると、Javaのプロセスにメモリが割り与えられます。そのメモリ領域は以下の4つの領域に分けられます。
- ヒープ領域
- スタック領域
- メタスペース
- ネイティブメモリ(※JVMではなくOSが管理する領域)
領域 | 用途・特徴 |
---|---|
ヒープ領域 | 生成されたインスタンスの情報が保存される領域。 |
スタック領域 | 実行中のプログラムの情報が保存される領域。 実行中の行が「どこから呼び出されていて、どんな情報を参照可能か」などを持っているイメージ。 |
メタスペース | クラス定義、メソッドコード、定数プールなどのクラス関連の情報が保存される領域。 |
ネイティブメモリ | Javaプログラムがネイティブコード(C、C++などで記述されたコード)を呼び出す際に使用される領域。 JVMではなくOSが直接管理をしている。 |
このうち、プログラム実行中にメモリ管理が必要になるのはヒープ領域になります。 ガベージコレクションでは、このヒープ領域を対象としてメモリの管理を行っています。
ヒープ領域の詳細
ヒープ領域は大きく分けて以下の2つの領域で構成されています。
- Young領域
- Old領域
領域 | 特徴 |
---|---|
Young領域 | メモリに格納されてからの時間が比較的短いデータが格納されている領域。 新しく生成されたオブジェクトが一時的に配置される。 |
Old領域 | メモリに格納されてからの時間が比較的長いデータが格納されている領域。 Young領域からのオブジェクトが長寿命のものとして移動する。 |
Young領域にはさらにEden
とSurvivor
の2つの領域に分けられます。
Survivor
は領域が2つありますが、どちらも役割は同じです。
ガベージコレクション
ガベージコレクションの処理の流れを理解する前に、ガベージコレクションには主に以下の2種類があるということを押さえておきましょう。
ガベージコレクションの種類 | 対象領域 | トリガー条件 |
---|---|---|
マイナーGC | Young領域(Eden領域、Survivor領域) | Eden領域がいっぱいになった場合 |
フルGC | ヒープ領域全体(Young領域とTenured領域) | Tenured領域がいっぱいになった場合 |
マイナーGC
マイナーGCは以下のルールに沿ってメモリ解放を行います。 * Eden領域のデータ * データが不要(参照されてない)の場合、データを削除してメモリを解放する * データがまだ必要な場合、データをSurvivor領域に移動させてメモリを解放する * Survivor領域のデータ * データが不要の場合、データを削除してメモリを解放する * データがまだ必要な場合、データをもう片方のSurvivor領域かTenured領域に移動させてメモリを解放する
具体的に図で見ていきましょう。 インスタンスが生成される、そのデータはEden領域に格納されます。 これが繰り返されるとEden領域がいっぱいになり、いずれはメモリが足りなくなってしまいます。 その際に実行されるのがマイナーGCです。
実行開始後、Eden領域のデータがまだ参照されているかどうかを確認します。参照されてない場合は削除、参照されている場合はSurvivor領域に移動させることでメモリ解放を行います。
同じタイミングでSurvivor領域のメモリ解放も行います。参照されてない場合は削除することでメモリ解放を行い、まだ参照されている場合はもう片方のSurvivor領域に移動することでメモリを解放します。
そのため、長い間解放されないデータは2つのSurvivor領域を行き来することになります。この回数には上限があり、上限を超えたデータはTenured領域に移動されるというわけです。
フルGC
マイナーGCを繰り返していくと、必要なデータが多く残っている場合にTenured領域がどんどん埋まっていってしまいます。 これを繰り返すと、マイナーGC実行時にTenured領域に空きがないために必要なデータを移動できないという事態が発生し、マイナーGC自体が失敗してしまいます。 その失敗を契機として実行されるのがフルGCです。
Full GCでは、YoungとOldの両方に対してメモリ解放を行います。Young領域に対してはマイナーGCと同じルールで実施し、Oldについては不要になったメモリを解放するというシンプルなルールです。
おわりに
今回はJavaのメモリ解放について記載しました!
自分が普段メモリ解放を意識せずにプログラミングできているのはこのような便利な機能のおかげなんですね!勉強になりました!
それではまたの記事でお会いしましょう!
参考
以下の記事を参考にさせていただきました!ありがとうございました!
JavaのGCの仕組みを整理する
Javaのインスタンス変数とクラス変数(スタック領域とヒープ領域)
Javaの並行処理を理解する(入門編)
Java8以降のメモリ設定について
JVM のメモリ構造