티스토리 뷰

Java에서는 C나 C++에서 처럼 메모리 관리에 대한 고민을 할 필요가 없다.

가비지 콜렉터가 판단해 사용되지 않는 메모리를 해제해 주기 때문이다.

오호 이제 메모리 관리는 신경을 쓰지 않아도 되겠군!이라고 생각했다면 만만의 콩떡이다.

package effectivejava.chapter2.item7;
import java.util.*;

// 코드 7-1 메모리 누수가 일어나는 위치는 어디인가? (36쪽)
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 static void main(String[] args) {
        Stack stack = new Stack();
        for (String arg : args)
            stack.push(arg);

        while (true)
            System.err.println(stack.pop());
    }
}

stack 클래스의 메모리 누수가 일어나는 지점은 pop() 메소드가 호출될 때이다.

stack을 dynamic array의 형태로 설계하여 배열의 크기가 가득 차면 두배로 늘려주고 제일 끝에 객체를 넣어준다.

 

push()를 와장창창 호출하고 pop()을 와장창창 호출하게 됐을 때를 생각해 보면

스택의 배열의 인덱스마다 Object 객체의 레퍼런스가 들어가 있다.

우리는 코드를 읽고 뒤에 Object 객체들이 더 이상 사용되지 않는 것을 알지만 가비지 콜렉터는 그렇지 않다.

물론 저 상황에서 다시 push가 호출되어 배열의 인덱스에 새로운 객체의 레퍼런스로 덮어 씌워지면 그때는 회수될 수 있다. 그러나 그 시점이 언제인지 알 수 없고 덮어 씌워지기 전까지 사용되지도 않는데 Heap 영역의 메모리를 잡아먹고 있게 된다.

예시가 Object 객체 하나라 와닿지 않을 수 있지만, 객체는 다른 객체들을 물고 그 객체는 또 다른 객체들을 물고 있을 때가 많다. 이와 같은 상황에서도 모든 메모리를 회수해 갈 수 없게 된다.

이는 잠재적으로 성능에 악영향을 줄 수 있다.

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

해결 방법은 가비지 콜렉터에게 명시적으로 사용하지 않는 메모리라고 가르쳐 주는 것이다.

다 쓴 참조에 null 값을 집어넣어 사용하지 않는다고 표시할 수 있다. 다른 곳 어디에서도 해당 객체의 레퍼런스를 사용하지 않고 있다면 가비지 콜렉터는 이 객체의 메모리를 수거해 갈 수 있게 된다.

또한 다 쓴 객체의 참조를 해제하면 실수로 이 객체를 참조하려고 하면 NullPointerException이 발생해 null 값을 넣지 않았을 때 발생할 수 있는 문제들을 알 수 있게 된다.

 

 

 

그럼 이런 사용되지 않을 객체를 null 값으로 초기화하는데 혈안이 되어야 하느냐?

답은 그렇지 않다. 객체에 null 값을 넣는 행위는 코드를 지저분하게 할 뿐이다.

Stack과 같은 클래스 설계가 아니라면 객체의 Scope 범위를 한정해서 가비지 콜렉터의 수거 대상이 되게 할 수 있다.

Iterator<E> i = c.iterator();
while(i.hasNext()){
	doSomething(i.next());
}
...
Iterator<E> i2 = c2.iterator();
while(i.hasNext()){	// 비어있구만 넘어가~
	doSomething(i2.next());
}

개발자의 필수 소양 중 하나인 복사 붙여 넣기를 하다 발생한 버그상황이다.

밑에 i2는 제대로 바꿨지만 while문 안의 조건을 고치지 않았다.

i2를 검사하고 싶었지만 실수로 인해 두 번째  while문은 i2의 next를 실행하지 않게 된다.

 for(Iterator<E> i = c.iterator(); i.hasNext();){
	E e = i.next();
    ...
}
...

for(Iterator<E> i2 = c2.iterator(); i.hasNext();){ // i를 찾을 수 없다!! 컴파일 에러
	E e = i2.next();
    ...
}

개선된 코드이다.

똑같이 복붙 신공을 사용했지만 지금 코드는 Iterator의 선언 Scope를 for문 내부로 제한했다.

아래의 for문은 i라는 변수가 무엇인지 알지 못하게 되어 컴파일 에러를 낸다.

 

이처럼 사용될 변수의 Scope 범위를 한정하면 발생할 버그에도 대응할 수 있고, 위의 for문에서 탈출 한 i는 가비지 콜렉터의 수거 대상이 되어 메모리 누수에도 대응할 수 있게 된다.

 

 

 

 

 

그렇다면 null 처리는 언제 해야 하는 걸까?

Stack 클래스처럼 자신의 내부 메모리를 직접 사용하는 경우에는 null 처리를 해줘야 한다.

Stack의 입장에서는 사용되지 않을 레퍼런스이지만 가비지 콜렉터의 입장에서는 사용되지 않는 객체인지 알 수가 없다.

따라서 null처리를 직접 해줘 가비지 콜렉터에게 사용하지 않을 객체임을 알려야 한다.

 

 

 

캐시 역시 메모리 누수를 일으키는 주범이다.

HashMap을 이용해 Key, Value를 캐싱한다고 가정하자. 엔트리의 값에 null이 메모리는 수거 대상이 된다.

만약 해당 엔트리만을 위한 캐시가 필요한 상황이면 WeakHashMap을 사용할 수 있다.

WeakHashMap<Integer, String> map = new WeakHashMap<>();
Integer key1 = 1000;
Integer key2 = 2000;
map.put(key1, "test a");
map.put(key2, "test b");
key1 = null;
System.gc();  //강제 Garbage Collection
map.entrySet().stream().forEach(el -> System.out.println(el)); //2000=test b

기존 HashMap이었다면 key1에 null이 들어가든 말든 두 개의 결과 값을 반환한다.

그러나 WeakHashMap은 엔트리의 내부 객체가 참조 해제되면 가비지 콜렉터의 수거대상이 된다.

이러한 WeakHashMap은 WeakReference를 사용해 구현된다.

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

 prime == null 되면 (해당 객체를 가리키는 참조가 WeakReference 뿐일 경우) 가비지 콜렉터의 대상이 된다.

 

 

 

 

 

메모리 누수의 세 번째 주범은 리스너(Listener) 혹은 콜백(Callback)이라 부르는 것이다.

자바스크립트 배울 때 많이 본 친구들이다.

콜백 메소드는 어떤 이벤트나 함수가 발생할 때 호출되는 함수이다.

 

클라이언트가 콜백을 등록을 하고 명시적으로 해제하지 않으면 이 콜백 메소드들은 호출을 무한정 기다리게 된다.

사용되지 않는 콜백을 수거하기 위해 이를 WeakHashMap의 Key로 등록하게 되면 이를 자동으로 가비지 콜렉터가 수거하게 된다.

 

 

 

 

WeakHashMap :

https://blog.breakingthat.com/2018/08/26/java-collection-map-weakhashmap/

 

Java – Collection – Map – WeakHashMap (약한 참조 해시맵) – 조금 늦은, IT 관습 넘기 (JS.Kim)

> Weak Reference    WeakHashMap의 작동 방식을 이해하려면 JVM의 GC와 관련하여 WeakReference  를 조금은 이해할 필요가 있다.  Java에서는 세 가지 주요 유형의 참조(Reference) 방식이 존재한다.    강한

blog.breakingthat.com

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/11   »
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
글 보관함