はじめに
[!NOTE] この記事は、筆者が鈍器本を読みながら書き殴った個人的な読書メモをAIに食べさせ、ブログ用に読みやすく再構成させたものです。
これまでCPUやOSの基礎、アプリケーション分析の極意をまとめてきたが、今回はパフォーマンス問題のもう一つの主役である「7章 メモリ」だ。
システムの大規模なリアーキテクチャやスレッドモデルの激変といった影響評価においては、単なるCPU使用率だけでなく、アロケーション効率やメモリフットプリントの変化を正確に評価することがどうしても欠かせない。
CPUのときと同様、ガベージコレクション(GC)の負荷による突然死や、メモリ枯渇の限界ラインを正しく見極めるための基礎知識を、「現場のサバイバル術」としてサクッと見返せるようにチートシート化している。
1. 「メモリ確保=消費」ではない? デマンドページングの罠
アプリケーションが「メモリを確保した」と言っても、実際に物理メモリが割り当てられるのは「初めて書き込んだ瞬間」だ。これを知らないと、メモリの使用量を完全に見誤る羽目になる。
- デマンドページング: アプリが
malloc()などで仮想メモリ空間を割り当てられても、その瞬間に物理メモリが消費されるわけではない。その仮想アドレスに「初めて書き込みを行った瞬間(オンデマンド)」にページフォールトが発生し、カーネルがそこで初めて物理メモリを割り当てる。 - オーバーコミットとOOMキラー: この遅延割り当ての仕組みを利用して、Linuxは実際の物理メモリ(とスワップ領域の合計)以上のメモリ割り当て要求を許可する「オーバーコミット」をサポートしている。しかし、限界を超えてみんなが一斉に本気でメモリを書き込み始めると、「OOM(Out of Memory)キラー」が発動し、重いプロセスが強制終了(容赦ない斬首)されるという恐ろしい事態を招く。
2. パフォーマンスを殺す「スワッピング」の恐怖
同じ「ディスクへのデータ退避」でも、ファイルシステムのキャッシュを追い出すのは正常だが、アプリケーションの稼働データを追い出す「スワッピング」が発生するとパフォーマンスは壊滅する。
- 良いページング(ファイルシステムページング): ディスクから読み込んだファイルのキャッシュを捨てる動作。また必要になれば読み直せばいいだけなので、これは正常な動作(良いページング)だ。
- 悪いページング(無名ページング / スワッピング): アプリケーションのヒープ領域など、ファイルと結びついていない「無名メモリ」が足りなくなり、強制的にスワップデバイス(ディスク)に退避される動作だ。Linuxではこれを「スワッピング」と呼ぶ。これが発生すると、アプリはデータを読み戻すために深刻なディスクI/Oのレイテンシに巻き込まれるため、パフォーマンスが激減する。絶対に避けたい。
3. 本当のメモリ使用量を暴く「WSS」と「PSS」
プロセスが「確保した」メモリサイズ(仮想メモリ)だけを見ていても実態は掴めない。パフォーマンスを左右するのは、実際に頻繁に使っているメインメモリの量だ。
- WSS(ワーキングセットサイズ): プロセスがシステム上で仕事をするために「頻繁に使っているメインメモリの量」のこと。このWSSがメインメモリの容量に収まりきらなくなると、スワッピングが頻発して急激にパフォーマンスが地獄に落ちる。
- PSS(プロポーショナルセットサイズ): システム共有ライブラリなど、他のプロセスとシェアしているメモリ空間をユーザー数で平等に割り、プライベートメモリに足し合わせた指標だ。「本当にこのプロセスが消費している物理メモリの量」をリアルに把握したい場合、
pmapコマンドなどでこのPSSを見るのが極めて有効になる。
4. アロケータの選択による劇的な影響
メモリの割り当てを行うユーザーレベルの「アロケータライブラリ」を変更するだけで、マルチスレッドアプリのパフォーマンスが劇的に向上することがある。
- ロック競合とフラグメンテーション: 標準の
glibc以外にも、TCMalloc(スレッドごとのキャッシュを使ってロック競合を削減)やjemalloc(フラグメンテーションを削減。Facebook等で利用)など、さまざまなアロケータが存在する。 - 特に高度なマルチスレッド環境では、メモリ確保時の同期ロック競合自体が致命的なボトルネックになることがある。環境変数(
LD_PRELOAD)でアロケータを差し替えるだけで、これらが原因のパフォーマンス劣化を簡単に改善・テストできる。
5. JVMチューニングの定石「ヒュージページ」
OSはメモリを「ページ(通常4KB)」という細切れの単位で管理している。しかし、Javaのヒープのように巨大なメモリ空間を使う場合、4KB単位ではページの位置を管理するCPUの辞書(TLBキャッシュ)がすぐにパンクし、変換のオーバーヘッド(TLBミス)がパフォーマンスの足を強烈に引っ張ってしまう。
これを解決するのが「ヒュージページ(2MBや1GBの巨大なページ)」だ。ページサイズを大きくすることでTLBのヒット率が上がり、メモリI/Oのパフォーマンスが劇的に向上する。JVMのパフォーマンスをチューニング・評価するなら、絶対に外してはいけない観点だ。
6. 「メモリリーク」か「正常な成長」かを見極める
稼働中のアプリのメモリ使用量が延々と増え続けているのを見たとき、脊髄反射で「メモリリークだ!」と騒ぐのは早計だ。
- メモリリーク: 使われていないのに解放されないソフトウェアのバグ。コードを直すしかない。
- メモリの成長: アプリがキャッシュをウォームアップさせているなど、実は正常な動作。アプリケーションの構成変更で上限を設けることで対処できる。
これがどちらなのかを見極めるには、アプリがメモリキャッシュをどう使うように構成されているかを理解しておく必要がある。
7. 現場で使えるメモリ分析の武器
「メモリが足りない!」と思ったら、まずは vmstat でスワッピングの有無を確認し、怪しい場合は perf でメモリを食い潰している犯人を直接プロファイリングする。
vmstatでスワッピングを検知:vmstat 1を実行し、si(スワップイン)とso(スワップアウト)の欄が継続的に0以外の値を出している場合、システムは深刻なスワッピングを行っている証拠だ。ここが0をキープしているなら、ひとまず最悪の事態は免れていると判断できる。perfによるページフォールトのプロファイリング: アプリのメモリ使用量がどんどん増えていく理由を知りたい場合、perf record -e page-faults -a -gを使ってページフォールト発生時のスタックトレースを記録する。これを火の玉グラフ(フレームグラフ)で可視化すれば、「どの関数のコードパスが物理メモリの割り当てを要求しているのか」が一目瞭然になる。
おわりに
この章を読むと、アプリケーションのメモリ使用量は「確保した量(仮想メモリ)」ではなく「実際に書き込んだ量(常駐メモリ)」で見なければならないことや、スワッピングというOSの挙動がアプリケーションのレイテンシにどれほどの致命傷を与えるかが痛いほどよく分かる。
次回のアーキテクチャアップデート検証においても、単にヒープサイズの設定だけを見るような愚行は避けたい。vmstat でスワッピングの有無を監視し、perf によるページフォールトのプロファイリングを行うことで、新環境でのメモリアロケーションの効率やガベージコレクション(GC)への影響をしっかり定量化していきたいと思う。









