JAVA/Effective Java
item7. 다 쓴 객체 참조를 해제하라
Garonguri
2022. 4. 16. 16:32
728x90
Java의 특징 : GC! GC가 있어 다 쓴 객체를 알아서 회수해간다.
- 따라서 메모리 관리를 개발자가 신경쓰지 않아도 될까? -> NO!
-> 자칫하다간 메모리 누수 문제가 일어날 수 있다. 메모리를 직접 관리하는 클래스는 개발자가 메모리 누수에도 신경써야 한다.
[메모리 문제가 일어날 수 있는 경우]
1. Stack 사용 시
스택은 자기 메모리를 직접 관리한다. 배열로 저장소 풀(pool^.^)을 만들어 원소들을 관리한다.
가용 영역은 사용되며, 유효 영역 밖의 원소는 사용하지 않는데, GC는 이러한 정보를 해석할 수 없다.
// Can you spot the "memory leak"?
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
this.ensureCapacity();
this.elements[size++] = e;
}
/*-------------- Bad ------------------*/
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return this.elements[--size];
}
/*-------------- Good ------------------*/
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object value = this.elements[--size];
this.elements[size] = null;
return value;
}
/***
* 원소를 위한 공간을 적어도 하나 이상 확보한다.
* 배열 크기를 늘려야 할 때마다 배열의 크기가 대략 2배씩 늘어난다.
*/
private void ensureCapacity() {
if (this.elements.length == size) {
this.elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
- Bad code
- 스택에 원소를 계속 쌓다가 원소를 pop했을지라도, 스택이 차지하고 있는 메모리는 줄어들지 않는다.
- 스택의 구현체는 필요 없는 객체에 대한 참조를 그대로 가지고 있다.
- pop()메소드의 this.element[--size]와 같은 경우, 실제 값은 삭제되지 않는다. 이런 경우 메모리 누수가 발생할 수 있다.
- 즉 size보다 작은 부분의 범위 내에서 사용가능하지만, size보다 큰 부분(활성 영역 밖)에 있는 값들은 사용 불가하며 메모리만 차지하고 있다.
- 따라서, 비활성 영역의 객체가 더이상 사용하지 않는다는 것을 GC에게 알려주기 위해 null처리를 해서 GC에게 알려야 한다.
왜 이런 경우가 생기는 걸까? -> 다 쓴 객체 참조를 여전히 가지고 있기 때문이다.
어떻게 해결할 수 있을까? -> 다 쓴 객체의 참조를 해제하면 된다.
- Good code
- 스택에서 꺼낼 때, 해당 위치에 있는 객체를 null로 초기화해준다. 따라서 GC가 참조를 해제한다.
- 메모리 누수도 막을 수 있으며, 해당 요소가 null로 초기화한 공간을 사용하려 하면 nullpointException을 발생시켜 주기 때문에, 프로그램 오류를 조기에 발견할 수 있다.
그렇다고 필요 없는 객체가 생길때마다 null처리를 하면 안된다.
객체를 null로 초기화 하는 것은 매우 예외적인 경우로, 과용하면 프로그램을 지저분하게 만들 수 있다.
필요없는 객체를 정리하는 최선의 방법은
참조를 담은 변수를 유효 범위 밖으로 밀어내는 것이다.
(로컬 변수는 유효범위 밖으로 넘어가면 GC가 정리해주므로)
item 57에도 나온대요
2. 캐시
객체 참조를 캐시에 넣고. 객체를 다 쓴 뒤에도 캐시에 그대로 두는 경우는 메모리 누수를 발생시킬 수 있다.
[해결 방법]
1. WeakHashMap을 사용해 캐시를 만든다.
- 한계 : 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아있는 캐시가 필요한 경우에만 유용한 방법이다.
2. 캐시 엔트리 유효 기간을 정한다.
- 시간이 지날 수록 엔트리의 가치를 떨어트리는 방식을 흔히 사용한다.
- 한계 : 캐시 엔트리 유효 기간은 정확히 정의하기 어렵다.
- 이런 경우, 쓰지 않는 메모리를 청소해야 한다.
- 백그라운드 스레드(Scheduled ThredPoolExecutor)를 활용하거나, 캐시에 새 엔트리를 추가할 때 부가적으로 기존 캐시를 비우는 일을 수행한다.
- LinkedHashMap의 경우 removeEldestEntry 메서드를 사용한다.
3. 리스너, 콜백
- Callback이란? : 이벤트가 발생하면 특정 메소드를 호출해 알려주는 것.
- Listner이란? : 이벤트가 발생하면 연결된 리스너들에게 이벤트를 전달한다.
- 클라이언트 코드가 콜백을 등록만 하고 해지하는 방법을 제공하지 않는다면, 콜백이 쌓여 메모리 누수가 일어날 수 있다.
- 이런 경우 콜백을 Weak reference로 저장해 GC가 수거해갈 수 있게 한다.
- ex) WeakHashMap에 키로 저장한다.
728x90