출근길에 책을 하나 읽고 있는데 자바 메모리 누수 얘기가 나왔다. 그동안 자바는 가비지 컬렉터가 있으니 막연히 메모리 누수가 발생할 거라는 생각을 하지 않았다. 결론부터 말하면 발생할 수 있다. 이에 대한 이해가 필요한 것 같아 직접 테스트를 통해서 확인하고 싶은 마음이 생겼다. 책에 있는 예제를 참고하여 코딩해봤다.
public class MemoryLeakSample {
private static final SimpleStack<String> SIMPLE_STACK = new SimpleStack<>();
public static class SimpleStack<T> {
private final List<T> stack;
private int pointer = 0;
public SimpleStack() {
this.stack = new ArrayList<>();
pointer = 0;
}
public void push(T element) {
stack.add(pointer++, element);
}
public T pop() {
if (pointer > 0) {
return stack.get(--pointer);
}
return null;
}
}
}
SimpleStack
클래스는 데이터를 저장하고 삭제를 할 수 있는 아주 심플한 스택 클래스이다. 테스트를 해보면 정상적으로 동작하는 것을 확인할 수 있다. 문제는 20번째 줄에 있는 stack.get()
이다. List
에서 remove 하는 것이 아니라 get()
을 통해서 처리하고 있다.
pointer의 위치를 변경 했기 때문에 다시 push()
를 하면 새로운 element로 해당 pointer에 add()
를 하기 때문에 동작 자체는 이상 없다. 하지만 해당 객체의 참조 정보는 여전히 stack에 남아 있게 된다. 참조 정보가 남아 있다는 것은 GC의 대상이 아니라는 의미이다. SIMPLE_STACK이 GC 대상이 되지 않는 한 해당 정보는 사라지지 않는다. (static final인 SIMPLE_STACK이 애플리케이션이 종료되기 전에 GC 대상이 될 일은 없다.)
MemoryLeakSample
에 main()
메서드를 추가하여 실제 메모리 누수가 발생하는지 간단하게 테스트를 해보자. 코드를 간단하게 설명하면 SIMPLE_STACK에 문자열을 push()
하고 바로 pop()
을 호출한다. 등록/삭제를 100,000번 반복하는 것이다. 이 과정을 다시 100,000번 반복한다. Assert.isTrue를 통해서 실행 자체에 대한 이상 없음을 확인할 수 있다.
public static void main(String[] args) {
IntStream.rangeClosed(0, 99999)
.forEach(i -> IntStream.rangeClosed(0, 99999)
.forEach(j -> {
String value = "테스트 데이터입니다. 넘버: " + i + j;
SIMPLE_STACK.push(value);
Assert.isTrue(Objects.equals(value, SIMPLE_STACK.pop()), "ERROR");
}));
}
문제는 SIMPLE_STACK에서 remove 처리되지 못한 문자열의 참조 정보들은 GC 대상이 되지 못한 상태로 여전히 남아 있게 된다. 실제로 그런지 결과를 확인하기 위해 JVM option을 -Xms128m -Xmx128m로 지정하고 VisualVM으로 모니터링을 해봤다. 아래 그래프를 통해서 보듯이 일정 시간 이후에 OOM이 발생하면서 애플리케이션이 종료하게 된다.
Heap Dump를 통해서 String 객체가 압도적으로 많이 생성될 걸 볼 수 있는데 모두 SIMPLE_STACK에 있음을 확인할 수 있다.
이제 stack.get()
을 stack.remove()
로 변경하고 다시 테스트를 진행해보자. 아래 그래프 보듯이 OOM 발생 없이 정상적으로 실행되는 것 을 알 수 있다.
궁금해서 ArrayList
의 remove()
메서드를 보니 명시적으로 null을 지정한다. (clear to let GC to its work) 그렇다고 개발할 때 모든 경우에 null을 명시적으로 지정해야 GC 대상이 되는건 아니다. 오해하지 말자.
그동안 자바 메모리 누수에 대해 생각해 본 적이 없었는데 이번 기회에 직접 테스트를 통해서 결과를 확인하니 이해하는데 많은 도움이 되는 것 같다.
'Dev > Java' 카테고리의 다른 글
ConcurrentHashMap은 Client lock이 안된다. (0) | 2017.06.15 |
---|---|
멀티 스레드에서 synchronized가 필요한 경우 (0) | 2017.04.13 |
CompletableFuture에 관해서 (0) | 2017.02.27 |
Collector 인터페이스 (0) | 2017.02.14 |