본문 바로가기

프로그래밍/JAVA

[JAVA] Effective Java - 7. 다 쓴 객체 참조를 해제하라

반응형

Java는 C, C++ 처럼 메모리를 직접 관리(malloc(), free(), new, delete) 하는 것이 아닌

Garbage Collector가 존재해 메모리를 자동으로 관리 해주므로 프로그래머가

직접 메모리의 할당과 해제를 할 필요가 줄어들었다.

 

Garbage Collector 참조)

https://tgio.tistory.com/36?category=776542 

 

[JAVA] JVM 구조와 동작 2 (Runtime Data Area, Garbage Collector)

Runtime Data Area JVM의 메모리 영역으로 크게 Method Area, Heap Area, Stack Area, PC Register, Native Method Stack 5개의 영역으로 나뉜다. Method Area 클래스의 이름, 타입, 멤버변수, 접근제어자, 메소..

tgio.tistory.com

 

하지만 직접 메모리를 관리해야 하는 경우가 생긴다.

책에서는 Stack을 직접 구현한 코드를 예로 들고 있다.

public class Stack {
  private Object[] elements;
  private int size = 0;
  private static final int DEFAULT_INITAL_CAPACITY = 16;

  public Stack() {
    elements = new Object[DEFAULT_INITAL_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);
    }
  }
}

해당 구현 코드는 겉으로 보기에도 테스트를 진행해도 문제없는 코드처럼 보인다.

하지만 해당 클래스를 사용하는 프로그램을 오래 실행하다보면 성능 저하가 발생할 수 있다.

문제가 되는 코드는 pop 메서드에서 객체를 반환만 할 뿐 객체 참조를 끊지 않는다.

즉 사용하지 않는 객체의 참조가 끊어지지 않아 가비지 컬렉터가 회수하지 못한다.

(또한 해당 객체가 참조 중인 다른 객체 또한 회수하지 못함 즉, 더 메모리의 누수가 심해질 가능성이 크다.)

 

개선된 pop 메서드

public Object pop() {
  if (size == 0) {
    throw new EmptyStackException();
  }
  Object result = elements[--size];
  elements[size] = null;
  return result;
}

 

객체를 반환해주고 스택은 해당 참조를 끊는다  elements[size] = null;

보통은 객체를 참조 중인 변수를 scope 밖으로 밀어내는 것이 일반적이고,

이렇게 객체 참조를 null 처리 하는 일은 예외적인 경우여야 한다.

 

위 예제는 Stack이라는 클래스가 배열을 통해 직접 메모리를 관리하는 경우

가비지 컬렉션이 사용 중인 영역과 미사용인 영역을 알지 못 하므로

null 처리를 통해 사용하지 않을 것 을 가비지 칼렉션에게 알려주는 역할을 한다. 

 

두번째로는 캐시이다.

 

객체 캐싱을 위한 캐시를 사용할 경우 사용하지않는 객체들을 그대로 두면 메모리 누수가 일어날 수 있다.

이때 여러가지 방법으로 메모리 누수를 방지 할 수 있다.

 

1. 외부에서 캐시를 사용할 때 키를 참조하는 동안만 캐싱 필요한 상황이면 WeakHashMap을 사용하여 해결할 수 있다.

public static WeakHashMap<String, String> cacheMap = new WeakHashMap<>();

public static void main(String[] args) throws Exception {
  String key = new String("a");
  cacheMap.put(key, "LIVE");
  printCacheValues();
  // 키의 참조를 없앰
  key = null;
  // 강제로 가비지 콜렉터 실행
  System.gc();
  printCacheValues();
}

public static void printCacheValues() {
  for (Entry<String, String> entry : cacheMap.entrySet()) {
    System.out.println(entry.getValue());
  }
}

 

위의 코드 실행 결과는 

"LIVE"가 한줄이 되는 것을 확인 할 수 있다. 즉 key의 객체가 참조가 해제 되면 ( key = null; )

WeakHashMap만 해당 객체를 약한 참조 하고 있기 때문에 GC의 대상이 되어버린다.

 

2. 백그라운드 스레드를 활용, 새 엔트리 추가시 부수 작업 실행

백그라운드 스레드 활용은 ScheduledThreadPoolExecutor와 같이 주기적으로 돌아가는 스레드를 통하여 사용하지 않는 엔트리를 청소해주는 방식이다.

여기서 사용하지 않는 엔트리란 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식이므로 가장 먼저 들어온 엔트리를 정리하는 방식이다. (엔트리의 정확한 유효기간을 알 수 없기 때문)

또 새 엔트리 추가시 부수 작업을 실행하는 방법으로는 LinkedHashMap의 removeEldestEntry 메서드를 오버라이딩 하여 구현할 수 있다.

LinkedHashMap<String, String> cacheMap = new LinkedHashMap<>() {
  @Override
  protected boolean removeEldestEntry(Entry<String, String> eldest) {
    return this.size() > 5;
  }
};
for (int i = 0; i < 26; i++) {
  String value = (char) (65 + i) + "";
  cacheMap.put(value, value);
}

for (Entry<String, String> entry : cacheMap.entrySet()) {
  System.out.println(entry.getValue());
}

위 코드의 실행 결과는

V
W
X
Y
Z

Map의 사이즈가 5개가 넘어갈 경우 가장 오래된 엔트리를 삭제하고 신규 엔트리를 추가한다.

 

세번째는 리스너 혹은 콜백이다.

API 호출 시 클라이언트가 콜백을 등록만 하고, 해지 않는다면 콜백은 쌓여만 갈 것이다.

이때, 콜백을 약한 참조(WeakHashMap의 키)로 저장한다면 가비지 컬렉터가 즉시 수거 하여 메모리 누수를 막을 수 있다.

반응형