❓ C언어도 아닌데 객체 참조를 해제??
C, C++ 처럼 메모리를 직접 관리해야 하는 언어를 쓰다가 자바처럼 가비지 컬렉터를 갖춘 언어로 넘어오면 프로그래머의 삶이 훨씬 편안해집니다. 다 쓴 객체를 알아서 회수해가니 말입니다. 하지만 종종 이것이 메모리 관리에 더 이상 신경 쓰지 않아도 된다고 오해하는 개발자들이 있는데 전혀 아닙니다.
꼭꼭 숨어 있는 문제가 존재합니다. 바로 메모리 누수입니다.
이제부터 우리는 메모리가 누수되는 3가지 경우에 대해서 알아보겠습니다.
먼저 아래 스택 코드를 예로들어봅시다.
import java.util.Arrays;
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);
}
}
한 눈에봐도 이상이 별로 없어보이고 테스트까지 잘 통과합니다. 하지만 메모리 누수가 발생합니다. 이 스택을 사용하는 프로그램을 오래 실행하다 보면 점차 가비지 컬렉션 활도오가 메로리 사용량이 늘어나 결국 성능이 저하됩니다.
아래와 같이 스택이 커졌다가 줄어들었을 때 스택에서 거내진 객체들을 가비지 컬렉터가 회수하지 않기 때문에 메모리 누수가 발생합니다.
public static void main(String[] args) {
Stack stack = new Stack();
for (String arg : args)
stack.push(arg);
while (true)
System.err.println(stack.pop());
}
심지어 프로그램에서 그 객체들을 더 이상 사용하지 않더라도 회수하지 않습니다. 왜냐하면 이 스택이 그 객체들의 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문입니다.
📌 다 쓴 참조(obsolete reference)
객체에 대한 참조가 더 이상 필요하지 않게 되었지만 그 참조가 연전히 존쟇여 가비지 컬렉터가 그 객체를 메모리에서 제거하지 못하게 만드는 상황을 가리킵니다. 이는 메모리 누수의 한 형태로, 프로그램이 필요하지 않은 데이터를 계속해서 메모리에 유지시켜 프로그램의 성능 저하나 시스템 리소스의 낭비를 초래 할 수 있습니다. 예를 들어, 컬렉션(Collection) 객체에 저장된 대량의 데이터가 더 이상 필요하지 않지만, 해당 컬렉션 객체에 대한 참조가 계속 유지되는 경우, 그 데이터는 가비지 컬렉션의 대상이 되지 못하고 메모리에 계속 남게 됩니다. 이러한 상황을 방지하기 위해서는 더 이상 필요하지 않은 객체 참조를 명시적으로 null로 설정하거나, 참조 범위를 벗어나도록 조치하여 가비지 컬렉터가 메모리를 효율적으로 관리할 수 있도록 해야 합니다.
해결법은 간답합니다. 해당 참조를 다썻을 때 null처리(참조 해제)하면 됩니다.
예시의 스택 클래스에서 각 원소의 참조가 더 이상 필요 없어지는 시점은 스택에서 꺼낼질 때입니다.
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
하지만 이 방법이 만능은 아닙니다. 객체 참조를 null 처리하는 일은 예외적인 경우여야 합니다.
그렇다면 그 예외적인 경우가 언제일까요?
가비지 컬렉션이 대상이 되지 않는 예외적인 경우인 스택, 배열, 리스트, Set, Map 을 사용할 때는 메모리를 우리가 직접 관리하는 경우 메모리 누수를 주의해야합니다.
다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것입니다.
캐시 역시 메모리 누수를 일으키는 주범입니다.
객체 참조를 캐시에 넣고 나서, 이 사실으 ㄹ까맣게 잊은 채 극 객체를 다 쓴 뒤로도 한참을 그냥 놔두는 일을 자주 접할 수 있습니다.
WeakHasMap을 사용해 캐시를 만들면 됩니다.
다 쓴 엔트리는 그 즉시 자동으로 제거될 것입니다. (단, WeakHashMap은 이러한 상황에서만 유용하다는 사실을 기억해야합니다.)
❓WeakHashMap
WeakHashMap은 Java 컬렉션 프레임워크의 일부로, HashMap과 유사하지만 키(key)에 대해 약한 참조(weak reference)를 사용한다는 점에서 차이가 있습니다. 이는 WeakHashMap에 있는 키로 참조되는 객체가 다른 곳에서 더 이상 사용되지 않고 가비지 컬렉터에 의해 수집될 수 있게 한다는 것을 의미합니다. 즉, WeakHashMap의 키에 대한 참조가 가비지 컬렉터에 의해 자동으로 제거될 수 있어 메모리 효율성을 높일 수 있습니다. WeakHashMap은 주로 메모리 누수를 방지하기 위한 캐시와 같은 용도로 사용됩니다. 예를 들어, 어떤 객체에 대한 캐시를 구현할 때 해당 객체에 대한 강력한 참조(strong reference)를 유지하면, 그 객체는 캐시가 존재하는 한 가비지 컬렉터에 의해 수집될 수 없습니다. 하지만 WeakHashMap을 사용하면, 캐시에 있는 객체에 대한 참조가 약한 참조로 유지되므로, 해당 객체가 다른 곳에서 더 이상 사용되지 않을 경우 가비지 컬렉션 대상이 될 수 있습니다.
아래는 WeakHashMap을 사용하는 간단한 예제입니다
import java.util.Map;
import java.util.WeakHashMap;
public class WeakHashMapExample {
public static void main(String[] args) {
Map<Object, String> weakHashMap = new WeakHashMap<>();
Object key1 = new Object();
Object key2 = new Object();
// 키와 값 추가
weakHashMap.put(key1, "value1");
weakHashMap.put(key2, "value2");
// 가비지 컬렉션 전에 맵의 내용 출력
System.out.println("Before GC: " + weakHashMap);
// key1에 대한 강력한 참조 제거
key1 = null;
// 가비지 컬렉터를 강제로 호출 (실제 환경에서는 가비지 컬렉터 호출을 강제로 하지 않습니다)
System.gc();
// 잠시 대기 (가비지 컬렉터가 실행될 시간을 주기 위함)
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 가비지 컬렉션 후에 맵의 내용 출력
// key1에 대응하는 엔트리는 가비지 컬렉터에 의해 제거될 수 있음
System.out.println("After GC: " + weakHashMap);
}
}
key1에 대한 참조를 null로 설정하고 가비지 컬렉터를 강제로 실행합니다. 만약 key1에 대한 다른 강력한 참조가 없다면, key1과 그에 대응하는 값은 가비지 컬렉터에 의해 WeakHashMap에서 제거될 수 있습니다. System.gc() 호출과 스레드의 sleep은 가비지 컬렉터가 실행되도록 유도하기 위한 것이며, 실제 애플리케이션에서 이 방법은 권장되지 않습니다. 가비지 컬렉션의 실행 시점은 JVM에 의해 결정되기 때문입니다.
리스너와 콜백은 특정 이벤트가 발생했을 때 사전에 정의된 동작을 실행하는 객체입니다.
예를 들어, 사용자 인터페이스에서 버튼 클릭, 데이터 변경 등의 이벤트에 대응하기 위해 사용됩니다.
문제는 이러한 리스너나 콜백이 등록된 컴포넌트에 대한 참조를 유지하고 있기 때문에, 해당 컴포넌트가 더 이상 필요 없게 되어도 가비지 컬렉터에 의해 회수되지 않는 경우가 발생할 수 있습니다. 즉, 이벤트 발생 대상 객체가 메모리에서 해제되어야 할 상황임에도 불구하고, 리스너나 콜백이 그 객체에 대한 참조를 계속 유지하고 있어 메모리 누수가 발생하는 것입니다.
약한 참조(weak reference)를 사용하는 것 입니다.
WeakHashMap은 키(key)에 대해 약한 참조를 유지하기 때문에, 가비지 컬렉터(GC)가 해당 키를 언제든지 회수할 수 있습니다. 즉, WeakHashMap에 저장된 키로 사용되는 객체에 대한 모든 강한 참조(strong reference)가 사라지면, 그 객체는 가비지 컬렉터에 의해 수거될 수 있습니다.
리스너나 콜백을 WeakHashMap을 사용하여 관리하는 예시는 다음과 같습니다
import java.util.Map;
import java.util.WeakHashMap;
public class EventManager {
private final Map<EventListener, Boolean> listeners = new WeakHashMap<>();
public void registerListener(EventListener listener) {
// 리스너를 약한 참조로 등록
listeners.put(listener, Boolean.TRUE);
}
public void notifyListeners(Event event) {
for (EventListener listener : listeners.keySet()) {
listener.onEvent(event);
}
}
}
interface EventListener {
void onEvent(Event event);
}
class Event {}
이 예시에서 EventManager는 WeakHashMap을 사용하여 리스너를 관리합니다.
리스너(키)에 대한 참조가 약한 참조이므로, 리스너 객체에 대한 모든 강한 참조가 사라지면 가비지 컬렉터는 자동으로 이 리스너 객체를 회수할 수 있습니다. 따라서, 개발자가 명시적으로 리스너를 등록 해제할 필요 없이 메모리 누수를 방지할 수 있습니다.
WeakHashMap을 사용하는 이 방식의 주요 장점은 메모리 관리가 자동으로 이루어진다는 것입니다. 하지만, 이 방법을 사용할 때는 리스너나 콜백이 예상보다 일찍 사라질 수 있다는 점을 유의해야 합니다. 즉, 해당 리스너나 콜백을 사용하는 동안에는 어딘가에 강한 참조를 유지해야 합니다. 그렇지 않으면, GC가 이를 너무 일찍 회수해 버릴 수 있습니다.
도구 사용: Java VisualVM, Eclipse Memory Analyzer Tool과 같은 도구를 사용하여 메모리 누수를 진단할 수 있습니다. 코드 리뷰: 정기적인 코드 리뷰를 통해 메모리 누수 가능성을 사전에 감지하고 수정할 수 있습니다.
'정보' 카테고리의 다른 글
[Effective java] 🚀 item 12. 항상 toString을 재정의하라 Always override toString (0) | 2024.03.31 |
---|---|
[Effective java] Item 11 equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2024.03.24 |
[Effective java] Item 6 불필요한 객체 생성을 피하라 (0) | 2024.03.10 |
[Effective java] Item 5 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2024.03.03 |
[Effective java] Item 4 인스턴스화를 막을려거든 private 생성자를 사용하라 (0) | 2024.02.25 |