C나 C++ 처럼 손수 메모리 관리를 하다가 GC가 있는 Java를 사용하면 무척이나 편하다. 볼 일 없는 객체는 자동적으로 반환되기 때문에 메모리 관리가 필요하다는 사실을 망각하게 되는데 잘못된 습관이다.
아래 스택 코드에는 뚜렷하게 잘못된 부분이 없다. 아무리 많은 테스트를 하더라도 통과할 것이다 하지만 이 프로그램은 보이지 않는 문제가 있다.
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 | class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if (size == 0) { throw new EmptyStackException(); } return elements[--size]; } private void ensureCapacity() { if(elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } } |
위 Stack 클래스에는 메모리 누수가 있다. 그 결과로 GC가 해야할 일이 많아져서 성능이 저하되거나, 메모리 요구량이 증가할 것이다. 극단 적인 경우에는 디스크 페이징이 발생하고, 심지어는 OutOfMemoryError가 던저지면서 프로그램이 중단될 것이다.
메모리 누스는 어디에서 생길까? 스택이 줄어들면서 제거한 객체들을 쓰레기 수집기가 처리하지 못해서 생긴다. 스택을 사용하는 프로그램이 그 객체들을 더 이상 참조하지 않는데도 말이다. 스택이 그런 객체에 대한 만기 참조를 제거하지 않기 때문이다. 만기 참조란, 다시 이용되지 않을 참조를 말한다.
위의 스택에서 elements 배열에서 실제로 사용되는 부분을제외한 나머지 영역에 보관된 참조들이 만기 참조다. size 보다 작은 곳에 있는 요소들은 실제로 쓰이는 참조들이지만, 나머지 영역에 있는 참조들은 그렇지 않다. 자동적으로 쓰레기 객체를 수집하는 언어에서 발생하는 메모리 누수 문제는 찾아내기 어렵다. 실수로 객체 참조를 게속 유지하는 경우, 해당 객체만 쓰레기 수집에서 제외되는 것이 아니라 그 객체를 통해 참조되는 다른 객체들도 쓰레기 수집에서 제외된다. 따라서 만기 참조가 몇 개라도 있으면 굉장히 많은 객체가 쓰레기 수집에서 제외될 수 있다.
위의 스택을 간단히 고치려면 쓸 일 없는 객체 참조는 무조건 null로 만드는 것이다. Stack 클래스의 경우 pop을 했었을 때 pop된 객체에 대한 참조는 그 즉시 null 로 만들면 된다.
1 2 3 4 5 6 7 8 9 10 | public Object pop() { if (size == 0) { throw new EmptyStackException(); } Object data = element[--size]; elements[size] = null; //만기 참조 제거 return data; } |
이런 오류를 한번 겪으면 객체 사용이 끝나면 즉시 그 참조를 null 처리해야 한다는 강박관념에 사로잡히는 경우가 있다. 하지만 그럴경우 프로그램이 난잡해지기 때문에 옳은 방법은 아니다. 객체 참조를 null 처리하는 것은 규범이라기보단 예외적인 조치가 되어야 한다. 만기 참조를 제거하는 가장 좋은 방법은 해당 참조가 보관된 변수가 유효범위를 벗어나게 두는 것이다. 변수를 정의할 때 그 유효범위를 최대한 좁게 만들면 자연스럽게 해결된다.
일반적으로, 자체적으로 관리하는 메모리가 있는 클래스를 만들 때는 메모리 누수가 발생하지 않도록 주의해야 한다. 더 이상 사용되지 않은 객체 참조는 반드시 null로 바꿔 주어야 한다.
캐시도 메모리 누수가 흔히 발생하는 장소이고 또 흔히 발견되는 곳은 리스너 등의 역호출자(CallBack)이다.
메모리 누수는 보통 뚜렷한 오류로 이어지지 않기 때문에 수년간 시스템에 남아있는 경우가 있다. 이런 문제는 보통 주의 깊게 코드를 검토하다 발견되거나, 힙 프로파일러 같은 도구를 통해 검증하다가 발견된다. 따라서 이런 문제가 생길 수 있다는 것을 사전에 인지하고 방지 대첵을 세우는 것이 바람직하다.
'Effective Java > 1장 객체의 생성과 삭제' 카테고리의 다른 글
Effective Java #7 종료자 사용을 피하라 (0) | 2018.11.24 |
---|---|
Effective Java #5 불필요한 객체는 만들지 마라 (0) | 2018.10.20 |
Effective Java #4 객체 생성을 막을 때는 private 생성자를 사용하라 (0) | 2018.10.18 |
Effective Java #3 private 생성자나 enum 자료형은 싱글턴 패턴을 따르도록 설계하라 (0) | 2018.10.18 |
Effective Java #2 생성자 인자가 많을 때는 빌더 패턴 적용을 고려하라 (0) | 2018.10.17 |