자바는 가비지 컬렉터(GC)를 사용하여 메모리를 관리해주기 때문에 직접 메모리를 관리해야 하는 C/C++ 언어보다 사용하기 편리하다.
하지만 가비지 컬렉터가 모든 메모리 관리를 대신해준다고 믿고 있다가는 메모리 누수로 성능 저하가 일어날 수 있다.
메모리 누수의 예를 살펴보자.
스택의 예
public class Stack {
private Object[] elements;
// 배열에 들어가있는 데이터의 개수
private int size = 0;
// 배열의 고정 할당 크기, size가 이 크기를 넘어가면 복사해서 새로 만든다.
private static final int DEFAULT_INITIAL_CAPACITY = 10;
public Stack()
{
elements = new Object[Default_INITIAL_CAPACITY];
}
public void push(Ojbect e)
{
elements[size++] = e;
}
public Object pop()
{
if(size == 0)
{
throw new EmptyStackException();
}
return elements[--size];
}
}
위의 스택에서 pop()을 진행한다고 생각해보자.
pop()을 통해 size를 하나 줄인뒤 배열에서 원소를 하나 꺼내고 있다.
이때 중요한건 단순히 size가 줄었을뿐, 실제 스택의 마지막에 들어있던 원소는 아직 메모리 상에 존재한다는 점이다.
객체의 참조를 없앤 것이 아니기 때문에 가비지 컬렉터는 제거할 대상이라 전혀 인식하지 못한다.
위와 같은 객체들이 Heap에 많이 남아있게 되면 이는 성능에 악영향을 줄 수 있다.
스택 같은 클래스를 자기 메모리를 직접 관리하는 클래스라고 한다.
그렇다면 어떻게 해야 스택에서 pop한 원소를 가비지 컬렉터 대상으로 만들 수 있을까?
public Object pop()
{
if(size == 0)
{
throw new EmptyStackException();
}
Object result = elements[--size];
element[size] == null; // 배열의 활성 영역 밖으로 벗어난 객체는 null 참조 시킨다.
return result;
}
방법은 간단하다. 배열 내부에서 size 범위 내부의 영역을 활성 영역이라고 한다. pop을 통해 활성 영역 밖으로 밀려난 객체에 null을 할당하면 가비지 컬렉터가 수거 대상으로 인식한다.
캐시
메모리 누수를 일으킬 수 있는 대표적인 예로 캐시가 있다.
객체 참조를 캐시에 넣은 뒤, 해제하는 걸 잊으면 캐시는 계속 메모리 상에 적재된다.
또한 객체의 참조를 해제한다고 해도 map상에는 계속 데이터가 존재하게 된다.
아래의 코드를 보자.
CacheKey 객체와 Person 객체를 사용해서 캐시를 구현했다.
외부에서 더이상 cacheKey1의 데이터를 사용하지 않기로 해서 null을 할당했다고 생각해보자.
하지만 map상에서 데이터는 사라지지 않는다.
위의 사진과 같이 CacheKey 2개가 똑같이 유지되고 있는걸 알 수 있다.
이를 해결하기 위해서 외부에서 Key를 참조하는 동안만 map의 데이터가 유지 되도록 하는 WeakHashMap을 사용할 수 있다.
위의 코드에서 가비지 컬렉팅 과정을 거치면 cacheKey1의 엔트리는 사라지는 것을 알 수 있다.