-
Notifications
You must be signed in to change notification settings - Fork 3
Description
Discussed in https://github.com/orgs/Study-2-Effective-Java/discussions/15
Originally posted by JoisFe December 13, 2022
아이템 7. 다 쓴 객체 참조를 해제하라.
C 혹은 C++ 언어를 사용해 보았는데 메모리를 직접 관리, 특히 생성 후 해제하는 것이 매우 귀찮고 혹은 해제하는 일을 잊어버리는 경우를 많이 경험했다.
하지만 자바를 접했을때 Garbage Collector가 알아서 다 쓴 객체 참조를 회수(해제) 하는 것을 보고 매우 편하다고 느꼈다.
GC(Garbage Collection)
시스템에서 더 이상 사용하지 않는 동적 할당된 메모리 블럭을 찾아 자동으로 다시 사용 가능한 자원으로 회수하는 것 그리고 자바 시스템에서 GC를 수행하는 부분을 JVM의 Garbage Collector.
JVM 메모리 구조
Garbage Collector의 타깃이 되는 영역은 Heap 영역.
Heap 영역은 자바 프로그램에서 사용되는 모든 인스턴수 변수(객체)들이 저장되는 영역.
Heap 영역 객체의 메모리 주소를 가지고 있는 참조 변수가 삭제되는 경우 등의 이유로 더 이상 해당 객체에 접근할 수 없는 객체를 Unreachable 객체라 부르고 이러한 객체를 주기적으로 Garbage Collector가 제거해줌.
Q) 그런데 Garbage Collector가 있는데 갑자기 '다 쓴 객체 참조를 해제하라' ???? Garbage Collector가 해주는게 아니였나?
메모리 관리를 직접 해줘야 하는 경우
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);
}
// // 코드 7-2 제대로 구현한 pop 메서드 (37쪽)
// public Object pop() {
// if (size == 0)
// throw new EmptyStackException();
// Object result = elements[--size];
// elements[size] = null; // 다 쓴 참조 해제
// return result;
// }
public static void main(String[] args) {
Stack stack = new Stack();
for (String arg : args)
stack.push(arg);
while (true)
System.err.println(stack.pop());
}
}Q) 해당 코드의 문제점은 ??
A) 메모리 누수
스택을 사용하는 경우 프로그램을 오래 실행하다 보면 Garbage Collector 활동과 메모리 사용량이 늘어나 결국 성능이 저하가 됨. (심한 경우 디스크 페이징 or OutOfMemoryError 발생하여 프로그램이 종료)
해당 코드의 메모리 누수 위치는 ? --> 스택에서 꺼내진(pop) 객체들을 Garbage Collector가 회수 하지 않음.
Q)스택에서 꺼내진 객체들은 더 이상 접근할 수 없는 unreachable 객체라 Garbage Collector가 회수하지 않나???
--> 그렇게 함부로 unreachable 객체로 판단해선 안된다.
위 코드에서 아직 element[size] 로 접근이 가능하기 때문에 unreachable 하지 않음.
A)
위 경우 스택이 해당 객체들의 다 쓴 참조 (obsolete reference)를 가지고 있기 때문
- obsolete reference : 앞으로 다시 쓰지 않을 참조.
앞의 코드에서는 elements 배열의 '활성 영역' 밖의 참조들이 모두 여기에 해당.
활성 영역은 인덱스가 size보다 작은 원소들로 구성
GC 언어 (java와 같은) 에서는 사실 의도치 않게 객체를 살려두는 메모리 누수를 찾기 매우 까다로움.
객체 참조 하나를 살려두면 Garbage Collector는 그 객체 뿐 아니라 그 객체가 참조하는 모든 객체(그리고 또 그 객체들이 참조하는 모든 객체 ~~~~)를 회수 하지 못함.
--> 따라서 단 몇개의 객체가 매우 많은 객체를 회수되지 못하게 하고 이로 인해 잠재적으로 성능을 낮춤.
GC의 동작 과정을 더 자세히 안다면 이해하기 쉽다고 판단된다.
GC 동작 과정 <-- 해당 링크를 참고하길 바란다.
해결 방안
매우 간단하다. 해당 참조를 다 썼을 때 null 처리 (참조 해제)하면 됨
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result
}다 쓴 참조를 null 처리 하게 되었을때 다른 이점
- 만약 null 처리한 참조를 실수로 사용하려 하면 프로그램은 즉시 NullPointerException을 던지며 종료
(null 처리를 하지 않았더라면 무언가 잘못된 일을 수행할 것.)
문제점
null 처리에 혈안이 되면 프로그램을 필요 이상으로 지저분하게 만듬
- 객체 참조를 null 처리하는 일은 예외적인 경우여야 함.
obsolete reference 처리 하는 다른 방법 (null 처리 보다 더 좋은 방법)
- 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것.
위 방법은 item 57에서 공부하기로 하자.
null 처리를 언제 할까?
1. 자기 메모리를 직접 관리하는 클래스에서 비활성 영역이 되는 순간
먼저 위의 Stack 클래스가 왜 메모리 누수에 취약한지부터 알아야 한다.
--> 스택이 자기 메모리를 직접 관리 하기 때문
- 위 스택은 객체 자체가 아니라 객체 참조를 담는 elements 배열로 저장소 풀을 만들어 원소들을 관리
- 배열의 활성 영역에 속한 원소들이 사용
- 비활성 영역은 쓰이지 않음
문제점
- 가비지 컬렉터는 비활성 영역이 쓰이지 않는다는 것을 알 수가 없음
(비활성 영역에서 참조하는 객체도 똑같이 유요한 객체)
해결책
- 비활성 영역이 되는 순간 부터 null 처리해서 해당 객체를 더는 쓰지 않을 것임을 가비지 컬렉터에 알려야함.
2. 캐시의 사용이 필요 없어졌을 때
- 캐시 역시 메모리 누수를 일으키는 주범.
객체 참조를 캐시에 넣고나서 캐시의 유효기간이 끝났지만 해제하지 않는 경우 그 동안 메모리 누수가 일어남.
문제점
캐시를 만들때 보통 캐시 엔트리의 유효 기간을 정확히 정의하기 어려움 따라서 언제 참조를 해제 해야할지 알기 어려윰.
해결책
이런 경우 쓰지 않는 엔트리를 때때로 청소 해줘야 함.
- Scheduled ThreadPoolExecutor 같은 백그라운드 스레드를 활용
- 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행
(LinkedHashMap은 removeEldestEntry 메서드를 사용하여 2번 방식으로 처리)
3. listener (혹은 callback)
문제점
클라이언트가 콜백을 등록만 하고 명확히 해지하지 않을시 콜백은 계속 쌓여감
해결책
콜백을 약한 참조(weak reference)로 저장하면 가비지 컬렉터가 즉시 수거.
(ex - WeakHashMap에 키로 저장)
