출근길에 책을 하나 읽고 있는데 자바 메모리 누수 얘기가 나왔다. 그동안 자바는 가비지 컬렉터가 있으니 막연히 메모리 누수가 발생할 거라는 생각을 하지 않았다. 결론부터 말하면 발생할 수 있다. 이에 대한 이해가 필요한 것 같아 직접 테스트를 통해서 확인하고 싶은 마음이 생겼다. 책에 있는 예제를 참고하여 코딩해봤다.

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 대상이 될 일은 없다.) 

MemoryLeakSamplemain() 메서드를 추가하여 실제 메모리 누수가 발생하는지 간단하게 테스트를 해보자. 코드를 간단하게 설명하면 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 발생 없이 정상적으로 실행되는 것 을 알 수 있다.


궁금해서 ArrayListremove() 메서드를 보니 명시적으로 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

+ Recent posts