다 쓴 객체 참조를 해제하라

JDK 8버전 Stack 코드 문제점

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);
    }
}

JDK 8버전에서는 현재 index를 변경해서 stack의 top을 관리하지만, pop()을 할 때 TOP의 값을 줄이기만 합니다. 이 부분에서 메모리 누수가 발생합니다.

Stack 메모리 누수 문제

참조 무효화로 해결

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null;  // 참조 해제!
    return result;
}

null 처리를 해주면 GC가 해당 객체를 정리합니다.

메모리 낭비를 줄이는 방법

1. 스코프 밖으로 밀어내라

public void test() {
    if (true) {
        Integer a = 10;  // if문이 종료되면 변수가 해제
    }
}

2. JDK 11 실제 Stack 구현부

public synchronized void removeElementAt(int index) {
    if (index >= elementCount) {
        throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
    }
    else if (index < 0) {
        throw new ArrayIndexOutOfBoundsException(index);
    }
    int j = elementCount - index - 1;
    if (j > 0) {
        System.arraycopy(elementData, index + 1, elementData, index, j);
    }
    modCount++;
    elementCount--;
    elementData[elementCount] = null;  /* to let gc do its work */
}

JDK 11버전에서는 null 처리를 통해 객체 참조 해제를 해줘서 Stack을 사용해도 메모리 누수로부터 안전합니다.

JDK 11 Stack 구현

캐시 메모리 누수

객체 참조를 캐시에 넣고 제거하지 않으면 지속적인 참조 때문에 메모리 누수가 발생합니다.

Map<Object, String> map = new HashMap<>();

HashMap은 강한 참조 객체의 한 종류로, 직접적으로 key를 해제하지 않으면 메모리 누수가 발생합니다.

해결 방안: WeakHashMap

Map<Object, String> map = new WeakHashMap<>();

WeakHashMap을 사용하면 외부에서 해당 key 객체가 살아있는 동안만 key-value가 살아있습니다 (약한 참조). 단, key가 상수 풀에 저장되어 있으면 적용되지 않습니다 (예: primitive type).

HashMap vs WeakHashMap

예시 코드

HashMap

public static void hashMap() {
    HashMap<Integer, String> map = new HashMap<>();
    Integer num1 = new Integer(10);
    String str1 = new String("str1");
    Integer num2 = new Integer(20);
    String str2 = new String("str2");
    map.put(num1, str1);
    map.put(num2, str2);

    System.out.println(map.toString());  // {10=str1, 20=str2}
    num1 = null;
    System.gc();
    System.out.println(map.toString());  // {10=str1, 20=str2} - 여전히 존재
}

WeakHashMap

public static void weakHashMap() {
    WeakHashMap<Integer, String> map = new WeakHashMap<>();
    Integer num1 = new Integer(10);
    String str1 = new String("str1");
    Integer num2 = new Integer(20);
    String str2 = new String("str2");
    map.put(num1, str1);
    map.put(num2, str2);

    System.out.println(map.toString());  // {10=str1, 20=str2}
    num1 = null;
    System.gc();
    System.out.println(map.toString());  // {20=str2} - 10이 사라짐
}

WeakHashMap은 key를 참조 해제했을 때 key가 사라진 것을 볼 수 있으며, HashMap은 강한 결합 때문에 key를 가지고 있어 메모리 누수가 발생할 수 있습니다.

WeakHashMap 실행 결과

메모리 누수 방지 요약

참조

  • 이펙티브 자바