1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「メモリリークって何?」そのコード、数時間後に爆発しませんか?

Posted at

はじめに

「アプリを起動してしばらくすると、だんだん動作が重くなってくる…」
「サーバーが突然クラッシュして、再起動すると直る…」

こうした症状の背後には、メモリリークが潜んでいる可能性があります。

image.png

この記事では、メモリリークとは何か、なぜ問題になるのか、どのように対策すればよいのかを、具体例を交えながら解説します。

この記事の対象読者

  • プログラミングを始めて間もない方
  • メモリリークという言葉は聞いたことがあるが、詳しくは知らない方
  • アプリのパフォーマンス問題に直面している方

メモリリークとは?

メモリリークとは、プログラムが使用したメモリを適切に解放せず、使用可能なメモリが徐々に減少していく現象のことです。

メモリの基本的な仕組み

プログラムが動作する際、以下のようなプロセスでメモリを使用します。

通常、プログラムは次のサイクルを繰り返します。

  1. メモリ確保: データを保存するために必要なメモリ領域を確保する
  2. データ使用: 確保したメモリにデータを書き込み、読み取る
  3. メモリ解放: 不要になったメモリを解放し、他の用途に使えるようにする

メモリリークは、この 3番目のステップ「メモリ解放」が適切に行われない ときに発生します。

具体的な例で理解する

「図書館で本を借りる」場面で例えてみましょう。

  • 正常な動作: 本を借りる → 読む → 返却する(繰り返し可能)
  • メモリリーク: 本を借りる → 読む → 返却しない → また借りる → 読む → 返却しない → …

この状態が続くと、図書館の本がどんどん減っていき、最終的には貸し出せる本がなくなってしまいますね。これがメモリリークの状態です。

なぜメモリリークが問題になるのか

メモリリークは、以下のような深刻な問題を引き起こします。

1. パフォーマンスの低下

使用可能なメモリが減少すると、プログラムの動作が遅くなります。

  • アプリの起動時間が長くなる
  • 操作のレスポンスが悪くなる
  • UIがカクカクする

2. システムクラッシュ

メモリが枯渇すると、プログラムやシステム全体が予期せず終了してしまいます。

  • アプリが突然落ちる
  • サーバーがダウンする
  • 「Out of Memory」エラーが発生する

3. 他のプロセスへの影響

メモリは限られたリソースのため、一つのプログラムがメモリを占有し続けると、他のプログラムが使えるメモリが減少します。

4. 本番環境での障害

特にサーバー側のアプリケーションでメモリリークが発生すると、長時間稼働する中で徐々にメモリが消費され、最終的にサービス全体がダウンする可能性があります。

メモリリークが発生する原因とパターン

メモリリークは、プログラミング言語や環境によって異なる原因で発生します。代表的なパターンを見ていきましょう。

パターン1: イベントリスナーの解放忘れ

イベントリスナーを登録したまま削除を忘れると、不要なオブジェクトがメモリに残り続けます。

問題のあるコード例(JavaScript)

class ComponentWithLeak {
  constructor() {
    this.data = new Array(1000000); // 大きなデータ
    
    // イベントリスナーを登録
    window.addEventListener('resize', this.handleResize.bind(this));
  }
  
  handleResize() {
    console.log('Window resized');
  }
  
  // コンポーネントが不要になっても、イベントリスナーが残り続ける
}

// 新しいインスタンスを作るたびにメモリリークが発生
const component1 = new ComponentWithLeak();
const component2 = new ComponentWithLeak();
// component1, component2 が不要になってもメモリが解放されない

改善したコード例

class ComponentWithoutLeak {
  constructor() {
    this.data = new Array(1000000);
    this.handleResize = this.handleResize.bind(this);
    window.addEventListener('resize', this.handleResize);
  }
  
  handleResize() {
    console.log('Window resized');
  }
  
  // クリーンアップメソッドを追加
  destroy() {
    window.removeEventListener('resize', this.handleResize);
    this.data = null;
  }
}

const component = new ComponentWithoutLeak();
// 使い終わったら明示的にクリーンアップ
component.destroy();

パターン2: タイマーの解放忘れ

setIntervalsetTimeout で設定したタイマーを適切にクリアしないと、メモリリークが発生します。

問題のあるコード例(JavaScript)

class DataFetcher {
  constructor() {
    this.cache = [];
    
    // 1秒ごとにデータを取得
    setInterval(() => {
      this.cache.push(this.fetchData());
    }, 1000);
  }
  
  fetchData() {
    return { timestamp: Date.now(), data: new Array(10000) };
  }
}

const fetcher = new DataFetcher();
// fetcherを使い終わっても、setIntervalは動き続け、cacheが増え続ける

改善したコード例

class DataFetcher {
  constructor() {
    this.cache = [];
    this.timerId = null;
  }
  
  start() {
    this.timerId = setInterval(() => {
      this.cache.push(this.fetchData());
    }, 1000);
  }
  
  fetchData() {
    return { timestamp: Date.now(), data: new Array(10000) };
  }
  
  stop() {
    if (this.timerId) {
      clearInterval(this.timerId);
      this.timerId = null;
    }
    this.cache = [];
  }
}

const fetcher = new DataFetcher();
fetcher.start();
// 使い終わったら必ずstopを呼ぶ
fetcher.stop();

パターン3: グローバル変数への参照蓄積

グローバル変数やキャッシュに、データを追加し続けて削除しない場合もメモリリークになります。

問題のあるコード例(Python)

# グローバルなキャッシュ
user_cache = {}

def get_user(user_id):
    if user_id not in user_cache:
        # ユーザー情報を取得してキャッシュに保存
        user_cache[user_id] = fetch_user_from_db(user_id)
    return user_cache[user_id]

# ユーザーIDが増え続けると、キャッシュが肥大化
for i in range(1000000):
    get_user(i)  # user_cacheに100万件のデータが蓄積

改善したコード例

from collections import OrderedDict

class LRUCache:
    def __init__(self, max_size=1000):
        self.cache = OrderedDict()
        self.max_size = max_size
    
    def get(self, user_id):
        if user_id not in self.cache:
            # 新しいデータを追加
            self.cache[user_id] = fetch_user_from_db(user_id)
            
            # キャッシュサイズを制限
            if len(self.cache) > self.max_size:
                # 最も古いエントリを削除
                self.cache.popitem(last=False)
        
        # アクセスしたアイテムを末尾に移動
        self.cache.move_to_end(user_id)
        return self.cache[user_id]

# サイズ制限付きキャッシュを使用
user_cache = LRUCache(max_size=1000)

パターン4: 循環参照(C/C++など)

手動でメモリ管理を行う言語では、循環参照によってメモリが解放されないことがあります。

パターン5: クロージャによる予期しない参照保持

クロージャが外部のスコープの変数を参照し続けることで、メモリリークが発生することがあります。

問題のあるコード例(JavaScript)

function createHandlers() {
  const largeData = new Array(1000000).fill('data');
  
  return {
    // この関数はlargeDataへの参照を保持し続ける
    onClick: function() {
      console.log('Clicked');
      // largeDataは使っていないが、クロージャで参照が保持される
    }
  };
}

const handlers = createHandlers();
// handlersを保持している限り、largeDataもメモリに残る

改善したコード例

function createHandlers() {
  const largeData = new Array(1000000).fill('data');
  
  // 必要な処理だけを別の関数に分離
  function processData() {
    // largeDataを使った処理
    return largeData.length;
  }
  
  // 処理結果だけを保持
  const result = processData();
  
  return {
    onClick: function() {
      console.log('Clicked', result);
      // largeDataへの参照は保持されない
    }
  };
}

メモリリークの検出方法

メモリリークを早期に発見することが重要です。各環境で使える検出ツールを紹介します。

ブラウザ環境(JavaScript)

Chrome DevTools のメモリプロファイラ

  1. Chrome DevToolsを開く(F12)
  2. 「Memory」タブを選択
  3. 「Heap snapshot」を取得
  4. 操作を繰り返す
  5. 再度「Heap snapshot」を取得
  6. スナップショットを比較して、メモリ使用量の増加を確認

メモリタイムラインの確認

  • Performance タブで録画を開始
  • アプリを操作
  • メモリ使用量のグラフを確認
  • 右肩上がりに増え続けている場合はメモリリークの可能性

Node.js環境

process.memoryUsage()を使った監視

// メモリ使用量を定期的にログ出力
setInterval(() => {
  const usage = process.memoryUsage();
  console.log({
    rss: `${Math.round(usage.rss / 1024 / 1024)} MB`,
    heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)} MB`,
    heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)} MB`,
    external: `${Math.round(usage.external / 1024 / 1024)} MB`
  });
}, 5000);

Node.jsのメモリプロファイラ

# --inspect フラグでデバッガを有効化
node --inspect app.js

# Chrome DevToolsで chrome://inspect にアクセスして分析

Python環境

memory_profilerを使った分析

pip install memory_profiler
from memory_profiler import profile

@profile
def my_function():
    a = [1] * (10 ** 6)
    b = [2] * (2 * 10 ** 7)
    del b
    return a

if __name__ == '__main__':
    my_function()

実行すると、行ごとのメモリ使用量が表示されます。

python -m memory_profiler script.py

tracemalloc(Python標準ライブラリ)

import tracemalloc

# トレース開始
tracemalloc.start()

# メモリを消費する処理
data = []
for i in range(100000):
    data.append({'id': i, 'value': i * 2})

# 現在のメモリ使用量を取得
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory: {current / 10**6:.2f} MB")
print(f"Peak memory: {peak / 10**6:.2f} MB")

# トレース終了
tracemalloc.stop()

メモリリークを防ぐベストプラクティス

メモリリークを未然に防ぐための実践的なテクニックを紹介します。

1. リソースのライフサイクルを明確にする

リソースの作成と破棄をペアで考える習慣をつけましょう。

// ✅ 良い例:ライフサイクルが明確
class DataService {
  constructor() {
    this.connection = null;
  }
  
  async connect() {
    this.connection = await createConnection();
  }
  
  async disconnect() {
    if (this.connection) {
      await this.connection.close();
      this.connection = null;
    }
  }
}

// 使用例
const service = new DataService();
await service.connect();
try {
  // データ処理
} finally {
  await service.disconnect(); // 必ず解放
}

2. 自動クリーンアップを活用する

言語やフレームワークが提供する自動クリーンアップ機能を活用しましょう。

React の useEffect クリーンアップ

import { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // セットアップ
    const timer = setInterval(() => {
      console.log('tick');
    }, 1000);
    
    // クリーンアップ関数を返す
    return () => {
      clearInterval(timer);
    };
  }, []);
  
  return <div>My Component</div>;
}

Pythonのコンテキストマネージャ

class DatabaseConnection:
    def __enter__(self):
        self.conn = create_connection()
        return self.conn
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        # 自動的にクリーンアップ
        self.conn.close()

# with文を使うと自動的に__exit__が呼ばれる
with DatabaseConnection() as conn:
    conn.execute("SELECT * FROM users")
# ここでconnは自動的にクローズされる

3. データのサイズに制限を設ける

キャッシュやバッファには必ず上限を設定しましょう。

class LimitedCache {
  constructor(maxSize = 100) {
    this.cache = new Map();
    this.maxSize = maxSize;
  }
  
  set(key, value) {
    // サイズ制限チェック
    if (this.cache.size >= this.maxSize) {
      // 最も古いエントリを削除(FIFO)
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    
    this.cache.set(key, value);
  }
  
  get(key) {
    return this.cache.get(key);
  }
}

4. 定期的なメモリ監視を実装する

本番環境でもメモリ使用量を監視し、異常を早期に検出しましょう。

// Node.js での例
class MemoryMonitor {
  constructor(threshold = 500 * 1024 * 1024) { // 500MB
    this.threshold = threshold;
    this.checkInterval = 60000; // 1分
  }
  
  start() {
    this.timerId = setInterval(() => {
      const usage = process.memoryUsage();
      
      if (usage.heapUsed > this.threshold) {
        console.error('Memory usage exceeded threshold', {
          heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)} MB`,
          threshold: `${Math.round(this.threshold / 1024 / 1024)} MB`
        });
        
        // アラート送信などの処理
        this.sendAlert(usage);
      }
    }, this.checkInterval);
  }
  
  stop() {
    if (this.timerId) {
      clearInterval(this.timerId);
    }
  }
  
  sendAlert(usage) {
    // 監視システムへ通知
  }
}

const monitor = new MemoryMonitor();
monitor.start();

5. コードレビューでチェックする

メモリリークのリスクが高い箇所をレビュー時に重点的にチェックしましょう。

チェックリスト

  • イベントリスナーの登録と解除がペアになっているか
  • タイマー(setInterval/setTimeout)が適切にクリアされているか
  • グローバル変数やキャッシュにサイズ制限があるか
  • クロージャが不要な大きなオブジェクトを参照していないか
  • リソース(ファイル、DB接続など)が適切に解放されているか
  • 循環参照の可能性がある箇所で弱参照を使用しているか

まとめ

メモリリークは、適切に対処しないと深刻なパフォーマンス問題やシステムクラッシュを引き起こします。

重要なポイント

  1. メモリリークとは: 使用したメモリを適切に解放せず、使用可能なメモリが減少する現象
  2. 主な原因: イベントリスナーの解放忘れ、タイマーのクリア忘れ、グローバル変数への蓄積、循環参照など
  3. 検出方法: ブラウザのDevTools、memory_profiler、Valgrindなどのツールを活用
  4. 予防策: リソースのライフサイクル管理、自動クリーンアップの活用、弱参照の使用、サイズ制限の設定

日々の開発で意識すること

メモリリークを防ぐためには、日頃から以下を心がけましょう。

  • リソースの作成と破棄を常にペアで考える
  • 定期的にメモリプロファイリングを実施する
  • コードレビューでメモリ管理を確認する
  • メモリ使用量の監視を実装する

メモリリークは一度発生すると原因特定が難しいことがありますが、この記事で紹介したパターンと対策を知っておくことで、多くの問題を未然に防ぐことができるでしょう。

参考リンク


※この記事は2025年12月時点の情報に基づいています。ツールや手法は更新される可能性がありますので、最新の公式ドキュメントもご確認ください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?