메모리를 직접 관리해야 하는 C, C++와 달리, 자바는 다 쓴 객체를 알아서 회수해가는 가비지 컬렉터를 가지고 있다. 그렇다고 해서, 메모리 관리에 더 이상 신경 쓰지 않아도 된다는 말은 아니다.
자바의 가비지 컬렉터 대상은, 힙 영역에 참조되지 않는 객체들이다. 따라서 힙 Method Area
내부 정적 변수, Stack
, Native Stack
에서 참조되고 있는 경우 가비지 컬렉터의 대상에서 제외된다.
객체의 생존 기간에 따라 물리적으로 Young
/ Old
영역 두 가지로 구분이 되어있다.
Young
: 새롭게 생성되는 객체가 할당되는 영역으로, 해당 영역에 대한 가비지 컬렉터는 Minor GC
이다.Eden
: 새롭게 생성된 객체가 존재Survival
: Eden
에서의 Reachable
객체가 이동하는 영역Old
: Young
영역에서 참조가 유지되어 살아남는 객체가 복사되는 영역으로, 해당 영역에 대한 가비지 컬렉터는 Major GC
혹은 Full GC
이다. 쉽게 말하면 Young
영역을 주기적으로 청소하고, 살아남은 애들은 정말 오래 참조될 객체라고 판단해 Old
영역으로 내보내 관리하는 방식이다.
참고로 자바 8 이후부터는 static
객체가 GC
의 관리 대상으로 변경되었다.
즉 Class
, Method
의 메타 정보, static
객체 등이 존재하던 permanent
영역이 JVM 이 메모리를 필요에 따라 리사이징 가능한 metaspace
로 영역으로 변경되고, static object
와 상수화된 String object
는 heap
영역으로 이동된 것이다.
따라서 자연스럽게 permanent generation
영역이 꽉 차서 나는 메모리 누수 문제 OutofMemoryError
는 자바 8 이후로부터는 발생하지 않는다.
https://blogs.oracle.com/javamagazine/post/java-garbage-collectors-evolution
https://code-factory.tistory.com/48
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);
}
겉으로 보기에는 문제 없어보이지만, 해당 코드는 메모리 누수 가 발생하고 있어 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능 저하로 이어질 것이다.
return elements[--size]
에서 스택에서 원소를 꺼낸다 해도, 스택이 객체들의 참조를 그대로 가지고 있기 때문에 가비지 컬렉터가 회수를 하지 않기 때문이다.
가비지 컬렉터는 객체 참조 하나만 살려둬도 해당 객체가 참조하는 모든 객체를 회수해가지 못한다. 따라서, 잠재적으로 성능에 악영향을 줄 수 있다.
다 쓴 참조를 null
처리(참조 해제) 하면, 메모리 누수를 방지할 수 있다. 아래의 예시는 비활성 영역이 되는 순간 null
처리를 통해 해당 객체를 쓰지 않을 것임을 가비지 컬렉터에 알리는 코드이다.
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제!
return result;
}
하지만, nullpointerexception
등을 고려해야 하기 때문에 자기 메모리를 직접 관리하는 스택에 경우를 제외하고는 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.
따라서, 다 쓴 객체의 참조를 해제하는 가장 좋은 방법은 아래 코드와 같이 참조를 담은 변수를 유효 범위 밖으로 밀어내는 것이라 할 수 있다.
public static void main(String[] args) {
Stack stack = new Stack();
for (String arg : args)
stack.push(arg);
while (true)
System.err.println(stack.pop());
}
객체 참조를 캐시에 넣고, 까먹어서 객체를 다 쓴 이후로도 놔두는 일은 메모리 누수로 이어진다.
하지만 캐시 외부에서 키를 참조하는 동안만(값 X) 엔트리가 살아 있는 캐시가 필요하다면, WeakHashMap
을 사용해 캐시를 만들어 다 쓴 엔트리가 그 즉시 자동으로 제거되게 할 수 있다.
또한, 캐시를 만들때 주로 캐시 엔트리의 유효 기간을 정의하기 어려워 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 사용한다. 이런 방식에서 쓰지 않는 엔트리를 청소하는 방법에는 1)백그라운드 스레드를 활용하거나, 2)캐시에 새 엔트리 추가 시 부수 작업으로 수행하는 방법이 있다.
예를 들어, LinkedHashMap
은 removeEldestEntry()
메서드를 통해 2번 방법으로 제거를 수행한다.
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
클라이언트가 콜백을 등록만 하고 해지하지 않는다면 계속 쌓여 간다.
이럴 때, 콜백을 약한 참조(weak refrence)로 저장하면 가비지 컬렉터가 즉시 수거해갈 수 있다.WeakHashMap
에 키로 저장하는 것이 그 예가 될 것이다.