概要
メモリリーク(Memory Leak)は、プログラムが動的に確保したメモリを、不要になった後も解放せずに使い続ける状態です。解放されないメモリは再利用できないため、長時間稼働するとシステムの使用可能メモリが徐々に減少し、最終的にメモリ不足(OOM: Out of Memory)を引き起こします。
組み込みシステムでのメモリリークは特に深刻です。PCと異なり組み込み機器は数年〜数十年間連続稼働することが多く、わずかなリークでも長期間で致命的な量になります。また、マイコン(MCU)の内蔵SRAMは数KB〜数MBと非常に少ないため、PC環境では問題にならないようなリークでも即座に影響が出ます。
メモリリークはC/C++で多く発生します(ガベージコレクタがないため)。Rust等のメモリ安全言語では言語仕様がリークを防ぎますが、組み込み開発では依然C/C++が主流のため、malloc/freeの対称性確保・所有権の明確化・静的メモリ管理への移行が重要な設計方針です。
歴史・背景
メモリリークはC言語の普及(1970〜80年代)と同時に認識された古典的な問題です。C言語はメモリの確保(malloc/new)と解放(free/delete)をプログラマが明示的に管理する必要があり、ミスがそのままメモリリークになります。
初期のPCプログラムではプロセス終了時にOSがメモリを一括解放するため、短命なプログラムではメモリリークが問題にならないことも多くありました。しかし長時間稼働のサーバー・デーモン・組み込みシステムでは深刻な問題となりました。
1990年代にGarbage Collection(GC)を持つJava・Python・.NETが普及し、GC言語ではメモリリークは大幅に減少しました(ただしGCも参照サイクル等でリークが発生することはあります)。一方、リソース制約の厳しい組み込みシステムではGCのオーバーヘッドが許容できないことが多く、C/C++が使われ続けているため、メモリリーク対策は引き続き重要な課題です。
2010年代にMozillaが開発したRust言語は、コンパイル時の所有権システムでメモリリークを根本的に防ぐアプローチを取り、組み込み分野でも採用が広がっています(Rustのembedded-halエコシステム)。
技術仕様
メモリリークの分類
1. 真のメモリリーク(Classic Leak): mallocしてfreeしないコードパス
void process_request(int req_id) {
char *buf = malloc(1024); // 確保
if (req_id < 0) {
return; // エラー時にfreeせずreturn → リーク!
}
do_process(buf);
free(buf); // 正常系のみ解放
}
2. グロービングリーク(Growing Leak): 参照は残っているが再利用されない
char *global_log_buffer = NULL;
void append_log(const char *msg) {
int new_size = strlen(global_log_buffer) + strlen(msg) + 1;
char *new_buf = malloc(new_size);
strcpy(new_buf, global_log_buffer);
strcat(new_buf, msg);
free(global_log_buffer); // 古いバッファを解放
global_log_buffer = new_buf;
// この設計は問題ないが、freeを忘れるとリークになる
}
3. 間接的なリーク(Indirect Leak): リークしたブロックが参照する別のブロック
typedef struct Node { int data; struct Node *next; } Node;
Node *head = malloc(sizeof(Node));
head->next = malloc(sizeof(Node)); // head をリークすると next もリーク
// free(head) を忘れると head も head->next もリーク
4. RTOSタスクのスタック未解放
// FreeRTOS: タスク削除後にスタックが解放されるか要確認
void delete_task(TaskHandle_t handle) {
vTaskDelete(handle);
// heap_1/heap_2ではスタックメモリが解放されない場合がある
// heap_4/heap_5を使うか、明示的にメモリを解放する
}
組み込みシステムでのメモリ使用量
マイコンのSRAM配置(典型例):
STM32F4(192KB SRAM)のメモリ配置:
0x20000000 ─ .data (初期値あり変数): 8KB
0x20002000 ─ .bss (初期値なし変数): 16KB
0x20006000 ─ heap (動的確保): 最大 40KB
0x20010000 ─ stack (コールスタック): 下方向に伸びる, 最大 8KB
↑ここが衝突するとスタックオーバーフロー
heap が枯渇する = malloc が NULL を返す = メモリリークが原因のことも
動作原理
ヒープ管理の仕組み
C言語のmalloc()はヒープ(heap)と呼ばれる動的メモリ領域から指定サイズを確保します。ヒープマネージャはフリーリストを管理し、適切なブロックを見つけてポインタを返します。free()で解放されたブロックはフリーリストに戻ります。
ヒープの状態変化:
初期: [free: 40KB ]
malloc(1KB) → [used:1KB][free:39KB ]
malloc(2KB) → [used:1KB][used:2KB][free:37KB ]
free(1KB目) → [free:1KB][used:2KB][free:37KB ] ← 断片化が発生
malloc(4KB) → [free:1KB][used:2KB][used:4KB][free:33KB]
(1KBのフリーブロックは使えず断片化)
フラグメンテーション(断片化)
動的メモリ管理を繰り返すと、小さなフリーブロックが散在する外部断片化が発生します。合計フリーサイズは十分でも連続した大きなブロックが確保できなくなります。組み込みシステムでは断片化もメモリ枯渇の原因になります。
FreeRTOSのヒープ管理
FreeRTOSはヒープ管理の実装を5種類(heap_1〜heap_5)提供しています:
| 実装 | 特徴 | 推奨用途 |
|---|---|---|
| heap_1 | 確保のみ(解放なし) | 最もシンプル・静的なシステム |
| heap_2 | 解放可能・断片化あり | 同サイズの繰り返し確保 |
| heap_3 | 標準malloc/freeのラッパー | スレッドセーフ |
| heap_4 | 解放可能・断片化対策あり(隣接ブロック統合) | 最も一般的 |
| heap_5 | heap_4 + 複数の不連続メモリ領域をサポート | 外部RAM追加時 |
// FreeRTOS ヒープ使用量確認
size_t free_heap = xPortGetFreeHeapSize(); // 現在のフリー量
size_t min_heap = xPortGetMinimumEverFreeHeapSize(); // 最小フリー量の履歴
// min_heap が 0 に近い場合は危険
用途・ユースケース
組み込みLinuxでのメモリリーク監視
長期稼働する組み込みLinux機器では、プロセスのメモリ使用量を定期監視することが重要です:
# プロセスのメモリ使用量を定期確認
watch -n 60 'ps aux --sort -rss | head -20'
# /proc/PID/status で詳細確認
PID=$(pgrep myapp)
watch -n 60 "cat /proc/$PID/status | grep -E 'VmRSS|VmSize|VmPeak'"
# VmRSS(実メモリ使用量)が時間とともに増え続けるならリーク疑い
Valgrindによるリーク検出(組み込みLinux開発環境)
クロスコンパイル環境でValgrindを使ってLinuxアプリのメモリリークを検出できます(x86_64のデバッグ環境でテスト):
$ valgrind --leak-check=full --show-leak-kinds=all ./myapp
==12345== HEAP SUMMARY:
==12345== in use at exit: 1,024 bytes in 1 blocks
==12345== total heap usage: 10 allocs, 9 frees, 10,240 bytes allocated
==12345== 1,024 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C29F73: malloc (in /usr/lib/valgrind/vgpreload_memcheck.so)
==12345== by 0x400571: process_request (main.c:42)
==12345== by 0x4005A1: main (main.c:60)
AddressSanitizer(ASan)によるリーク検出
GCC/Clangのアドレスサニタイザーはコンパイル時にメモリ安全性チェックを組み込みます:
# AddressSanitizer付きでビルド
$ gcc -fsanitize=address,leak -g -o myapp myapp.c
$ ./myapp
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 1024 byte(s) in 1 object(s) allocated from:
#0 0x7f... in malloc (...)
#1 0x400... in process_request main.c:42
#2 0x400... in main main.c:60
マイコン(ベアメタル・RTOS)でのリーク対策
マイコンではValgrindは使えません。代わりにヒープ使用量を定期的にモニタリングします:
// ヒープウォーターマークモニタリング(FreeRTOS)
void monitor_task(void *pvParameters) {
for (;;) {
size_t free_heap = xPortGetFreeHeapSize();
printf("Free heap: %d bytes\n", free_heap);
if (free_heap < 1024) {
// ヒープ残量が1KB以下 → 警告 or [ウォッチドッグ](/embedded/glossary/watchdog/)リセット
trigger_alarm(HEAP_CRITICAL);
}
vTaskDelay(pdMS_TO_TICKS(5000)); // 5秒ごとに確認
}
}
// タスクのスタック高水位マーク
UBaseType_t watermark = uxTaskGetStackHighWaterMark(task_handle);
printf("Task stack watermark: %d words\n", watermark);
実装・開発のポイント
動的メモリを使わない設計(静的割り当て)
組み込みの最も確実なメモリリーク対策は、動的メモリを使わないことです:
// 動的: リークのリスクあり
char *buf = malloc(1024);
do_something(buf);
free(buf); // これを忘れるとリーク
// 静的: リークなし(スタックまたはグローバルに固定確保)
char buf[1024]; // スタック変数(関数ローカル)
static char buf[1024]; // 静的変数(グローバルライフタイム)
RAIIパターン(C++)
C++ではRAII(Resource Acquisition Is Initialization)パターンで自動的なメモリ解放を実現します:
// unique_ptr を使った安全なメモリ管理
#include <memory>
void safe_func() {
auto buf = std::make_unique<uint8_t[]>(1024);
// 関数を抜けると自動的に解放される(例外でも安全)
process(buf.get());
} // ここで自動delete[]
// C++17以降はstd::pmr::memory_resourceで組み込みフレンドリーなアロケータも可能
ダブルフリーとNULLポインタ対策
// 安全な解放パターン
void safe_free(void **ptr) {
if (ptr && *ptr) {
free(*ptr);
*ptr = NULL; // NULLにすることでダブルフリーを防ぐ
}
}
// 使用例
char *buf = malloc(1024);
// ... 処理 ...
safe_free((void**)&buf);
safe_free((void**)&buf); // 2回呼んでもOK(NULLチェックで保護)
ウォッチドッグによる自動回復
メモリ枯渇が検出されたらウォッチドッグタイマーによるシステムリセットで自動回復させる設計を組み込みます。ログに残してから再起動し、原因調査できるようにします。
// ヒープ枯渇時の自動リセット
void heap_check_task(void *arg) {
while (1) {
if (xPortGetFreeHeapSize() < HEAP_CRITICAL_THRESHOLD) {
log_error("HEAP CRITICAL: system will reset");
// ウォッチドッグをキックしない → 自動リセット
while (1); // または NVIC_SystemReset();
}
watchdog_kick();
vTaskDelay(1000);
}
}
他技術との比較
C/C++ vs Rust のメモリ安全性
Rustは所有権(Ownership)・借用(Borrowing)・ライフタイム(Lifetime)のシステムによって、コンパイル時にメモリリーク・ダングリングポインタ・ダブルフリーを検出します。embedded-halクレートを使ったRust組み込み開発はメモリ安全性の面で優れていますが、C/C++のエコシステムと比較するとライブラリ・ツール・採用実績はまだ発展中です。
メモリリーク vs スタックオーバーフロー
| 症状 | メモリリーク | スタックオーバーフロー |
|---|---|---|
| 発生タイミング | 時間とともに徐々に | 深い関数呼び出し時に突然 |
| 原因 | heap の解放忘れ | stack の使い過ぎ |
| 検出 | ヒープ残量の監視 | スタックカナリア・MPU |
| 影響 | malloc失敗・OOM | データ破壊・クラッシュ |
ガベージコレクション vs 手動管理
Python・Java・Go等のGC言語はランタイムが自動的にメモリを解放します。組み込みではGCのポーズタイム(停止時間)とメモリオーバーヘッドが問題になることが多く、リアルタイム要件の厳しいシステムには不向きです。MicroPython等は軽量GCで組み込みへの適用を試みていますが、マイコンの制約内での使用可能なヒープ容量は数KB〜数百KB程度になります。