| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 정렬
- 메모리관리
- 자바개발
- 프로그래머스
- 개발공부
- 멀티스레드
- 자바프로그래밍
- 알고리즘공부
- 자바
- 자바공부
- HashMap
- 예외처리
- 코딩테스트준비
- 코딩테스트팁
- 코딩테스트
- 백준
- 코딩공부
- 알고리즘
- 개발자팁
- 프로그래밍기초
- 코딩인터뷰
- 자료구조
- 객체지향
- 파이썬
- Java
- 클린코드
- 가비지컬렉션
- JVM
- 자바기초
- 개발자취업
- Today
- Total
코드 한 줄의 기록
자바 메모리 누수(Memory Leak) 원인과 해결법 완벽 가이드 본문
자바 개발을 하다 보면 어느 순간 애플리케이션이 느려지거나 OutOfMemoryError가 발생하는 경험을 하게 됩니다. 저 역시 프로젝트를 진행하면서 이런 문제들을 겪으며 메모리 관리의 중요성을 깨달았습니다. 오늘은 자바에서 발생하는 메모리 누수의 원인을 파악하고, 이를 방지하는 실전 방법들을 함께 살펴보겠습니다.
메모리 누수란 무엇인가?
메모리 누수는 프로그램이 동적으로 할당한 메모리를 더 이상 사용하지 않음에도 불구하고 해제하지 않아 사용 가능한 메모리가 점점 줄어드는 현상입니다. 자바는 가비지 컬렉션(GC)을 통해 자동으로 메모리 관리를 해주지만, 개발자의 실수로 인해 여전히 메모리 누수가 발생할 수 있습니다.
자바에서 메모리 누수는 더 이상 사용되지 않는 객체들이 가비지 컬렉터에 의해 회수되지 않고 계속 누적되는 현상을 말합니다. 이러한 객체들이 Old 영역에 계속 쌓이면서 Major GC가 빈번하게 발생하고, 결국 애플리케이션의 응답속도가 늦어져 성능 저하를 불러옵니다.
메모리 누수가 발생하는 주요 원인들
- Static 변수의 무분별한 사용
가장 흔한 메모리 누수 원인 중 하나는 static 변수입니다. Static 변수는 클래스가 로드될 때 생성되어 JVM이 종료될 때까지 메서드 영역에 남아있기 때문에, static 변수가 참조하는 객체는 가비지 컬렉션의 대상이 되지 않습니다.
위 코드에서public class StaticMemoryLeak { public static List<String> staticList = new ArrayList<>(); public void addData() { for (int i = 0; i < 1000000; i++) { staticList.add("데이터 " + i); } } }staticList는 애플리케이션이 종료될 때까지 메모리에 남아있어 메모리 누수를 발생시킵니다. - 장기간 생존하는 컬렉션 객체
HashMap이나 ArrayList와 같은 컬렉션에 객체를 계속 추가하면서 사용하지 않는 객체를 제거하지 않는 경우 메모리 누수가 발생합니다. 특히 캐시로 사용되는 컬렉션에서 자주 발생하는 문제입니다.public class CacheService { private Map<String, Object> cache = new HashMap<>(); public void addToCache(String key, Object value) { cache.put(key, value); // 계속 추가만 하고 제거는 안 함 } } - 리스너와 콜백의 미해제
이벤트 리스너나 콜백 객체를 등록한 후 적절히 해제하지 않으면 해당 객체들이 메모리에서 해제되지 않습니다. 이는 특히 GUI 애플리케이션에서 자주 발생하는 문제입니다.public class EventManager { private List<EventListener> listeners = new ArrayList<>(); public void addEventListener(EventListener listener) { listeners.add(listener); // 리스너 해제 메서드가 없음! } } - 리소스 미해제
파일 스트림, 데이터베이스 연결, 네트워크 소켓 등의 리소스를 사용 후 적절히 닫지 않으면 메모리 누수가 발생합니다. Java 7 이전 버전에서는 이런 문제가 특히 많이 발생했습니다.// 잘못된 예시 public String readFile(String filePath) throws IOException { FileInputStream fis = new FileInputStream(filePath); // fis.close()를 호출하지 않음! return "내용"; } - Inner Class의 잘못된 사용
비정적 내부 클래스는 항상 외부 클래스에 대한 참조를 가지고 있어, 내부 클래스 객체가 존재하는 한 외부 클래스도 가비지 컬렉션되지 않습니다.public class OuterClass { private String data = "큰 데이터"; // 비정적 내부 클래스 - 문제 발생 가능 class InnerClass { public void doSomething() { // 외부 클래스 참조 유지 } } }
메모리 누수 진단 방법
- 프로파일링 도구 활용
VisualVM, Eclipse Memory Analyzer(MAT) 등 메모리 사용량을 실시간으로 모니터링하고 힙 덤프 분석이 가능한 도구를 활용할 수 있습니다.visualvm --jdkhome "/path/to/jdk"
힙 덤프는jmap -dump:format=b,file=heap.hprof <pid>형식으로 생성하며, JVM 옵션-XX:+HeapDumpOnOutOfMemoryError로 자동 생성도 가능합니다.
GC 로그도 함께 분석하면 원인 파악에 도움이 됩니다.
메모리 누수 방지 방법
- 참조 관리 개선
불필요한 강한 참조 대신 WeakReference, WeakHashMap 등을 활용하여 꼭 필요한 객체만 유지해줍니다.
더 이상 사용하지 않는 객체는public class ImprovedCacheService { private Map<String, WeakReference<Object>> cache = new WeakHashMap<>(); public void addToCache(String key, Object value) { cache.put(key, new WeakReference<>(value)); } public Object getFromCache(String key) { WeakReference<Object> ref = cache.get(key); if (ref != null) { Object value = ref.get(); if (value == null) { cache.remove(key); // 참조된 객체가 GC됨 } return value; } return null; } }null로 참조 해제하거나, Scope에서 벗어나게 만들어야 합니다. - 리소스 관리 개선
try-with-resources를 도입하면 close() 누락 걱정 없이 안전하게 리소스를 관리할 수 있습니다.public String readFile(String filePath) throws IOException { try (BufferedReader reader = Files.newBufferedReader(Paths.get(filePath))) { return reader.lines().collect(Collectors.joining("\n")); } // reader가 자동으로 close됨 } - 컬렉션 크기 제한
LRU(Least Recently Used) 캐시 등, 크기가 제한된 컬렉션을 직접 구현하여 무분별한 메모리 사용을 막을 수 있습니다.public class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int maxSize; public LRUCache(int maxSize) { super(16, 0.75f, true); this.maxSize = maxSize; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > maxSize; } } - 정적 변수 사용 최소화
꼭 필요한 경우가 아니면 static 변수 사용을 피하고, 종료 시점 또는 사용이 끝난 뒤null로 지정하여 메모리 회수를 유도합니다. - 리스너 관리
리스너나 콜백 등록 시 반드시 해제 메서드도 구현해, 불필요한 참조를 제거합니다.public class EventManager { private final Set<EventListener> listeners = new CopyOnWriteArraySet<>(); public void addEventListener(EventListener listener) { listeners.add(listener); } public void removeEventListener(EventListener listener) { listeners.remove(listener); } public void clearAllListeners() { listeners.clear(); } }
GC 최적화를 통한 성능 개선
- JVM 옵션 튜닝 및 객체 생성 최소화
불필요한 객체 생성을 최소화하면 GC 효율이 향상됩니다.# 힙 메모리 크기 설정 -Xms2g -Xmx4g # GC 종류와 옵션 -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+PrintGC -XX:+PrintGCDetails
메모리 누수 모니터링 및 예방
- 지속적인 모니터링
실서비스에서는 메모리 사용률, GC 발생 빈도 및 시간, Old Generation 패턴, OutOfMemoryError 발생 여부 등을 꾸준히 모니터링합니다. - 코드 리뷰 체크리스트
- 리소스 해제: try-with-resources 활용 또는 finally 블록 체크
- 컬렉션 관리: 무제한 증가 가능성 확인
- 리스너: 등록 해제 관리 여부
- 정적 변수: 대용량 객체 참조 유무
- 내부 클래스: 비정적 내부 클래스 사용 체크
실전 디버깅 사례
실제 프로젝트에서 메모리 누수를 해결한 경험을 공유합니다.
어떤 웹 애플리케이션에서 시간이 지날수록 메모리 사용량이 계속 증가해 OutOfMemoryError가 발생했습니다.
VisualVM으로 힙 덤프를 분석해보니, 세션 정보를 static Map에 쌓아두고 만료된 세션을 제거하지 않아 누적되던 문제였습니다.
아래처럼 주기적으로 세션을 정리하도록 개선해 해결할 수 있었습니다.
// 개선된 세션관리 예시
public class SessionManager {
private final Map<String, UserSession> sessions = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public SessionManager() {
// 주기적으로 만료된 세션 정리
scheduler.scheduleAtFixedRate(this::cleanupExpiredSessions, 5, 5, TimeUnit.MINUTES);
}
private void cleanupExpiredSessions() {
long currentTime = System.currentTimeMillis();
sessions.entrySet().removeIf(entry ->
currentTime - entry.getValue().getLastAccessTime() > 30 * 60 * 1000);
}
}
자바 메모리 누수는 애플리케이션의 안정성과 성능에 직접적으로 영향을 미치는 중요한 문제입니다. 가비지 컬렉션이 자동으로 메모리를 관리해주지만, 개발자의 세심한 주의가 여전히 필요합니다.
리소스 해제, 컬렉션 관리, 정적 변수 최소화, 적절한 참조 타입(WeakReference 등) 활용, 지속 모니터링을 명심하고, 코드와 인프라를 체계적으로 관리하면 더 성능 좋은 안정적인 자바 애플리케이션을 개발할 수 있습니다.
자바 객체지향 설계를 망치는 7가지 이해 부족 실수와 올바른 접근법
객체지향 프로그래밍(OOP)은 자바의 핵심 철학입니다. 하지만 객체지향 개념을 제대로 이해하지 못하면 코드가 지저분해지고 유지보수가 어려워집니다. 이 글에서는 자바 개발자가 흔히 저지르
byteandbit.tistory.com
'JAVA' 카테고리의 다른 글
| 초보자를 위한 Java PATH/JAVA_HOME 설정과 첫 번째 Hello World 실행하기 (Maven/Gradle 없이) (0) | 2025.09.16 |
|---|---|
| 자바 개발 완전정복! JDK/JRE/JVM 개념 차이부터 설치까지 한 번에 끝내기 (0) | 2025.09.15 |
| 자바 객체지향 설계를 망치는 7가지 이해 부족 실수와 올바른 접근법 (0) | 2025.09.12 |
| Java 개발자를 위한 필수 가이드: main 메소드 의존 습관을 버리고 깔끔한 코드로 나아가는 길 (0) | 2025.09.11 |
| 자바 변수 초기화와 스코프(Scope) 완벽 정리: 실무에서 놓치기 쉬운 함정들 (0) | 2025.09.10 |