태그 보관물: LinkedHashMap

자바(Java)에서의 LRUCache 메모리릭과 해결방안..

LRU(Least Recently Used)는 페이지를 사용하는 운영체제에서, 새로운 페이지를 할당하기 위해서 마지막에 사용한 페이지를 교체하는 알고리즘이다. LRUCache는 LRU 알고리즘을 이용하는 캐시클래스라고 보면 되겠다. 여기에서는 많이 사용하는 LRUCache 형태를 살펴보고, 이 형태가 가지고 있는 문제(메모리 릭)를 해결해 보자.

1. LRUCache 구현 방법

1.1 LinkedHashMap을 사용해서 만들기

final int MAX_ENTRIES = 100;
Map cache = new LinkedHashMap(MAX_ENTRIES+1, .75F, true) {
    // This method is called just after a new entry has been added
    public boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }
};

1.2 LinkedHashMap을 상속해서 만들기
대체로 아래의 코드가 많이 보이다. 얼핏 보기에는 잘 동작할 것으로 보인다. 하지만 시간이 지나면서 문제(메모리 릭)가 발생하게 된다.

public class LRUCache<K,V> extends LinkedHashMap<K,V> {
    private int lruSize;

    public LRUCache(int size) {
        lruSize = size;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > lruSize;
    }
}

2. 해결책
해결책을 찾아보니, https://code.google.com/p/reflection-dsl/source/browse/trunk/ReflectionDsl/src/br/com/bit/ideias/reflection/cache/LRUCache.java?spec=svn204&r=204 에서는 Value에 해당하는 객체를 SoftReference(참고로, 이넘으로 객체를 래핑하게 되면 풀 GC가 읽어날때 GC대상이 된다)로 래핑해서 GC에게 위임(?)하고 있네요..

개인적으로는 명시적으로 객체를 삭제해서 메모리릭을 방지하는게 좋다고 생각한다. 잠재적인 메모리릭을 방지해 주는 코드는 아래에서 볼 수 있다.

protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
	boolean isRemove = size() > maxsize;

	if (isRemove) {
		Object obj = this.get(eldest.getKey());  
		if(obj instanceof BufferedWriter) { // V가 BufferedWriter 인스턴스임.. 
			try {
				((BufferedWriter)obj).close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		this.remove(eldest.getKey());
	}

	return isRemove;
}

이상, 자바에서 LRU 형태의 캐시클래스를 만들 때 주의가 필요한 내용을 살펴봤다.

LinkedHashMap 기반으로 LRU(least-recently-used) 캐시하기

java 1.4 Collections API에 새롭게 첨가된 java.util.LinkedHashMap 클래스는 HashMap처럼 해쉬테이블에 기반을 둔 Map 클래스 입니다. LinkedHashMap이 다른 점은 맵 엔트리에서 실행되는 링크된 리스트를 관리할 수 있고 또한 그 엔트리에 대해서 예측할 수 있는 반복 작업을 수행할 수 있다는 것이다. 대부분 이 순서는 맵에 엔트리가 추가된 순서대로 되고 가끔씩 아주 유용한 특징으로 사용할 수 있는 상황도 있습니다. 

LinkedHashMap은 생성자의 세 번째 인자로 true 값을 넘겨줌으로써 가장 최근에 접근(즉 조회되거나 설정된) 순서에서 가장 오래 전에 접근된 순서로 엔트리 순서를 설정할 수 있다.

그리고, LinkedHashMap의 removeEldestEntry()를 새로운 Entry가 추가될 때 마다 호출을 하게되면, 이 메소드가 true를 반환하면 맵에서 ‘가장 오래된’ Entry, 즉 가장 오래 전에 입력되거나 사용된 것은 자동적으로 삭제된다. removeEldestEntry()의 기본값으로 항상 false를 반환하지만 맵이 정해진 어떤 최고 크기에 다다르면 true 값을 반환하도록 오버라이드 할 수 있다. 이와 같이 하면 LRU(least-recently-used) 캐시를 만들 수 있는 것입니다.

public class LRUCache extends java.util.LinkedHashMap {
    public LRUCache(int maxsize) {
        super(maxsize*4/3 + 1, 0.75f, true);
        this.maxsize = maxsize;
    }
    protected int maxsize;
    protected boolean removeEldestEntry() {
        return size() > maxsize;
    }
}