자바는 가비지 컬렉터가 참조하지 않는 객체들 다 알아서 회수 해주는데요!!?
하지만! 몇몇 경우 메모리 누수가 발생 할 수 있으니 조심해야 한다.
스택을 간단히 구현한 코드이다.
메모리 누수가 일어나는 위치는 어디일까?
public class Stack {
private Object[] elements;
private int size = 0;
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);
}
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
게시글 리스트를 가져오는 API 에 캐시를 사용하는 상황을 생각해보자.
public class Post {
private Integer id;
private String title;
private String content;
// getter, setter
}
public class PostRepository {
private Map<CacheKey, Post> cache;
public PostRepository() {
this.cache = new WeakHashMap<>(); // 1
// this.cache = new HashMap<>(); // 2
}
public Post getPostById(Integer id) {
CacheKey key = new CacheKey(id);
if (cache.containsKey(key)) {
return cache.get(key);
} else {
Post post = new Post();
cache.put(key, post);
return post;
}
}
public Map<CacheKey, Post> getCache() {
return cache;
}
}
public class CacheKey {
private Integer value;
private LocalDateTime created;
public CacheKey(Integer value) {
this.value = value;
this.created = LocalDateTime.now();
}
// getter
}
@Test
void cache() throws InterruptedException {
PostRepository postRepository = new PostRepository();
postRepository.getPostById(1);
assertFalse(postRepository.getCache().isEmpty());
// run gc
System.out.println("run gc");
System.gc();
System.out.println("wait");
Thread.sleep(3000L);
assertTrue(postRepository.getCache().isEmpty());
}
HashMap 사용
WeakHashMap 사용
- 캐시(Map<CacheKey, Post> cache)의 키가 다른 곳에서 참조하는 곳이 없으므로 가비지 컬렉션 대상이 된다.
@Test
void backgroundThread() throws InterruptedException {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
PostRepository postRepository = new PostRepository();
postRepository.getPostById(1);
// 제일 오래된 캐시 삭제
Runnable removeOldCache = () -> {
System.out.println("running removeOldCache task");
Map<CacheKey, Post> cache = postRepository.getCache();
Set<CacheKey> cacheKeys = cache.keySet();
Optional<CacheKey> key = cacheKeys.stream().min(Comparator.comparing(CacheKey::getCreated));
key.ifPresent((k) -> {
System.out.println("removing " + k);
cache.remove(k);
});
};
System.out.println("The time is : " + new Date());
// 3초에 한번 쓰레드 실행
executor.scheduleAtFixedRate(removeOldCache, 1, 3, TimeUnit.SECONDS);
Thread.sleep(20000L);
executor.shutdown();
}