자바는 가비지 컬렉터를 가지고 있어서 다 쓴 객체를 알아서 회수 해 간다.

처음엔 나도 그래서 메모리 관리에 전혀 신경을 쓰지 않았는데, 직접 배포를 하고 메모리 사용률 등을 보니 그렇지 않다는 것을 알게 됐던 기억이 있다..

 

 

메모리 관리에 대한 내용을 알아보기 전에 자바의 가비지 컬렉터에 대한 내용을 먼저 알아두는 것이 좋다.

2021/02/13 - [Java (+ Spring)] - JVM과 Garbage Collector

 

JVM과 Garbage Collector

자바를 이용하여 개발하는 개발자라면 누구나 자바 바이트코드가 JRE 위에서 동작한다는 사실을 잘 알고 있습니다. 이 JRE에서 가장 중요한 요소는 자바 바이트코드를 해석하고 실행하는 JVM(Java V

sysgongbu.tistory.com

 

메모리 누수를 주의 깊게 보아야 하는 경우 아래 3가지 정도가 있다. = 객체들이 다 쓴 참조(obsolete reference)를 여전히 가지고 있는 경우

 

1) 아래 코드는 스택을 구현한 코드인데, pop()을 보면 스택에서 꺼내진 객체를 더 이상 사용하지 않는데도 회수하지 않고 있다.

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    ...
    
    public Object pop() {
        if(size == 0){
            throw new EmpthStackException();
        }
        
        return elements[--size]; // 메모리 누수: elements[size] = null을 해 주어야
    }
}

가비지 컬렉터는 객체 참조 하나 뿐만 아니라, 이 객체가 참조하는/참조되는 모든 객체들을 회수하지 못하게 된다.

그래서 elements[size] = null 을 해 주는 부분이 필요하다. (이 부분을 추가할 경우 외부 영역 접근 시 NullPointerException을 던지게 됨으로써 오류를 조기에 발견할 수 있게 된다)

 

하지만, 모든 곳에 이 것을 해 줄 필요는 없다.

객체 참조를 꼭 null 처리 해 주어야 하는 경우는 위 스택 예제와 같이 자기 메모리를 직접 관리하는 클래스를 구현할 때이다.

위 스택 예제에서는 객체 자체가 아닌 객체 참조를 담는 elements 배열로 저장소 pool을 만들어 원소들을 관리한다.

 

 

 

2) 객체 참조를 캐시에 두고 계속 캐시에 두는 것 또한 메모리 누수를 일으키는 주범이다. 해결 방법은 아래와 같다.

- WeakHashMap을 사용하여 캐시를 만들면 다 쓴 엔트리는 그 즉시 자동으로 제거될 것이다.

package com.example.sypark9646.item07;

import java.util.Map;
import java.util.WeakHashMap;

public class WeakHashMapTest {

		public static void main(String[] args) {
				MyKey k1 = new MyKey("Hello");
				MyKey k2 = new MyKey("World");
				MyKey k3 = new MyKey("Java");
				MyKey k4 = new MyKey("Programming");

				Map<MyKey, String> wm = new WeakHashMap<>();

				wm.put(k1, "Hello");
				wm.put(k2, "World");
				wm.put(k3, "Java");
				wm.put(k4, "Programming");
				k1 = null;
				k2 = null;
				k3 = null;
				k4 = null;
				System.gc(); // 강제로 GC 발생
				System.out.println("Weak Hash Map :" + wm.toString()); //Weak Hash Map :{}
		}
}

 

- 캐시를 만들 때 ScheduledThreadPoolExecuter 등을 이용하여 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 사용한다.

- LinkedHashMap을 사용할 경우에는 removeEldestEntry 메소드를 사용해서 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 사용한다.

- java.lang.ref 패키지를 활용하여 만드는 방법

java.lang.ref 패키지는 전형적인 객체 참조인 strong reference 외에도 soft, weak, phantom 3가지의 새로운 참조 방식을 각각의 Reference 클래스로 제공합니다. 이 3가지 Reference 클래스를 애플리케이션에 사용하면 앞서 설명하였듯이 GC에 일정 부분 관여할 수 있고, LRU(Least Recently Used) 캐시 같이 특별한 작업을 하는 애플리케이션을 더 쉽게 작성할 수 있습니다. 이를 위해서는 GC에 대해서도 잘 이해해야 할 뿐 아니라, 이들 참조 방식의 동작도 잘 이해할 필요가 있습니다.

 

WeakReference<Sample> wr = new WeakReference<Sample>(new Sample());  
Sample ex = wr.get(); // 그림 1
// ...
ex = null; // 그림 2

https://d2.naver.com/helloworld/329631 그림 1
https://d2.naver.com/helloworld/329631 그림 2

 

Strong 레퍼런스를 Weak 레퍼런스로 감싸면 Weak 레퍼런스가 된다.

Garbage Collection 대상이 되려면 해당 객체를 가리키는 레퍼런스가 전부 없어져야 한다.

Weak 레퍼런스는 Strong한 레퍼런스가 없어지면 Weak 레퍼런스도 GC의 대상이 될 수가 있다.

 

즉, 정리하자면, 원래 GC 대상 여부는 reachable/unreachable인가로만 구분하였고 이를 사용자 코드에서는 관여할 수 없었다.

그러나 java.lang.ref 패키지를 이용하여 reachable 객체들을 strongly reachable, softly reachable, weakly reachable, phantomly reachable로 더 자세히 구별하여 GC 때의 동작을 다르게 지정할 수 있게 되었다. = GC 대상 여부를 판별하는 부분에 사용자 코드가 개입할 수 있다

https://d2.naver.com/helloworld/329631 Reachability

녹색으로 표시한 중간의 두 객체는 WeakReference로만 참조된 weakly reachable 객체이고, 파란색 객체는 strongly reachable 객체이다. GC가 동작할 때, unreachable 객체뿐만 아니라 weakly reachable 객체도 가비지 객체로 간주되어 메모리에서 회수된다.

root set으로부터 시작된 참조 사슬에 포함되어 있음에도 불구하고 GC가 동작할 때 회수되므로, 참조는 가능하지만 반드시 항상 유효할 필요는 없는 LRU 캐시와 같은 임시 객체들을 저장하는 구조를 쉽게 만들 수 있다.

 

위 그림에서 WeakReference 객체 자체는 weakly reachable 객체가 아니라 strongly reachable 객체이다. 또한, 그림에서 A로 표시한 객체와 같이 WeakReference에 의해 참조되고 있으면서 동시에 root set에서 시작한 참조 사슬에 포함되어 있는 경우에는 weakly reachable 객체가 아니라 strongly reachable 객체이다.

 

GC가 동작하여 어떤 객체를 weakly reachable 객체로 판명하면, GC는 WeakReference 객체에 있는 weakly reachable 객체에 대한 참조를 null로 설정한다. 이에 따라 weakly reachable 객체는 unreachable 객체와 마찬가지 상태가 되고, 가비지로 판명된 다른 객체들과 함께 메모리 회수 대상이 된다.

 

참고) Strengths of Reachability

  • strongly reachable: root set으로부터 시작해서 어떤 reference object도 중간에 끼지 않은 상태로 참조 가능한 객체, 다시 말해, 객체까지 도달하는 여러 참조 사슬 중 reference object가 없는 사슬이 하나라도 있는 객체
  • softly reachable: strongly reachable 객체가 아닌 객체 중에서 weak reference, phantom reference 없이 soft reference만 통과하는 참조 사슬이 하나라도 있는 객체
  • weakly reachable: strongly reachable 객체도 softly reachable 객체도 아닌 객체 중에서, phantom reference 없이 weak reference만 통과하는 참조 사슬이 하나라도 있는 객체
  • phantomly reachable: strongly reachable 객체, softly reachable 객체, weakly reachable 객체 모두 해당되지 않는 객체. 이 객체는 파이널라이즈(finalize)되었지만 아직 메모리가 회수되지 않은 상태이다.
  • unreachable: root set으로부터 시작되는 참조 사슬로 참조되지 않는 객체

 

 

3) Listener와 Callback 등록 후 해지 하지 않는 것도 메모리 누수의 주범이 될 수 있다.

이 경우 마찬가지로 콜백을 약한 참조로(WeakHashMap에) 저장하면 가비지 컬렉터가 즉시 수거해 갈 수 있다.

+ Recent posts