Effectiva java 06. 쓸모 없는 객체 참조를 제거하자

Jae·2024년 3월 17일

Effective Java

목록 보기
7/11

메모리 누출로 인해 가비지 컬렉션 작업이 증가하면서 메모리 할당과 회수가 빈번하게 생겨서 성능 저하가 발생할 수 있다. 최악의 경우 디스크 상의 paging이나 OOME까지 발생할 수도 있다.

메모리 누출이 생기는 쓸모 없는 참조가 발생하는 경우인데, 참조값이 null이 아닌 값을 갖고 있지만, 다시는 사용되지 않을 참조를 의미한다. 이 경우 참조값이 null이 아니기 때문에 가비지컬렉션 대상에서 제외되어 회수되지 않는다.

책에서는 stack을 통해 예를 들고 있다.

Code

import java.util.EmptyStackException;

public class Stack {

  private Object[] elements;
  private int size = 0;
  private static final int DEFAULT_INITIAL_CAPACITY = 4;

  public Stack() {
    this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
  }

  // 1. 데이터를 입력하고,
  public void push(Object o) {
    System.out.println("PUSH : " + o);
    elements[size++] = o;
  }

  // 2. 데이터를 갱신한다.
  public Object pop() {
    if (size == 0) {
      throw new EmptyStackException();
    }

    System.out.println("current stack size : " + --size);
    return elements[size];
  }

  public void printAll() {
    for (Object o : elements) {
      System.out.print(o + " ");
    }
    System.out.println();
  }

  public static void main(String[] args) {
    Stack s = new Stack();
    s.push(1);
    s.push(2);
    s.printAll();
    s.pop();
    s.printAll();
    s.push(3);
    s.printAll();
  }
}
  1. 은 stack 클래스에 데이터를 넣는 경우이고,
  2. 는 stack 클래스에 데이터를 제거하는 경우이다.

Result

PUSH : 1
PUSH : 2
1 2 null null 
current stack size : 1
1 2 null null 
PUSH : 3
1 3 null null 
  • pop 행위를 실행하여 stack size가 1이 되었음을 확인했지만, 실제 데이터를 보면 2가 그대로 들어가있고, 다시 값을 넣었을 때 초기화 되는 것을 알 수 있다. 여기서 쓸모없는 객체 참조가 발생함을 확인할 수 있다.

따라서 아래와 같이 바꿔줄 수 있다.

Code

~
  public Object pop() {
    if (size == 0) {
      throw new EmptyStackException();
    }
   
    Object result = elements[--size];
    elements[size] = null; // null로 변경해주었다.
    System.out.println("current stack size : " + size);
    return result;
  }

Result

PUSH : 1
PUSH : 2
1 2 null null 
current stack size : 1
1 null null null 
PUSH : 3
1 3 null null 

쓸모 없는 객체 참조를 제거하기 위한 방법

비추

  • null로 바꾸기.
    참조 값이 실수로 사용될 경우 에러가 발생하기 때문에 잘못된 값이 들어가서 추적이 어렵거나, 예외가 발생할 경우를 막을 수 있다.
    단, 다소 코드가 산만해진다.
  • 참조값을 갖는 변수가 유효 범위 밖에 있도록 하면 그 즉시 요소의 값을 null 값으로 판단하게 되고, 가비지컬렉터에게 알려줘서 관리를 하게 된다.

cache
객체 참조를 캐시에 저장하게 되면 캐시에 저장했다는 사실을 잊는 경우가 생긴다.
1. 캐시 외부에 캐시의 키에 대한 참조가 있을 동안만 캐시에 저장된 항목이 유효한 캐시를 구현하는 것으로 충분하면 weakHashMap을 캐시로 사용한다. 그러면 키로 저장된 캐시가 더이상 참조되지 않을 때 해당 항목이 자동으로 삭제 될 것이다. 단 생명주기가 외부 참조에 의해 결정되도록 할 때만 저 해시맵이 유용하다.

  1. 백그라운드 스레드 타이머나 scheduledThreadPoolExecutor로 처리하거나 새 항목을 캐시에 추가할 때 같이 처리하는 등 캐시를 수동으로 관리하는 것도 괜찮다.
    링크드해시맵은 removeEldestEntry 메소드를 사용해서 후자의 방법으로 처리한다. 복잡한 캐시는 java.lang.ref 패키지의 클래스를 직접 사용한다.
  2. listener와 callback
    클라이언트가 callback을 등록하되 말소는 하지않는 api를 구현한다면, 이 콜백은 계속해서 쌓이게 될 것이다. 이 때 콜백이 신속하게 가비지 컬렉션이 되도록하는 가장 좋은 방법은 약한 참조만을 저장 유지하는 것이다. 앞서 말한 weakhashmap과 같은 것을 의미한다.

결론

메모리 누출의 경우 명백하게 드러나지 않는 경우가 많기 때문에, 코드를 철저하게 검사하거나 heap profiler와 같은 디버깅 도구의 도움으로 원인을 분석하게 되는 경우가 있는데, 이러한 일이 생기지 않도록 미연에 방지하는 것이 매우 중요하다.

0개의 댓글