Java | Java 詳細・モダン文法:JVM・パフォーマンス – GC の基本

Java Java
スポンサーリンク

GC を一言でいうと「いらなくなったオブジェクトの自動掃除屋」

GC(Garbage Collection)は、
もう使われていないオブジェクトを、自動で見つけて捨ててくれる仕組み」です。

C や C++ では、自分で freedelete を呼んでメモリを解放しますが、
Java ではそれを GC が肩代わりしてくれます。

だから Java では「解放し忘れ」は起きにくい。
ただし、「まだ参照を持ち続けているせいで捨てられない」という別の種類の問題(メモリリーク)は普通に起こります。

GC の基本は、
「どのオブジェクトが“まだ生きている”かを判定し、“死んでいる”ものを回収する」
この一点です。


GC がどうやって「生きているオブジェクト」を見つけるか

ルート(root)からたどれるかどうか

GC は、「どこからも届かないオブジェクト」を「死んでいる」とみなします。
ここでいう「どこからも届かない」とは、
特定の「ルート」から参照をたどっていっても、そのオブジェクトに辿り着けない、という意味です。

この「ルート」に含まれるのは、例えばこんなものです。

スレッドのスタック上の変数(ローカル変数、引数など)。
static フィールド。
ネイティブコードから参照されているオブジェクトなど。

GC は、まずこのルートたちからスタートして、
参照をたどりながら「到達可能なオブジェクト」に印を付けていきます。

印が付かなかったオブジェクトは、
「どこからも参照されていない=もう使われていない」と判断され、回収対象になります。

簡単なイメージコード

public class GcBasic {
    static Object staticRef;

    public static void main(String[] args) {
        Object a = new Object();      // ①
        Object b = new Object();      // ②

        staticRef = a;                // ③

        a = null;                     // ④
        b = null;                     // ⑤

        System.gc();                  // ⑥(GC をお願いしてみる)
    }
}
Java

このときの状態を言葉で追ってみます。

① で作られたオブジェクト(A)と、② で作られたオブジェクト(B)は、どちらもヒープにあります。
ab はスタック上の変数で、それぞれ A と B を指しています。

③ で staticRef(クラスの static フィールド)が A を指すようになります。

④ で anull にすると、スタック上から A への参照は消えますが、
staticRef がまだ A を指しているので、A は「生きている」とみなされます。

⑤ で bnull にすると、B を指している参照はどこにもなくなります。
ルート(スタックや static フィールド)から辿っても B に届かないので、B は「死んでいる」と判断され、GC の回収対象になります。

このように、
「参照が残っているかどうか」ではなく
「ルートから辿れるかどうか」で生死を決める、
というのが GC の基本ルールです。


なぜ GC は「世代(Young / Old)」に分けるのか

多くのオブジェクトは「すぐ死ぬ」

現実のプログラムでは、
「すぐに使い捨てられるオブジェクト」が圧倒的に多いです。

例えば、ループの中で毎回作る一時オブジェクトや、
メソッド内だけで使ってすぐ捨てるオブジェクトなど。

この性質を利用して、JVM はヒープを「世代」に分けています。

Young(若い世代):生まれたばかりのオブジェクトが置かれる。
Old(年老いた世代):Young で何度も GC を生き延びた長寿オブジェクトが移される。

Young GC と Old GC

Young 領域は、「すぐ死ぬものが多い」前提で設計されています。

ここでは、

生きているオブジェクトだけを一気に別の場所にコピーし、
残りをまとめて捨てる

といった効率的なアルゴリズムが使われます。
その結果、Young GC は「頻繁に起きるけど、比較的短時間で終わる」ことが多いです。

一方、Old 領域は「長生きする大きなオブジェクト」が多いので、
GC の頻度は低い代わりに、一回のコストは重くなりがちです。

パフォーマンスチューニングでは、

「Young での GC が多すぎないか」
「Old に大きなゴミが溜まりすぎていないか」

といった観点でログを見ていくことになります。


GC が動くとき、アプリは止まるのか?

Stop-The-World(STW)という現実

GC がオブジェクトの生死を判定するとき、
「その間にアプリ側がメモリを書き換えたら困る」
という問題があります。

そのため、多くの GC 処理は、
一時的にアプリケーションスレッドを止めて(Stop-The-World)、
安全な状態でメモリをスキャンします。

この「止まっている時間」が長くなると、
レスポンスがガタッと悪くなったり、
リアルタイム性が求められるシステムで問題になったりします。

最近の GC(G1, ZGC, Shenandoah など)は、
この「止まる時間」をできるだけ短くするために、
並行処理や分割処理などの工夫をしていますが、
「GC がアプリを一瞬止めることがある」という事実は変わりません。

初心者がまず意識すべきこと

最初のうちは、GC のアルゴリズムの細かい違いよりも、

オブジェクトを無駄に作りすぎない。
長生きする大きなオブジェクトを、不要なのに参照し続けない。

この二つを意識するだけで、
GC 由来のトラブルはかなり減らせます。


GC とメモリリーク:「Java でもリークは普通に起きる」

「解放し忘れ」ではなく「参照し続けてしまう」

よくある誤解が、
「Java には GC があるからメモリリークは起きない」というものです。

実際には、
「もう使わないオブジェクトを、どこかが参照し続けている」
という形で、メモリリークは普通に起きます。

典型的なパターンは、

static なコレクションにオブジェクトを追加し続けて、削除しない。
キャッシュを作ったが、サイズ制限や期限を設けていない。
リスナーやコールバックを登録したまま解除しない。

などです。

GC は「どこからも参照されていないもの」しか捨てられません。
「まだ参照されているけど、論理的にはもう不要」というものは、
プログラマが設計でどうにかするしかありません。


System.gc() は呼ぶべきか?

「お願い」であって「命令」ではない

System.gc() を呼ぶと、
「今 GC をしてほしいです」と JVM にお願いすることができます。

ただし、これはあくまで「お願い」であって、
必ずしもその場で GC が走るとは限りません。

また、むやみに System.gc() を呼ぶと、
不要なタイミングで GC が走ってしまい、
パフォーマンスを悪化させることもあります。

基本的には、

System.gc() は特別な理由がない限り書かない。
GC のタイミングは JVM に任せる。

というスタンスでいてください。


まとめ:GC の基本を自分の言葉で説明するなら

あなたの言葉で整理すると、こうなります。

「GC は、ヒープ上のオブジェクトのうち『ルート(スタックや static フィールドなど)から辿れないもの』を“死んでいる”とみなし、自動で回収する仕組み。
ヒープは Young と Old の世代に分かれていて、短命オブジェクトは Young で頻繁に軽く掃除し、長生きオブジェクトは Old でたまに重く掃除する。
GC が動くときにはアプリが一瞬止まることがあり、それが長くなるとパフォーマンス問題になる。

Java でも、参照を持ち続けてしまえばメモリリークは普通に起きる。
だから、
『本当に必要なオブジェクトだけを作る』
『もう使わないものへの参照はちゃんと手放す』
という設計が、GC と仲良くやるための基本になる。」

タイトルとURLをコピーしました