メインコンテンツまでスキップ

メモリ管理

所要時間: 35分

前提知識: free, vmstat コマンドを使ったことがある

学べること:

  • 仮想メモリの仕組み
  • ページングとスワップ
  • メモリの種類(RSS, VSZ, Shared)
  • OOM Killer の動作

この章で答える疑問

「free の数字の意味は?」
「VSZ と RSS の違いは?」
「スワップって何?」
「OOM Killer って何?なぜプロセスが突然死ぬ?」

1. 仮想メモリとは

1.1 なぜ仮想メモリが必要か

┌─────────────────────────────────────────────────────────────────────┐
│ 仮想メモリがない世界 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 問題1: メモリ不足 │
│ ├── 物理メモリ 16GB に 20GB 分のプログラムは動かない │
│ │
│ 問題2: メモリの断片化 │
│ ├── プログラム A, B, C が起動・終了を繰り返すと │
│ └── 連続した空き領域がなくなる │
│ │
│ 物理メモリ: │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │ A │ │ B │ │ C │ │ A │ │ B │ │ │
│ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ │
│ ↑ ↑ ↑ ↑ ↑ │
│ 空き 空き 空き 空き 空き(断片化) │
│ │
│ 問題3: セキュリティ │
│ ├── プログラム A がプログラム B のメモリを読める │
│ └── 悪意のあるプログラムがパスワードを盗める │
│ │
└─────────────────────────────────────────────────────────────────────┘

1.2 仮想メモリの仕組み

┌─────────────────────────────────────────────────────────────────────┐
│ 仮想メモリの概念 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 各プロセスは「自分専用の巨大なメモリ空間」を持っているように見える │
│ │
│ プロセスA プロセスB │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 仮想アドレス空間 │ │ 仮想アドレス空間 │ │
│ │ │ │ │ │
│ │ 0x0000 │ │ 0x0000 │ ← 同じアドレスでも │
│ │ ... │ │ ... │ 別の物理メモリ │
│ │ 0x7FFF... │ │ 0x7FFF... │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ │ ページテーブル │ ページテーブル │
│ │ (変換表) │ (変換表) │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 物理メモリ │ │
│ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │
│ │ │ A │ │ B │ │ A │ │共有│ │ B │ │空き│ │ │
│ │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

1.3 アドレス変換

┌─────────────────────────────────────────────────────────────────────┐
│ 仮想アドレス → 物理アドレス変換 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ CPU │
│ │ │
│ │ 仮想アドレス: 0x00401000 │
│ ▼ │
│ ┌───────────────────────────────────────┐ │
│ │ MMU (Memory Management Unit) │ │
│ │ │ │
│ │ ページテーブルを参照して変換 │ │
│ │ │ │
│ │ 仮想ページ番号: 0x00401 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ ページテーブル │ │ │
│ │ ├──────────────────────────────────┤ │ │
│ │ │ 仮想 │ 物理 │ フラグ │ │ │
│ │ ├──────────┼───────────┼───────────┤ │ │
│ │ │ 0x00401 │ 0x12345 │ RW- │ │ │
│ │ │ 0x00402 │ 0x67890 │ R-- │ │ │
│ │ │ ... │ ... │ ... │ │ │
│ │ └──────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────┘ │
│ │ │
│ │ 物理アドレス: 0x12345000 │
│ ▼ │
│ 物理メモリ │
│ │
└─────────────────────────────────────────────────────────────────────┘

2. ページングとスワップ

2.1 ページとは

┌─────────────────────────────────────────────────────────────────────┐
│ ページの概念 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ メモリは「ページ」という固定サイズのブロックで管理される │
│ │
│ 典型的なページサイズ: 4KB (x86/x64) │
│ │
│ 仮想メモリ 物理メモリ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ ページ0 (4KB) │ ────────────→│ フレーム 5 │ │
│ ├─────────────────┤ ├─────────────────┤ │
│ │ ページ1 (4KB) │ ────────────→│ フレーム 12 │ │
│ ├─────────────────┤ ├─────────────────┤ │
│ │ ページ2 (4KB) │ ──(未割当) │ フレーム 8 │ │
│ ├─────────────────┤ ├─────────────────┤ │
│ │ ページ3 (4KB) │ ────────────→│ フレーム 3 │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ページ = 仮想メモリのブロック │
│ フレーム = 物理メモリのブロック │
│ │
└─────────────────────────────────────────────────────────────────────┘

2.2 ページフォルト

┌─────────────────────────────────────────────────────────────────────┐
│ ページフォルトの流れ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. プログラムがメモリアドレス 0x12345678 にアクセス │
│ │ │
│ ▼ │
│ 2. MMU がページテーブルを確認 │
│ │ │
│ ├── 物理メモリにある → 通常のアクセス │
│ │ │
│ └── 物理メモリにない → ページフォルト発生! │
│ │ │
│ ▼ │
│ 3. カーネルが処理を引き継ぐ │
│ │ │
│ ├── 正当なアクセスの場合: │
│ │ ├── ディスクからデータを読み込み(スワップイン) │
│ │ ├── または新しいページを割り当て │
│ │ └── プログラムを再開 │
│ │ │
│ └── 不正なアクセスの場合: │
│ └── Segmentation Fault(セグフォ)→ プロセス終了 │
│ │
└─────────────────────────────────────────────────────────────────────┘

2.3 スワップ

┌─────────────────────────────────────────────────────────────────────┐
│ スワップの仕組み │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 物理メモリが足りなくなったら、使用頻度の低いページをディスクへ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 物理メモリ (16GB) │ │
│ │ │ │
│ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ← 全部使用中 │ │
│ │ │ A │ │ B │ │ C │ │ D │ │ E │ │ F │ │ │
│ │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 新しいメモリが必要! │
│ │
│ スワップアウト(物理メモリ → ディスク) │
│ ┌───┐ │
│ │ C │ ──────────────────────────────────────┐ │
│ └───┘ │ │
│ ↓ │ │
│ 空きになる ▼ │
│ ┌─────────────────────────────┐ │
│ │ スワップ領域 (ディスク) │ │
│ │ │ │
│ │ ┌───┐ ┌───┐ ┌───┐ │ │
│ │ │ C │ │...│ │...│ │ │
│ │ └───┘ └───┘ └───┘ │ │
│ │ │ │
│ └─────────────────────────────┘ │
│ │
│ ※ ディスクはメモリより約1000倍遅い → スワップが多発すると性能低下 │
│ │
└─────────────────────────────────────────────────────────────────────┘

2.4 Linux での確認

# スワップの状態確認
$ free -h
total used free shared buff/cache available
Mem: 15Gi 8.2Gi 2.1Gi 512Mi 5.0Gi 6.2Gi
Swap: 8.0Gi 1.2Gi 6.8Gi

# スワップ詳細
$ swapon --show
NAME TYPE SIZE USED PRIO
/swap.img file 8G 1.2G -2

# vmstat でスワップ活動を監視
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 1228800 2150400 102400 5120000 0 0 0 10 100 500 10 5 85 0 0
1 0 1228800 2150400 102400 5120000 0 0 0 0 120 600 12 6 82 0 0

# si = swap in (ディスク → メモリ)
# so = swap out (メモリ → ディスク)
# これらが大きいと性能問題

# ページフォルト確認
$ cat /proc/[pid]/stat | awk '{print "minor faults:", $10, "major faults:", $12}'
minor faults: 1234567 major faults: 89

# minor fault: メモリ内で解決(軽い)
# major fault: ディスクアクセスが必要(重い)

3. メモリの種類と free コマンド

3.1 free の出力を読む

$ free -h
total used free shared buff/cache available
Mem: 15Gi 8.2Gi 2.1Gi 512Mi 5.0Gi 6.2Gi
Swap: 8.0Gi 1.2Gi 6.8Gi
┌─────────────────────────────────────────────────────────────────────┐
│ free コマンドの各フィールド │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ total : 物理メモリの総量 (15GB) │
│ │
│ used : 使用中のメモリ (8.2GB) │
│ ├── プロセスが使用 │
│ └── カーネルが使用 │
│ │
│ free : 完全に空いているメモリ (2.1GB) │
│ ※ これが少なくても問題ない場合がある │
│ │
│ shared : 共有メモリ (512MB) │
│ └── tmpfs, 共有ライブラリなど │
│ │
│ buff/cache: バッファとキャッシュ (5.0GB) │
│ ├── buffer: ディスクI/O用の一時領域 │
│ └── cache: ファイルの内容をキャッシュ │
│ ※ 必要に応じて解放される │
│ │
│ available : 新しいプロセスが使えるメモリ (6.2GB) │
│ └── free + 解放可能なbuff/cache │
│ ★ これを見るのが重要! │
│ │
└─────────────────────────────────────────────────────────────────────┘

3.2 よくある誤解

┌─────────────────────────────────────────────────────────────────────┐
│ 「free が少ない!」は問題か? │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ❌ 間違い: free が 2GB しかない → メモリ不足! │
│ │
│ ✅ 正解: available を確認 │
│ │
│ Linux は空きメモリを無駄にしない │
│ ├── 空いているメモリはファイルキャッシュに使う │
│ ├── 新しいプロセスが必要になったらキャッシュを解放 │
│ └── だから free が少なくても available があれば OK │
│ │
│ 本当に問題なのは: │
│ ├── available が少ない │
│ ├── swap が増え続けている │
│ └── OOM Killer が発動 │
│ │
└─────────────────────────────────────────────────────────────────────┘

3.3 VSZ と RSS

$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
java 1234 5.0 8.5 4568792 1397624 ? Sl 10:30 15:23 java -jar app.jar
┌─────────────────────────────────────────────────────────────────────┐
│ VSZ と RSS の違い │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ VSZ (Virtual Size): 4,568,792 KB ≈ 4.3GB │
│ ───────────────── │
│ プロセスの仮想アドレス空間の合計 │
│ ├── 実際に物理メモリを使っているわけではない │
│ ├── 「予約」されているだけのメモリも含む │
│ └── 共有ライブラリの全体も含む(重複カウント) │
│ │
│ RSS (Resident Set Size): 1,397,624 KB ≈ 1.3GB │
│ ──────────────────────── │
│ 実際に物理メモリに存在するサイズ │
│ ├── プロセスが今使っている物理メモリ │
│ ├── 共有ライブラリの共有部分も含む(重複カウント) │
│ └── こちらの方が実態に近い │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 仮想アドレス空間 (VSZ: 4.3GB) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 物理メモリにある (RSS: 1.3GB) │ │ │
│ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │
│ │ │ │ヒープ │ │スタック │ │共有lib │ │ │ │
│ │ │ │(使用中) │ │ │ │ │ │ │ │
│ │ │ └────────┘ └────────┘ └────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 物理メモリにない(スワップ or 未割当) │ │ │
│ │ │ ┌────────────┐ ┌────────────────────────────────┐ │ │ │
│ │ │ │スワップ中 │ │まだ使っていない予約領域 │ │ │ │
│ │ │ └────────────┘ └────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

3.4 PSS と USS

より正確なメモリ使用量を知りたい場合:

# smaps で詳細確認
$ cat /proc/[pid]/smaps_rollup
Rss: 1397624 kB
Pss: 987432 kB
Shared_Clean: 234567 kB
Shared_Dirty: 8901 kB
Private_Clean: 123456 kB
Private_Dirty: 567890 kB
┌─────────────────────────────────────────────────────────────────────┐
│ メモリ測定の種類 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ USS (Unique Set Size): │
│ ├── そのプロセス固有のメモリ │
│ └── Private_Clean + Private_Dirty │
│ │
│ PSS (Proportional Set Size): │
│ ├── 共有メモリを共有しているプロセス数で割った値 │
│ ├── 例: 3プロセスで共有する 300KB → 各プロセスに 100KB として計上 │
│ └── システム全体のメモリ使用量を把握するのに最適 │
│ │
│ RSS (Resident Set Size): │
│ ├── 共有メモリを全額カウント │
│ └── 複数プロセスの RSS を足すと実際より大きくなる │
│ │
│ 正確さ: USS < PSS < RSS │
│ │
└─────────────────────────────────────────────────────────────────────┘

4. OOM Killer

4.1 OOM Killer とは

┌─────────────────────────────────────────────────────────────────────┐
│ OOM Killer の役割 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ OOM = Out Of Memory │
│ │
│ システムがメモリ不足になったとき: │
│ ├── スワップも使い果たした │
│ ├── 新しいメモリを確保できない │
│ └── システム全体がフリーズする危険 │
│ │
│ → カーネルの OOM Killer が「犠牲者」を選んで強制終了 │
│ │
│ 目的: システム全体のクラッシュを防ぐ │
│ │
└─────────────────────────────────────────────────────────────────────┘

4.2 犠牲者の選び方

┌─────────────────────────────────────────────────────────────────────┐
│ OOM Score の計算 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 各プロセスに「OOM スコア」が付けられる(0〜1000) │
│ スコアが高いほど kill されやすい │
│ │
│ スコアを上げる要因: │
│ ├── メモリ使用量が大きい │
│ ├── 子プロセスのメモリ使用量 │
│ └── 実行時間が短い(長く動いているプロセスは保護される傾向) │
│ │
│ スコアを下げる要因: │
│ ├── root 権限で動いている │
│ ├── ハードウェアに直接アクセスしている │
│ └── oom_score_adj で調整されている │
│ │
│ 確認: │
│ $ cat /proc/[pid]/oom_score │
│ 250 │
│ │
│ $ cat /proc/[pid]/oom_score_adj # -1000〜1000 で調整可能 │
│ 0 │
│ │
└─────────────────────────────────────────────────────────────────────┘

4.3 OOM Killer の発動を確認

# dmesg で OOM Killer のログを確認
$ dmesg | grep -i "out of memory"
[12345.678901] Out of memory: Kill process 5678 (java) score 850 or sacrifice child
[12345.678902] Killed process 5678 (java) total-vm:4568792kB, anon-rss:1234567kB

# journalctl でも確認可能
$ journalctl -k | grep -i oom
Jan 01 12:34:56 server kernel: java invoked oom-killer: gfp_mask=0x...

4.4 OOM Killer から守る

# 重要なプロセスを OOM Killer から保護
# oom_score_adj を -1000 に設定すると絶対に kill されない
$ echo -1000 > /proc/[pid]/oom_score_adj

# systemd サービスの場合
# /etc/systemd/system/myapp.service
[Service]
OOMScoreAdjust=-1000

# ただし注意:
# 保護されたプロセスが原因で OOM になった場合、
# 他のプロセスが kill されるか、最悪システムがフリーズする

4.5 OOM を防ぐ設計

┌─────────────────────────────────────────────────────────────────────┐
│ OOM 対策 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. メモリ上限を設定(Java の場合) │
│ java -Xmx4g -Xms4g -jar app.jar │
│ └── 最大ヒープを明示的に制限 │
│ │
│ 2. コンテナでメモリ制限 │
│ docker run --memory=4g myapp │
│ └── コンテナがメモリを使いすぎたら OOM で kill │
│ │
│ 3. 監視とアラート │
│ ├── available メモリを監視 │
│ ├── スワップ使用量を監視 │
│ └── 閾値を超えたらアラート │
│ │
│ 4. 適切なサイジング │
│ ├── 負荷テストでメモリ使用量を測定 │
│ └── ピーク時の 1.5〜2 倍の余裕を確保 │
│ │
└─────────────────────────────────────────────────────────────────────┘

5. Copy-on-Write (CoW)

5.1 CoW とは

┌─────────────────────────────────────────────────────────────────────┐
│ Copy-on-Write の仕組み │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ fork() でプロセスをコピーする時: │
│ │
│ ❌ 素朴な実装: メモリを全部コピー → 遅い、メモリ消費大 │
│ │
│ ✅ CoW: ページを共有し、書き込み時にコピー │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Step 1: fork() 直後 │ │
│ │ │ │
│ │ 親プロセス ──共有──→ ページA (読み取り専用) │ │
│ │ ↑ │ │
│ │ 子プロセス ──共有──┘ │ │
│ │ │ │
│ │ → 実際にはコピーしない、ページテーブルだけ更新 │ │
│ │ → メモリ効率が良い │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Step 2: 子プロセスがページAに書き込み │ │
│ │ │ │
│ │ 親プロセス ────────→ ページA (元のデータ) │ │
│ │ │ │
│ │ 子プロセス ────────→ ページA' (コピー、新しいデータ) │ │
│ │ │ │
│ │ → 書き込み時に初めてコピーが発生 │ │
│ │ → 読み取りだけなら永遠にコピーしない │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

6. idp-server での活用

6.1 JVM のメモリ設定

# 推奨設定(コンテナ環境)
java \
-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:InitialRAMPercentage=75.0 \
-jar idp-server.jar

# コンテナのメモリの 75% を JVM ヒープに割り当て
# 残り 25% は Metaspace, Native Memory, OS に使用

6.2 メモリ監視のポイント

┌─────────────────────────────────────────────────────────────────────┐
│ 監視すべきメトリクス │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ システムレベル: │
│ ├── available memory: 6GB 以下で警告 │
│ ├── swap used: 増加傾向で警告 │
│ └── OOM Killer 発動: 即時アラート │
│ │
│ JVM レベル: │
│ ├── Heap Used: Max の 80% 以上で警告 │
│ ├── GC Pause Time: 1秒以上で警告 │
│ └── GC Frequency: 頻繁な Full GC で警告 │
│ │
└─────────────────────────────────────────────────────────────────────┘

7. まとめ

┌─────────────────────────────────────────────────────────────────────┐
│ この章で学んだこと │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 仮想メモリ │
│ ├── 各プロセスは独立したアドレス空間を持つ │
│ ├── MMU がアドレス変換を行う │
│ └── プロセス間の分離を実現 │
│ │
│ 2. ページングとスワップ │
│ ├── メモリは 4KB のページ単位で管理 │
│ ├── 物理メモリが足りないとスワップが発生 │
│ └── スワップが多いと性能低下 │
│ │
│ 3. free コマンド │
│ ├── free より available を見る │
│ └── buff/cache は必要に応じて解放される │
│ │
│ 4. VSZ と RSS │
│ ├── VSZ: 仮想メモリサイズ(予約含む) │
│ └── RSS: 物理メモリ使用量(実態に近い) │
│ │
│ 5. OOM Killer │
│ ├── メモリ不足時にプロセスを強制終了 │
│ ├── oom_score で犠牲者を選択 │
│ └── 重要なプロセスは保護可能 │
│ │
└─────────────────────────────────────────────────────────────────────┘

確認問題

  1. 仮想メモリの利点を3つ挙げてください
  2. free コマンドで availablefree の違いは?
  3. VSZ と RSS の違いを説明してください
  4. OOM Killer はどのようにして犠牲者を選びますか?
  5. スワップが多発している場合、何が問題ですか?

次のステップ