不要なオブジェクトを回収するしくみ~ガベージコレクタ
前回の最後で触れたように、あるオブジェクトに対する参照をすべて削除すると、そのオブジェクトへはたどり着くことができなくなるため、プログラム中で使用できなくなります。このようなオブジェクトが増えていくと、二度と参照されることのないオブジェクトがヒープを占有してしまい、ヒープが枯渇してしまいます。
日常生活では、建物内にゴミが溜まってしまい、足の踏み場がなくなっても、週1~2回あるゴミの日にまとめて捨てれば部屋はきれいになります。しかし、不要なオブジェクトでヒープが占有されてしまったJVMは、どうすれば良いのでしょうか?
JVMでは、このような不要なオブジェクトを「ゴミ」として回収し、不要なオブジェクトが使用していたヒープを解放することで、ヒープが枯渇することを防ぎます。その仕組みが「ガベージコレクタ(GC)」です。
GCは、まずはじめにゴミとなるオブジェクトを見つけます。その後、そのオブジェクトを回収し、そのオブジェクトが使用していたヒープを解放します。
ゴミとして回収の対象となるオブジェクトの条件は、「オブジェクトに対する参照が存在しないこと」です。つまり、どの変数やオブジェクトからもそのオブジェクトにいっさいたどり着くことができない場合、そのオブジェクトはゴミとして回収の対象となるわけです。
ただし、不要なオブジェクトがGCの対象となっただけでは、メモリ空間は解放されません。その後の処理でオブジェクトが回収されて、そのオブジェクトが使用していたヒープが解放されます。
GCはJVMが自動的に実行してくれる
GCは、ヒープの残り使用量が少なくなるなどして、JVMがGCする必要があると判断されたタイミングで実行されます。このタイミングはプログラマが制御できず、JVMが自動的に実行しますが、プログラム中でSystem.gc()メソッドを実行することで、プログラマが任意に実行させることもできます。
ただし、最近のJVMは非常に賢いので、プログラマが任意のタイミングでGCを実行するのではなく、JVMで自動で実行させたほうが、無駄なGCを行わずに済むためオススメです。
C++など、これまでの多くのプログラミング言語では、アプリケーション開発者がメモリの割り当てや解放などを管理する必要がありました。
たとえばC++では、new演算子で使用したメモリ空間は、delete演算子で明示的に解放しなければ、メモリ空間を占有したままになります。delete演算子でメモリを解放し忘れて、参照を削除してしまった場合は、プログラム終了までメモリを占有したままとなります。
そのようにオブジェクトを明示的に解放する必要がないことがJavaの大きなメリットと言えます。
オブジェクトが生成されてからGCの対象となるまでの流れ
ここで、あるオブジェクトがGCの対象となるまでの流れを、Javaのソースコードとオブジェクト、参照、変数の関係から見てみましょう。
1行目のnew Object()が処理されると、ヒープ上にObjectオブジェクトが作成されます。そして、Objectクラスへの参照を格納できるaという名前の変数によって、このオブジェクトへ参照できるようにします。
2行目では、変数aで参照できるオブジェクトに、変数aと同様にbという名前の変数でも参照できるようにしています。この段階では、変数aと変数bの2つから、同じObjectクラスのオブジェクトへ参照しています。
3行目では、変数aにnullを代入することで、オブジェクトへの参照を消去します。この段階では、変数bからオブジェクトへ参照されているため、GCが発生してもこのオブジェクトはGCの対象となりません。
4行目では、変数bにもオブジェクトへの参照を消去しています。この段階で、このオブジェクトに対する参照がすべてなくなります。そのため、次にGCが行われるときに、このオブジェクトが使用していたヒープが解放されます。
なぜGC後でもヒープの使用量が上がってしまうか~メモリリーク
GCが行われると不要なオブジェクトが削除されますが、ヒープの使用量を監視していると、GC後のヒープ使用量が右肩上がりに上がっていくことがあります。このような場合、プログラムの実行に今後必要ないにもかかわらず、参照が残ってしまい、GCの対象とならないオブジェクトが増加している可能性があります。
このようなことをメモリリークと言います。
このようなオブジェクトが1つ2つある程度では問題がないように思えますが、塵が積もれば山となってしまい、ヒープが使用できる領域が圧迫されてしまいます。
メモリリークは、コレクションクラスと呼ばれるListインターフェースやMapインターフェースなどの実装クラスがアプリケーションの広い範囲で使用される場合には、コレクションクラスからオブジェクトの消し忘れによって発生する可能性が高く、メモリリークを発見することは非常に困難です。
結果として、プログラムで使えるはずのヒープが少なくなり、GCが多発します。そのため、発生が確認できた場合は優先的にプログラムを修正しましょう。
オブジェクトがGCに回収される前に処理を行うには~ファイナライザ
プログラムを作っていると、オブジェクトを回収する前にログを出力させたり、リソースを解放したりなど何か処理を行わせたいことがあるかもしれません。
そのような場合に利用するのが「ファイナライザ」です。
オブジェクト指向経験者の方には、オブジェクトを削除する際に実行される「デストラクタ」に似たものと考えていただいてかまいません。
ファイナライザは、すべてのクラスの親クラスであるObjectクラスに実装されているfinalize()メソッドをオーバーライドすることで実装できます。
ファイナライザは、オブジェクトへの参照がなくなってから、オブジェクトがGCに回収される前に、ファイナライザスレッドで実行されます。
ファイナライザスレッドは、Objectクラスで実装されたfinalize()メソッドを実行しますが、基本的には何も処理を行わないように実装されています。しかし、サブクラスでオーバーライドされている場合は、オーバーライドされたfinalize()メソッドを実行します。
そのとき、以下のような場合には、不要なオブジェクトがファイナライザで処理待ちとなり、ファイナライザスレッドの処理がGCでオブジェクトを回収する速度に追いつけなくなります。
- 処理を必要とするfinalize()メソッドをサブクラスでオーバーライドしたオブジェクトが多い場合
- finalize()メソッドで時間のかかる処理をしている場合
結果として、ヒープの解放も遅くなり、ヒープが枯渇するとOOMEが発生してしまいます。
C++では、先述のように、delete演算子で削除のタイミングをプログラマが制御できます。しかし、ファイナライザは「GCで回収される前に実行される」としか決まっていないため、いつどのタイミングで呼ばれるかわからず、実行される順番も保証されていません。
そのため、データベースへのコネクションの解放や、ファイルの解放など、リソースの解放処理をファイナライザで行うと、リソースがいつ解放されるかわからず、リソースを予期せずに占有してしまうことがあります。このようなリソースが解放されるタイミングに影響される設計は避ける必要があります。
次回は、どのような場合にGCが問題になるのかについて解説します。お楽しみに。