Release reference that is unused

jiho·2021년 5월 23일
0

EffectiveJava

목록 보기
8/12

자바에서는 메모리관리는 개발자가 아닌 GC(Gabage Collector)가 처리해줍니다. 하지만 전혀 메모리관리에 신경쓰지 않아도 된다고 오해해서는 안됩니다.

스택을 구현한 간단한 코드를 보겠습니다.

public class Stack {
	private Object[] elements;
    private int 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];
    }
	
    private void ensureCapacity() {
    	if(elements.length == size) 
        	elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

위 코드는 왠만한 테스트는 모두 통과할 것 같습니다.(그만큼 간단하니깐) 하지만 메모리 누수와 관련된 문제가 있습니다. 이 스택을 오래 사용하다보면 가비지 컬랙션의 활동과 메모리 사용량이 늘어나 결국 성능이 저하 될 것입니다. 메모리 누수가 발생하는 곳은 pop() 메소드에서 element를 제거했을 때 GC가 회수해가지 않습니다. 이 스택은 다 쓴 참조를 여전히 elements 배열 속에 들고 있기 때문입니다.

해법은 간단합니다. 해당 참조를 다썻을 때 null처리를 해주면 됩니다. 제대로 구현한 모습은 아래와 같습니다.


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

다 쓴 참조를 null 처리하면 다른 이점도 따라옵니다. null 처리한 참조를 실수로 사용하면 NullPointerException를 던지며 종료됩니다.

하지만 이러한 방식으로 모든 객체를 쓰자마자 null 처리를 해주는 방식은 프로그램을 필요 이상으로 지저분하게 만들기도합니다. 다 쓴 객체는 GC가 수거할 수 있게 변수를 담은 유효범위를 밖으로 밀어내는 것이 가장 좋은 방법입니다.

문제가 생긴 원인은 다시 되새겨보면 바로 스택이 자기 메모리를 직접 관리하기 때문입니다. 이 스택은 elements 배열로 저장소 풀을 만들어 원소를 관리합니다.

캐시 또한 메모리 누수를 일으킬 수 있다.

객체 참조를 캐시에 넣고 나서, 객체를 다쓴 뒤에도 한참 그냥 놔두게 되는 일을 자주 접할 수 있습니다.

해법은 여러가지 입니다. 캐시 외부에서 키(key)를 참조하는 동안만 엔트리가 살아있는 캐시가 필요한 상황이라면 WeakHashMap을 사용해 캐시를 만들면 좋습니다. 다쓴 엔트리는 즉시 자동으로 제거 될 것입니다.

캐시를 만들 때 보통은 캐시 엔트리의 유효기간을 정확히 정의하기 어렵기 때문에 시간이 지날 수록 엔트리의 가치를 떨어뜨리는 방식을 흔히 사용합니다.

이런 방식에서는 쓰지 않는 엔트리를 청소해줘야합니다.

(Scheduled ThreadpoolExecutor 같은)백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법이 있습니다.

WeakHashMap 내용이 나왔으니 한번 자세히 알아보겠습니다.

WeakHashMap

https://www.baeldung.com/java-weakhashmap

WeakHashMap는 캐시로 사용되는 자료구조 중 하나이며 java.util package에서 찾아볼 수 있습니다.

간단한 캐시 구현체를 만들어보면서 WeakHashMap을 사용해보겠습니다.

주의할 점은 여기서 예시는 해당 map 어떻게 동작하는지 이해하기 위해서 사용할 뿐, 우리만의 캐시 구현체를 구현하는것은 거의 항상 좋은 생각이 아닙니다.

WeakHashMap은 Map interface를 구현하는 hashtable기반의 구현체이며 WeakReference Type의 key를 가지고 있습니다.

WeakHashMap 속의 entry는 그것의 key가 더이상 사용되지 않을 때 자동적으로 제거될 것입니다. 이건key를 가리키는 reference가 없다는 것을 의미합니다. GC가 key를 제거했을 때, 그에 해당하는 entry는 효율적으로 map에서 제거됩니다. 이렇듯 이 클래스는 다른 Map 구현체들과는 다소 다르게 동작합니다.

Strong, Soft, and Weak References

WeakHashMap이 어떻게 동작하는지 이해하기위해서, key의 기본 구조인 WeakReference class를 살펴볼 필요가 있습니다. 자바에서는 3개의 주요 타입이 있습니다.

  • Strong Reference

가장 흔한 참조 타입입니다.

Integer prime = 1;

변수 prime은 Integer 객체에 강한 참조를 가지고 있습니다. strong reference를 가지는 어떠한 객체든 GC에 대상이 되지 않습니다.

  • Soft Reference

SoftReference를 가지는 객체는 JVM이 memory가 필요할 때까지 garbage collection이 수행되지 않습니다.

자바에서 SoftReference를 만들 수 있는 방법은 다음과 같습니다.

Integer prime = 1;
SoftReference<Integer> soft = new SoftReference<Integer>(prime);
prime = null;

prime 객체는 strong reference를 가지고 있었습니다. 하지만 우리는 softReference로 prime 의 strong reference를 감쌓고 strong reference는 null로 해제했습니다. prime object는 GC의 대상이긴 하지만 JVM이 절대적으로 memory가 필요할 때만 오직 collected되어집니다.

  • Weak Reference

weak reference에 의해 참조되어지는 객체는 garbage collected 되어집니다.이 경우에는 GC는 메모리가 필요할 때까지 기다리지않습니다.

우리는 아래와 같이 WeakReference를 생성할 수 있습니다.

Integer prime = 1;
WeakReference<Integer> soft = new WeakReference<Integer>(prime);
prime = null;

우리가 prime 참조를 null로 만들었을 때(strong reference가 없어졌을 때), prime 객체는 GC의 다음 주기에 garbage collecting되어질 것입니다.

WeakHashMap as an Efficient Memory Cache

큰 이미지 객체를 값으로하고 이미지 이름을 key로하는 Cache가 필요하며 이에 알맞는 적절한 Map 구현체를 선택하려고 합니다.

단순한 HashMap은 value 객체가 많은 메모리를 차지하기 때문에 좋은 선택이 아닙니다. 우리의 어플리케이션에서 더이상 사용되지않을 때 GC에 의해 cache로부터 제거되지 않을 것입니다.

이상적으로, 사용되지않는 객체를 자동적으로 GC가 삭제하도록 해주는 Map 구현체를 원합니다. 큰 사이즈의 이미지 객체의 key가 우리 어플리케이션 어디에서 사용되지 않을 때, 그 entry는 메모리에서 사라질 것입니다. 운좋게도 WeakHashMap은 정확히 이러한 특징들을 가지고 있습니다.

간단한 사용법은 아래와 같습니다.

WeakHashMap<UniqueImagename, BigImage> map = new WeakHashMap<>();
BigImage bigImage = new BigImage("image_id");
UniqueImageName imageName = new UniqueImageName("name_of_big_image");

map.put(imageName, bigImage);
assertTrue(map.containsKey(imageName));

imageName = null;
System.gc();

await().atMost(10, TimeUnit.SECONDS).util(map::isEmpty());
profile
Scratch, Under the hood, Initial version analysis

0개의 댓글