本ページはプロモーションが含まれています

Java入門

Javaのガベージコレクションの仕組みを徹底解説!

トム

・SaaS勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

「なんだかアプリケーションの動きが時々カクつくな…」「先輩からGCが原因かもと言われたけど、GCって一体何?」

Javaを学び始めた方や、運用に携わるようになったエンジニアの方から、このような相談を受けることがよくあります。私自身、10年以上のJava開発経験の中で、ガベージコレクション(GC)が原因のパフォーマンス問題に直面し、その都度チューニングで乗り越えてきました。

GCはJavaの便利な自動メモリ管理機能ですが、その仕組みを理解していないと、思わぬパフォーマンスの低下に悩まされることになります。

この記事を読めば、Javaのガベージコレクションとは何か、どのような仕組みで動いているのか、そしてパフォーマンスを改善するために何ができるのか、その基本を網羅的に理解できます。GCの謎を解き明かし、アプリケーションの安定稼働への第一歩を踏み出しましょう。

ガベージコレクションとは?

Javaのガベージコレクション(GC)とは、プログラムが使わなくなったメモリ領域を自動的に見つけて解放する仕組みです。これがあるおかげで、開発者はメモリ管理をJava(JVM)に任せて、アプリケーションのロジック開発に集中できるのです。

Javaにおけるメモリ管理の仕組み

プログラムを動かすには、データやオブジェクトを一時的に保存する場所、つまり「メモリ」が必要です。Javaでは、プログラムが必要なメモリをOSから確保し、その中でオブジェクトを作成したり、不要になったものを片付けたりしています。

この「片付け」を担当するのがガベ-ジコレクションです。例えるなら、優秀な自動お掃除ロボットのようなもの。部屋(メモリ)が散らかってきたら(不要なオブジェクトが増えたら)、自動で掃除(メモリ解放)をしてくれるので、私たちはいつでもきれいな部屋で快適に過ごせ(プログラムを安定して動かせ)ます。

C言語との違い(手動管理との比較)

一方、C言語のようなプログラミング言語では、このような自動お掃除ロボットは存在しません。開発者自身が、メモリの確保(malloc)と解放(free)をコードに明記する必要があります。

これは非常に手間がかかる上、「解放漏れ」や「二重解放」といったメモリ関連のバグを生み出しやすい、とても神経を使う作業でした。解放漏れが起きるとメモリリークとなり、アプリケーションが最終的にクラッシュする原因にもなります。

Javaのガベージコレクションは、このような手動メモリ管理の複雑さや危険性から開発者を解放してくれる、画期的な仕組みなのです。

Javaガベージコレクションの仕組み

それでは、この優秀なお掃除ロボットは、どのようにして「ゴミ」と「まだ使うもの」を見分けているのでしょうか。その仕組みを3つのステップで見ていきましょう。

ヒープ領域とオブジェクトの寿命

Javaでnewキーワードを使って作られたオブジェクトは、すべて「ヒープ領域」と呼ばれるメモリ空間に格納されます。このヒープ領域が、GCの主戦場、つまりお掃除の対象エリアです。

オブジェクトには「寿命」があります。プログラムのある部分で必要とされて作られても、処理が進むにつれて不要になります。この「不要になった」状態を、GCはどのように判断するのでしょうか。

ルート(GC Root)からの参照関係

GCは、まず「GC Root」と呼ばれる特別な起点から、オブジェクトをたどっていきます。GC Rootは、プログラムが直接アクセスできる場所のことで、例えば実行中のメソッド内にある変数などが該当します。

イメージとしては、家の玄関(GC Root)からスタートして、そこからつながっているすべての家具や物(オブジェクト)を一つひとつ確認していく作業に似ています。玄関からたどって行き着けるものは「まだ使っているもの」と判断されます。

「参照がない=回収対象」となる流れ

GC Rootからたどる作業を行った結果、どこからもたどり着けなかったオブジェクトが出てきます。これが「どこからも参照されていないオブジェクト」、つまり「不要になったゴミ」です。

GCは、このゴミと判断されたオブジェクトが使っていたメモリ領域を解放し、再利用できるようにします。この一連の流れを繰り返すことで、ヒープ領域は常にクリーンに保たれるわけです。

  1. マーキング: GC Rootからオブジェクトをたどり、生きているオブジェクトに印(マーク)を付ける。
  2. スイープ(回収): 印が付かなかったオブジェクト(ゴミ)を回収し、メモリを解放する。

このシンプルなルールが、Javaの安定した動作を支えています。

主なGCアルゴリズム4選

お掃除ロボットにも様々な種類があるように、GCにもいくつかのアルゴリズム(掃除の方法)が存在します。ここでは代表的な4つのアルゴリズムを紹介します。

Mark and Sweep(マーク&スイープ方式)

これは最も基本的なGCアルゴリズムで、先ほど説明した仕組みそのものです。

ポイント

  1. Mark(マーク): 生きているオブジェクトに印を付ける。
  2. Sweep(スイープ): 印が付いていないオブジェクトを回収する。

シンプルで分かりやすいですが、掃除後にメモリが虫食い状態になる「フラグメンテーション」が起きやすいという弱点があります。

Copying(コピー方式)

メモリ空間を2つの領域に分け、片方だけを使用します。GCが発生すると、生きているオブジェクトだけをもう一方の空いている領域にコピーします。

コピーが終わると、元の領域はすべてゴミだったことになるので、一気にクリアします。この方式はフラグメンテーションが起きず高速ですが、常に半分のメモリ領域が遊んでいる状態になるため、メモリ効率が悪いのが欠点です。

Generational GC(世代別GC)

多くのアプリケーションでは、「ほとんどのオブジェクトは作られてすぐに不要になる」という統計的な事実があります。この性質を利用したのが世代別GCです。

ヒープ領域を、生まれたばかりのオブジェクトを置く「New(Young)領域」と、何度もGCを生き延びたオブジェクトを置く「Old領域」に分けます。

ポイント

  • New領域: 頻繁にGC(Minor GC)を行い、すぐに不要になるオブジェクトを効率的に掃除する。ここでは高速なコピー方式が使われることが多いです。
  • Old領域: 生き残ったオブジェクトが移動してくる場所。GCの頻度は低く(Major GCまたはFull GC)、マーク&スイープ方式などが使われます。

このハイブリッドなアプローチにより、アプリケーション全体の停止時間を短くし、効率的なメモリ管理を実現しています。現在の多くのJavaアプリケーションで採用されている主流の考え方です。

G1GC(Garbage First GC)

G1GCは、ヒープ領域を多数の小さな「リージョン」に分割して管理します。そして、ゴミが最も多く溜まっているリージョンから優先的(Garbage First)に掃除を行います。

大きなヒープ領域でも予測可能な短い停止時間を実現できるように設計されており、近年のJavaでは標準的なGCとして採用されています。

JavaのGC実装5種類

これまで見てきたアルゴリズムをベースに、Java(JVM)にはいくつかのGC実装が用意されています。バージョンアップと共に、より高性能なGCが登場してきました。

Serial GC

シングルスレッドでGCを行う、最もシンプルな実装です。GC中はアプリケーションが完全に停止(Stop The Worldと呼ばれます)します。CPUコアが1つしかないような小規模な環境や、クライアントPC上の単純なアプリケーション向けです。

Parallel GC

複数のスレッドを使ってGC処理を並列実行することで、GC時間を短縮します。特にNew領域のGC(Minor GC)を高速に行うのが得意です。Java 8までのデフォルトGCであり、スループット(単位時間あたりの処理量)を重視するバッチ処理などに向いています。

CMS(Concurrent Mark-Sweep)GC

アプリケーションの停止時間(Stop The World)を極力短くすることに重点を置いたGCです。マークやスイープといった処理の一部を、アプリケーションスレッドと同時に(Concurrent)実行しようと試みます。応答速度が求められるWebアプリケーションなどで利用されていました。

G1GC

前述の通り、ヒープをリージョンに分割して管理するGCです。スループットと応答時間のバランスが良く、Java 9からデフォルトのGCとなりました。数GBから数十GBといった比較的大きなヒープサイズを扱う現代的なアプリケーションに適しています。

ZGC(最新のGC方式)

ZGCは、Stop The Worldを極限まで短くすることを目指した最新のGCです。数TB(テラバイト)級の巨大なヒープサイズでも、停止時間をわずか数ミリ秒に抑えることを目標としています。応答性能が非常にクリティカルな、大規模システム向けのGCと言えるでしょう。

パフォーマンス改善!GCチューニング実践

GCは自動で動きますが、時にはアプリケーションの特性に合わせて設定を調整(チューニング)することで、パフォーマンスが劇的に改善することがあります。

GCの動きを見る

まずは、GCがどのように動いているかを知るのが第一歩です。Javaの起動オプションに以下を追加すると、GCの動作がログに出力されます。

-Xlog:gc

このログを見ることで、GCの発生頻度や、一回のGCにかかった時間、メモリの解放量などを確認できます。パフォーマンス問題の調査には不可欠な情報です。

メモリサイズを調整する

最も基本的で効果的なチューニングが、ヒープサイズの設定です。

  • -Xms<size>: ヒープ領域の初期サイズ
  • -Xmx<size>: ヒープ領域の最大サイズ

例えば、初期サイズを1,024MB、最大サイズを2,048MBに設定する場合は以下のようになります。

-Xms1024m -Xmx2048m

一般的に、サーバーアプリケーションでは-Xms-Xmxを同じ値に設定することが推奨されます。実行中にヒープサイズが変動する際のオーバーヘッドをなくし、パフォーマンスを安定させることができます。

チューニングの基本的な考え方

GCチューニングは奥が深い世界ですが、基本はシンプルです。

  1. 現状把握: まずはGCログを取得し、現状のGCの振る舞いを正確に把握します。
  2. ボトルネック特定: GCの時間が長すぎるのか、それとも頻度が多すぎるのか、問題点を特定しましょう。
  3. 仮説と検証: ヒープサイズやGCアルゴリズムの変更といった対策を立て、実際に試して効果を測定します。

やみくもな設定変更は、かえって状況を悪化させることもあります。根本的な解決策は、不要なオブジェクトの生成を抑えるプログラムを書くことである、という点も忘れてはいけません。

まとめ

この記事では、Javaのガベージコレクションの基本的な仕組みから、アルゴリズムの種類、そして実践的なチューニングの第一歩までを解説しました。

Java GCを理解する3つのメリット

JavaのGCを理解することは、エンジニアにとって大きな力になります。

  1. パフォーマンス問題の原因究明: アプリケーションが遅い時、GCが原因かどうかを切り分けられるようになります。
  2. メモリ効率の良いコード: GCの仕組みを知ることで、メモリに優しい、より効率的なコードを書く意識が芽生えます。
  3. 適切なJVM設定: アプリケーションの特性に合わせた、適切なヒープサイズやGCアルゴリズムを選択できるようになるでしょう。
  • この記事を書いた人
  • 最新記事

トム

・SaaS勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

-Java入門