Dev Book/Effective Java

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

addmean 2024. 8. 9. 19:54

1. 메모리 누수를 막아라!

CG가 다 쓴 객체를 알아서 회수해 가도 메모리 관리에 신경 써야 한다.

    public class Stack {
    private Object[] elements;
    private int size = 0; //size 는 인덱스
    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];
    }

//    //p.37 코드 7-2 제대로 구현한 pop 메서드
//    public Object pop() {
//        if (size == 0) {
//            throw new EmptyStackException();
//        }
//        Object result = elements[--size];
//        elements[size] = null; // 다 쓴 참조 해제
//        return result;
//    }

    /**
     * 원소를 위한 공간을 적어도 하나 이상 확보한다.
     * 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
     */
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

 

[제네릭 ver](아이템 29)

    //p.172 코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법1
public class StackGenericVer<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    //배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
    //따라서 타입 안정성을 보장하지만,
    //이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
    @SuppressWarnings("unchecked") //경고를 숨기는 용도
    public StackGenericVer() {
        elements =(E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

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

    /**
     * 원소를 위한 공간을 적어도 하나 이상 확보한다.
     * 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
     */
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

 

메모리 누수 = 가비지 컬렉션 활동과 메모리 사용량이 늘어남 -> 성능 저하

 

스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들을 GC가 회수하지 않는다.

 

프로그램에서 그 객체 들을 더 이상 사용하지 않더라도 그 객체들의 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.

 

단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있다 : 객체 참조 하나를 살려두면 GC는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못한다.

 

[해결방안] : 다 쓴 참조를 null 처리한다.

 

 

2. null 처리는 언제 해야 할까?

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

  • 스택(Stack)은 element 배열로 저장소 풀을 만들어 원소들을 직접 관리하기 때문이다.
    • 배열의 활성 영역에 속한 원소들이 사용되고 비활성 영역은 쓰이지 않는다.
  • GC는 이 사실을 알 길이 없다. 비활성 영역 객체가 쓸모없다는 건 프로그래머만 아는 사실이다.
  • 비활성 영역이 되는 순간 null 처리하여 객체의 상태를 GC에게 알려야 한다.

 

다 쓴 참조를 해제하는 가장 좋은 방법

  • 참조를 담은 병수를 유효 범위(scope) 밖으로 밀어내는 것 (변수의 범위를 최소가 되게 정의. item57)
    • item 57 : 지역 변수의 범위를 최소화하라

 

3. 자기 메모리를 직접 관리하는 클래스라면 메모리 누수에 주의하자

[메모리 누수를 일으키는 주범]

  • Stack
  • Cache
    • [해결방안]
    • WeakHashMap을 사용해 캐시를 만들자 (엔트리가 살아 있는 캐시가 필요한 상황에서만 유용하다)
    • 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식으로 쓰지 않는 엔트리를 청소 - (ThreadPoolExecutor 같은) 백그라운드 스레드를 활용
    • 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법 - LinkedHashMap(remove EldestEntry)
    • 더 복잡한 캐시는 java.lang.ref 패키지를 직접 활용하자
  • 리스너(listener)와 콜백(callback)
    • [해결방안]
    • 약한 참조(weak reference)로 저장하면 GC가 즉시 수거해 간다. ex) WeakHashMap

 

4. GC와 java.lang.ref

참고
https://d2.naver.com/helloworld/329631
https://docs.oracle.com/javase/8/docs/api/java/util/WeakHashMap.html

reference object와 강한 참조

강한 참조(Strong Reference)

  • 자바에서 일반적인 참조 형태
  • ex) Object obj = new Object();와 같은 코드에서 obj는 강한 참조

 

java.lang.ref의 reference object

  • SoftReference
  • WeakReference
  • PhantomReference

 

GC와 Reachability

reachable

  • 어떤 객체에 유효한 참조가 있다

 

unreachable

  • unreachable 객체를 가비지로 간주해 GC를 수행

 

root set

  • 유효한 최초의 참조

 

java.lang.ref의 클래스 제공

Soft, Weak, Phantom Reference

 

클래스는 참조 대상인 객체를 캡슐화(encapsulate)한 객체를 생성한다.

 

new WeakReference() -> reference object

new Sample() -> referent

 

 

Softly Reachable과 SoftReference

Softly Reachable 객체는 메모리가 부족할 때 회수 대상

 

SoftReference를 사용하여 참조된 객체는 힙 메모리가 충분하다면 GC가 즉시 회수하지 않고 남겨둘 수 있으며, 자주 사용될수록 더 오래 유지

 

SoftReferencce 객체로만 참조된 객체는 힙에 남아 있는 메모리의 크기와 해당 객체의 사용 빈도에 따라 GC 여부가 결정

 

(마지막 strong reference가 GC 된 때로부터 지금까지의 시간) > (옵션 설정값 N) * (힙에 남아있는 메모리 크기)

 

 

Weakly Reachable과 WeakReference

Weakly Reachable 객체는 GC가 실행될 때마다 회수 대상

 

GC가 실제로 언제 객체를 회수할지는 GC 알고리즘에 따라 모두 다르므로, GC가 수행될 때마다 반드시 메모리까지 회수된다고 보장하지는 않는다

 

LRU 캐시와 같은 애플리케이션에서는 softly reachable 객체보다는 weakly reachable 객체가 유리하므로 LRU 캐시를 구현할 때에는 대체로 WeakReference를 사용, 메모리 효율성을 높이기 위해 많이 활용된다

    WeakReference<Sample> wr = new WeakReference<Sample>( new Sample());  
    Sample ex = wr.get();  // 다른 참조에 대입
    ...
    ex = null;

 

Phantomly Reachable과 PhantomReference

 

Phantomly Reachable 객체는 객체가 파이널라이즈 된 후, 메모리에서 회수되기 전의 상태를 의미

 

PhantomReference는 반드시 ReferenceQueue와 함께 사용해야 하며, get() 메서드는 항상 null을 반환

 

phantomly reachable로 판명된 객체는 더 이상 사용될 수 없게 된다

 

파이널라이즈된 이후에 할당된 메모리가 회수되는 시점에 사용자 코드가 관여할 수 있게 된다 ( 잘 사용하지 않는다 )

 

 

ReferenceQueue

ReferenceQueue는 SoftReference, WeakReference, PhantomReference와 함께 사용될 수 있으며, 객체가 GC에 의해 회수될 때 이 큐에 enqueue 되어 후처리 작업을 할 수 있다.

 

PhantomReference는 이 큐를 필수적으로 사용해야 하며, 이를 통해 파이널라이즈 이후 객체의 추가 작업을 처리할 수 있다.

    ReferenceQueue<Object> rq = new ReferenceQueue<Object>(); 
    PhantomReference<Object> pr = new PhantomReference<Object>(referent, rq);  

 

WeakHashMap

 

[주요 특징]

  • 자동 항목 제거: 키가 더 이상 강하게 참조되지 않으면 GC에 의해 키가 제거되고, 이에 따라 해당 항목도 WeakHashMap에서 자동으로 제거됩니다.
  • null 키와 값 지원: null 키와 null 값을 모두 허용합니다.
  • 동기화되지 않음: 동기화되지 않으므로 여러 스레드에서 안전하게 사용하려면 Collections.synchronizedMap으로 감싸야합니다.
  • 가비지 수집기 의존: 이 클래스의 동작은 가비지 수집기의 동작에 일부 의존합니다. 따라서 전통적인 Map의 불변식이 항상 적용되지는 않습니다.
  • fail-fast 반복자: WeakHashMap의 컬렉션 뷰에서 반환된 반복자는 fail-fast 메커니즘을 지원하며, 맵이 구조적으로 수정되면 ConcurrentModificationException을 던집니다.