자바를 사용하면서 GC(가비지 컬렉터)가 있기 때문에 메모리 관리에 소홀히 한 것은 사실이다. 그런데, 자바에서도 메모리 누수가 발생할 수 있으며, 다른 언어와 마찬가지로 메모리 관리에 신경을 써야한다고 한다.
사실 자바의 컴파일 방식이나 JVM, jdk, jre, 가비지 컬렉터의 동작도 한 번 정리해보려고 한다. 백기선 님의 자바 스터디에서 정리한 글들을 본 적이 있긴 한데 꽤나 어렵고 중요한 내용들이 많았다.
아래의 코드를 보고 메모리 누수가 어디서 일어나는 지 알아보자.
public 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);
}
}
`메모리 누수` 문제로 인해 위의 스택을 사용하는 프로그램은 오래 사용하면
- 가비지 컬렉션 활동과 메모리 사용량이 늘어나 성능이 저하될 것
- 드문 경우긴 하지만 디스크 페이징이나 OutOfMemoryError을 일으켜 프로그램이 종료될 수 있음
`메모리 누수`는 어디서 일어나는가?
스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다.
스택이 사용했던 객체들에 대해 다쓴 참조(obsolete reference)를 가지고 있기 때문
- 다 쓴 참조
- 다시 쓰지 않을 참조를 뜻 함.
- elements 배열에서 size가 줄어들면서 현재 size 밖에 있던 참조들(이를 `활성 영역`밖의 참조라고함)
- 가비지 컬렉션 언어에서는 객체 참조를 하나 살려두면 그 객체뿐 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못한다.
- 객체가 참조하는 객체, 또 그 객체가 참조하는 객체 ... chain처럼 이어질 수 있음
- 단 몇 개의 객체가 매우 많은 객체를 회수하지 못하게 할 수 있고 잠재적으로 성능에 악영향을 줄 수 있다
-> 해당 참조를 다 썼을 때 null 처리(참조 해제)하면 해결할 수 있다. (간단)
// 제대로 구현한 pop 메서드
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
pop 해서 스택에서 꺼낼 때 더 이상 참조가 필요 없어지기 때문에 이때 참조하던 부분을 null을 넣어주면 된다!
null 처리를 하면 따라오는 이점도 존재한다
- null 처리한 참조를 실수로 사용하려 하면 프로그램은 즉시 NPE를 던지며 종료
- 만약 null 처리를 하지 않았다면 잘못 객체를 참조하여 프로그램적으로 오류가 발생할 수 있음
모든 객체를 다 쓰자마자 일일이 null 처리하는 것은 프로그램을 필요 이상으로 지저분하게 만들며 바람직하지 않다
객체 참조를 null 처리하는 일은 예외적인 경우여야 한다
null 처리를 해야 하는 경우?
- 위의 스택처럼 자기 메모리를 직접 관리하는 경우
- `활성 영역`에 속한 원소들이 사용되고, `비활성 영역`에 속한 원소들은 사용되지 않을 때 가비지 컬렉터는 이러한 구분을 하지 못하고 오직 프로그래머만 구분이 가능하기 때문에 비활성 영역이 되는 순간 null 처리해서 해당 객체를 더 이상 쓰지 않을 것임을 가비지 컬렉터에 알려야 한다
하지만 위에서 말했던 것처럼 객체 참조를 null로 처리하는 일은 예외적인 경우여야만 한다
다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것!
Object pop() {
Object age = 24;
...
age = null; // null처리를 할 필요 없이 알아서 처리됨
}
age라는 Object는 pop() 메서드 안에서만 형성되어 있기 때문에 메서드가 끝나면(scope) 알아서 가비지 컬렉션에 의해 정리되어 사라진다.
즉, 메모리를 직접 관리하는 클래스는 메모리 누수를 조심해야 하며, 객체 참조를 null로 처리하는 것보다 유효 범위 밖으로 밀어내는 것으로 생각하는 게 효율적이고 코드도 깔끔하다!
캐시, 메모리 누수의 주범
객체 참조를 캐시에 넣고 객체를 다 쓴 뒤로도 한참을 자주 놔두는 일은 자주 접할 수 있다.
사용했던 시간을 체크해서 캐시를 비워낼 수 있지만, 유효 기간을 정의하기가 어렵기 때문에 시간이 지날수록 캐시에 넣어둔 엔트리의 가치를 떨어뜨리는 방식을 흔히 사용
캐시 구현의 안 좋은 예 - 객체를 다 쓴 뒤로도 엔트리를 정리하지 않음
public class CacheSample {
public static void main(String[] args) {
Object key = new Object();
Object value = new Object();
Map<Object, List> cache = new HashMap<>();
cache.put(key, value);
...
}
}
key의 유효 범위를 벗어나더라도 `cache`가 key의 참조를 가지고 있어, 가비지 컬렉션의 대상이 되지 않음
해결방법 - `WeakHashMap`
public class CacheSample {
public static void main(String[] args) {
Object key = new Object();
Object value = new Object();
Map<Object, List> cache = new WeakHashMap<>();
cache.put(key, value);
...
}
}
- 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아있는 캐시가 필요한 상황에서만 유용
- key 값을 모두 Weak 레퍼런스로 감싸 strong reference가 없어지면 가비지 컬렉션의 대상이 됨
- key의 참조가 다 쓴 참조가 되면 엔트리를 가비지 컬렉션의 대상이 되도록 해 캐시에서 비우는 방식
또는 시간이 지나면 캐시값이 의미가 없어지는 경우에 백그라운드 쓰레드를 사용하거나 (ScheduledThreadPoolExecutor), 새로운 엔트리를 추가할 때 부가적인 작업으로 기존 캐시를 비우는 일을 할 것이다. (LinkedHashMap 클래스는 removeEldestEntry라는 메서드를 제공)
리스너 혹은 콜백, 메모리 누수의 주범
- 리스너, 콜백 함수를 등록만 하고 명확히 해지하지 않는다면, 뭔가 조치해주지 않는 한 콜백은 계속 쌓여 메모리 누수 발생
- 리스너, 콜백을 weak reference로 저장하면 가비지 컬렉터가 즉시 수거
- WeakHashMap에 키로 저장하면 됨
정리
- 메모리 누수는 겉으로 잘 드러나지 않아 시스템에 수년간 잠복하는 사례도 있음
- 메모리 누수는 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야만 발견되기도 함
- 직접 메모리를 관리하는 클래스의 경우 메모리 누수를 조심하자
- 캐시에서도 방심하고 방치하면 메모리 누수가 발생한다
- 콜백, 리스너도 명확히 해지하지 않으면 메모리 누수가 발생하는 주범이 됨
- 위와 같은 종류의 문제는 예방법을 익혀두는 것이 매우 중요
'Java' 카테고리의 다른 글
Item9 try-finally보다는 try-with-resources를 사용하라 (0) | 2023.02.02 |
---|---|
Item8 finalizer와 cleaner 사용을 피하라 (0) | 2023.01.15 |
Item6 불필요한 객체 생성을 피하라 (0) | 2022.12.09 |
Item5 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2022.12.07 |
Item4 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2022.12.06 |