MENU

Javaメモリモデルを完全理解!メモリ管理のすべてが分かる解説記事

こんにちは!バックエンドエンジニアのキョンです。今回は、開発現場でよく混乱する「Javaメモリモデル(JMM)」について、実務経験を交えながら詳しく解説していきます。

目次

1. Javaメモリモデルの基礎

そもそもメモリモデルって?

入社当時、私も「メモリモデルって何?」って感じでした。簡単に言うと、「Javaがメモリをどう使うのか」というルールブックみたいなものです。

メモリモデルの重要性

// メモリの可視性問題の例
public class VisibilityProblem {
    private boolean flag = false;  // 共有変数
    
    // スレッド1で実行
    public void writerThread() {
        // 他の処理
        flag = true;  // フラグを更新
    }
    
    // スレッド2で実行
    public void readerThread() {
        while (!flag) {  // フラグが更新されたのに気づかない可能性がある
            // 待機処理
        }
    }
}

2. メモリ領域の種類と役割

ヒープ領域

オブジェクトが格納される場所です:

public class HeapExample {
    public static void main(String[] args) {
        // ヒープにオブジェクトが作られる
        List<String> list = new ArrayList<>();
        list.add("こんにちは");  // この文字列もヒープに
        
        // 大量のオブジェクトを作る例
        for (int i = 0; i < 10000; i++) {
            list.add("データ" + i);  // ヒープを消費
        }
    }
}

スタック領域

public class StackExample {
    public void methodA() {
        int x = 10;  // スタックに保存
        String str = "ローカル変数";  // 参照はスタック、実体はヒープ
        methodB(x);  // xの値がスタックにコピーされる
    }
    
    private void methodB(int param) {
        int y = param + 5;  // 新しいスタックフレームで変数を管理
    }
}

3. スレッド間のメモリ共有

volatileの正しい使い方

実務でよく使用するパターンです:

public class SharedResource {
    private volatile boolean initialized = false;
    private Map<String, String> configData;
    
    public void init() {
        if (!initialized) {
            synchronized (this) {
                if (!initialized) {
                    configData = loadConfig();  // 設定読み込み
                    initialized = true;  // volatileで可視性を保証
                }
            }
        }
    }
    
    private Map<String, String> loadConfig() {
        // 設定ファイルの読み込み処理
        return new HashMap<>();
    }
}

スレッドローカル変数の活用

public class UserContext {
    private static final ThreadLocal<User> currentUser = 
        new ThreadLocal<>();
    
    public static void setUser(User user) {
        currentUser.set(user);
    }
    
    public static User getUser() {
        return currentUser.get();
    }
    
    public static void clear() {
        currentUser.remove();  // メモリリーク防止
    }
}

4. 実践的なメモリ管理

キャッシュの適切な実装

実際のプロジェクトで使用している例です:

public class CacheManager {
    private static final int MAX_ENTRIES = 1000;
    private static final Map<String, SoftReference<CacheEntry>> cache = 
        new ConcurrentHashMap<>();
    
    public void put(String key, Object value) {
        if (cache.size() >= MAX_ENTRIES) {
            cleanup();  // 古いエントリの削除
        }
        cache.put(key, new SoftReference<>(
            new CacheEntry(value, System.currentTimeMillis())
        ));
    }
    
    private void cleanup() {
        long now = System.currentTimeMillis();
        cache.entrySet().removeIf(entry -> 
            entry.getValue().get() == null || 
            entry.getValue().get().isExpired(now)
        );
    }
}

メモリリークの防止

public class ResourceHandler implements AutoCloseable {
    private final List<Resource> resources = new ArrayList<>();
    private final ExecutorService executor = 
        Executors.newFixedThreadPool(5);
    
    public void processData(Data data) {
        Resource resource = new Resource();
        resources.add(resource);
        
        executor.submit(() -> {
            try {
                resource.process(data);
            } finally {
                resources.remove(resource);
                resource.close();
            }
        });
    }
    
    @Override
    public void close() {
        executor.shutdown();
        resources.forEach(Resource::close);
        resources.clear();
    }
}

5. よくあるメモリ問題と対策

メモリリーク対策

// 悪い例
public class EventManager {
    private static List<EventListener> listeners = new ArrayList<>();
    
    public void addListener(EventListener listener) {
        listeners.add(listener);  // リスナーが永遠に解放されない
    }
}

// 良い例
public class ImprovedEventManager {
    private static List<WeakReference<EventListener>> listeners = 
        new ArrayList<>();
    
    public void addListener(EventListener listener) {
        listeners.add(new WeakReference<>(listener));
        cleanupListeners();  // 不要なリスナーの削除
    }
    
    private void cleanupListeners() {
        listeners.removeIf(ref -> ref.get() == null);
    }
}

OutOfMemoryError対策

public class LargeDataProcessor {
    public void processLargeFile(String filePath) {
        try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
            lines.filter(line -> !line.isEmpty())
                 .map(this::processLine)
                 .forEach(this::saveResult);
        } catch (IOException e) {
            logger.error("ファイル処理エラー", e);
        }
    }
}

まとめ

Javaのメモリモデルは複雑に見えますが、基本的な考え方を押さえれば怖くありません。

私の経験から、以下のポイントを意識すると良いでしょう:

  1. メモリの可視性を意識した実装
  2. 適切なリソース解放
  3. スレッドセーフなコーディング
  4. メモリリークの予防

さらなる学習のために

  • Java Language Specification
  • Java Virtual Machine Specification
  • Java Performance Tuning Guide

次回は「Javaのパフォーマンスチューニング」について解説する予定です。お楽しみに!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次