본문 바로가기

Language & Framework/Java

이펙티브 자바 (6) 다 쓴 객체 참조를 해제하라

 

 

오늘 다룰 내용은 Item 8 : 다 쓴 객체 참조를 해제하라 파트다.

좋은 내용이지만 3페이지로 이해하기엔 어려운 내용인 것 같다.. 애초에 "이렇게 해라~"라는 게 이 책의 모토지 이론을 하나하나 알려주는 책은 아니니 어쩔 수 없긴 하다.

더 깊은 이해를 위해서는 GC와 메모리에 대한 공부가 필요할 것 같다. 우선 이번 장에서는 책에서 소개하는 예시들 위주로만 다룬다.

여기서 다루는 내용 외에 Memory Leak의 다양한 예시가 궁금하다면 아래 링크를 참조하자.

Garbage Collection의 경우 Oracle docs에서 기본적인 내용을 공부할 수 있다.

 

https://www.baeldung.com/java-memory-leaks

 

Understanding Memory Leaks in Java | Baeldung

Learn what memory leaks are in Java, how to recognize them at runtime, what causes them, and strategies for preventing them.

www.baeldung.com

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

 

Java Garbage Collection Basics

Java Overview Java is a programming language and computing platform first released by Sun Microsystems in 1995. It is the underlying technology that powers Java programs including utilities, games, and business applications. Java runs on more than 850 mill

www.oracle.com

 

 

 

우선 첫 번째 예시이자, 가장 흔한 예시인 Stack이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class EffectiveStack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    public EffectiveStack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; }
 
    public void push(Object e) {
        ensureCapacity();
        elements[size++= e;
    }
 
    public Object pop() {
        if (size ==0throw new EmptyStackException();
        return elements[--size];
    }
 
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size);
    }
 
    public Object pop2() {
        if (size == 0throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }
}
 
cs

 

위 예시에서 메모리 누수가 일어나는 위치는 어디일까?

바로 pop()이다. 해당 코드를 확인해보면 인덱스만 앞으로 이동하고 있을 뿐 기존 데이터를 전혀 비우고 있지 않다.

이렇게 될 경우 해당 Object에 대한 참조가 끊어지지 않아서 Garbage Collector가 해당 데이터를 비우지 않고 계속해서 저장되기 때문에 메모리 누수가 발생하고, 이는 결국 OutOfMemoryError로 이어질 것이다.

* StackOverFlow는 스택 영역과 관련된 에러이며, OutOfMemoryError는 힙 영역과 관계되어 있다.

 

이전 글에서 기존에는 String Pool이 Heap 영역에 있었으나, 이제 Metaspace로 자리를 옮겼다는 내용을 다루었는데 이 또한 String이 계속해서 쌓이기만 하고 GC되지 않는 문제를 해결하기 위해서이다.

 

위의 pop() 메서드를 메모리 누수가 발생하지 않는 올바른 메서드로 변경한 것이 pop2()이다.

해당 메서드에서는 pop으로 인덱스를 이동함과 함께 기존 Object에 대한 참조를 Null로 변경함으로써 해제한다.

이 경우 참조된 객체가 해당 함수 밖에서도 참조되지 않고 있다면 GC의 대상이 될 것이다.

 

 

 

두 번째 예시인 캐시이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class Dummy {
}
 
@Getter
public class EffectiveCacheKey {
    Integer key;
    private LocalDateTime created;
 
    public EffectiveCacheKey(Integer key) {
        this.key = key;
        this.created = LocalDateTime.now();
    }
 
    public LocalDateTime getCreated() {
        return created;
    }
}
 
 
public class EffectiveRepository {
    private Map<EffectiveCacheKey, Dummy> cache;
 
    public EffectiveRepository() {
        this.cache = new HashMap<>();
    }
 
    public Dummy getDummyId(Integer id) {
        EffectiveCacheKey key = new EffectiveCacheKey(id);
        if (cache.containsKey(key)) {
            return cache.get(key);
        } else {
            Dummy dummy = new Dummy();
            cache.put(key, dummy);
            return dummy;
        }
    }
 
    public Dummy getCacheKey(EffectiveCacheKey key) {
        if (cache.containsKey(key)) {
            return cache.get(key);
        } else {
            Dummy dummy = new Dummy();
            cache.put(key, dummy);
            return dummy;
        }
    }
 
    public Map<EffectiveCacheKey, Dummy> getCache() {
        return cache;
    }
}
 
cs

 

위 예시도 Stack의 그것과 별반 다를 바 없으니 문제점을 쉽게 찾아낼 수 있을 것이다.

바로 캐시에 무한하게 key - value를 쌓아놓기만 하고 어떠한 처리도 해주지 않고 있다는 것이 문제다.

 

이런 의도치 않은 객체 유지(unintentional object retention)는 메모리 누수의 흔한 원인 중 하나로써 관련된 실제 사례를 유명 기술 블로그들에서도 종종 볼 수 있다.

 

이를 해결하기 위해서는 구체적으로 해당 캐시에서 추후 어떻게 자원을 제거할 것인지에 대해 명시하거나, WeakHashMap을 사용하는 것 또한 하나의 방법이다.

 

WeakHashMap이나 WeakReference만 다뤄도 긴 글이 될 것 같아서 간단하게만 짚고 넘어가고 이에 대한 글은 추후 따로 작성할 예정이다. 이에 대해 전혀 모른다면 아래 링크의 글들을 읽어보자. 간단하게 설명되어 있다.

 

https://www.baeldung.com/java-weak-reference

 

Weak References in Java | Baeldung

Learn about Weak References in Java and their common usage scenarios

www.baeldung.com

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

 

Guide to WeakHashMap in Java | Baeldung

Explaining the WeakHashMap, how it works, when to use it, and a small example.

www.baeldung.com

 

일반적인 HashMap의 경우 Map에 key와 value가 put되면 사용 여부와 관계 없이 해당 데이터는 별도의 구현 없이 삭제되지 않으나, WeakHashMap은 key에 대한 참조가 사라질 경우 GC가 해당 key-value를 정리할 수 있는 권한을 가진다.

단, 주의할 점은 String이나 Integer 등 JVM 내부에 캐시한 값을 사용하는 경우 이 참조는 해제되지 않기 때문에 WeakHashMap의 키로 사용해도 GC 대상이 되지 않는다. (Wrapper class사용 시 GC가 되지 않는 것은 객관적 사실이나 이 설명이 적절한지는 검증이 필요합니다.)

 

책에서는 이 두 가지(캐시, 스택) 정도만 다루고 있으나 이외에도 Memory Leak의 원인은 다양하다.

대표적으로 우리가 Logging을 위해 AOP를 구현할 때 많이 사용하게되는 ThreadLocal의 경우 개발자의 부주의로 메모리 누수를 일으키는 것으로 유명하다.

또한 Static Inner class가 Outer class를 참조하는 경우, Outer class는 GC되지 않는다. (지속적으로 Inner class가 참조하고 있는 상태가 되므로).

우리가 사용하는 스프링 DB Connection같은 경우도 자원 반환을 명시하지 않을 경우 메모리 누수의 원인이 될 수 있다. try-with-resources를 사용하면 효과적으로 관리할 수 있으나 요즘 시대에는 프레임워크가 다 알아서 처리해주기 때문에 개발자가 직접 해당 자원 반환을 명시해줄 일은 거의 없다고 보면 된다.

 

 

 

 

이번 파트는 추가적으로 공부하는데 들어간 시간은 엄청난데 설명할 만큼 이해하지 못해서 평소보다 글이 짧아졌다..

내가 Garbage Collector나 JVM에 대한 이해가 굉장히 부족하다는 것을 깨닫게 해준 파트였다. 기회가 된다면 추후 이 글은 조금 더 보강하고 싶다.