こんにちは!バックエンドエンジニアのキョンです。今回は、開発現場でよく混乱する「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のメモリモデルは複雑に見えますが、基本的な考え方を押さえれば怖くありません。
私の経験から、以下のポイントを意識すると良いでしょう:
- メモリの可視性を意識した実装
- 適切なリソース解放
- スレッドセーフなコーディング
- メモリリークの予防
さらなる学習のために
- Java Language Specification
- Java Virtual Machine Specification
- Java Performance Tuning Guide
次回は「Javaのパフォーマンスチューニング」について解説する予定です。お楽しみに!
コメント