자바에서는 가비지 컬렉터(GC)가 사용하지 않는 객체들에 대해서 메모리를 알아서 관리해준다.
먼저 가비지 컬렉션에 대해 간단하게만 짚고 넘어가보자
# GC (Garbage Collection)
힙 영역 내의 참조되지 않는 객체가 아닌 Reachability 라는 개념을 통해 도달되지 않는 객체를 메모리에서 해제하여 메모리 관리를 해준다.
가비지 컬렉션의 동작 방식
Heap 에서의 Young 영역과 Old영역은 서로 다른 메모리 구조기 때문에, 세부적으로는 가비지 컬렉터가 다르게 동작하지만,
기본적으로 가비지 컬렉션은 2가지의 공통된 단계를 거친다.
- Stop The World
가비지 컬렉션을 실행하기 위해 JVM이 실행중인 GC를 진행하는 쓰레드를 제외한 모든 쓰레드의 작업을 중단하여 애플리케이션의 실행이 멈추게 된다. 이후, GC 작업이 완료되고 쓰레드들의 작업은 재개된다.
- Mark and Sweep
GC는 Stop The World 이후, 스택의 모든 변수 혹은 Reachable 객체들을 스캔하며 참조되고 있는 객체들에 대해 탐색을 진행한다. 이를 통해 사용되고 있는 메모리를 식별한다. (Mark)
이후, 마킹되지 않은 객체들에 대해서 메모리를 회수한다.(Sweep)
# Minor GC 의 동작 방식
1. 새로 생성한 객체는 Eden 영역에 할당
2. 객체가 추가적으로 생성되어 결국 Eden 영역은 포화상태가 되고 Minor GC가 실행 (다시 1번으로)
- 이 때 Eden 영역에서 사용되지 않은 객체의 메모리는 회수
- Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동시킴
3. 결국 Survivor 영역도 포화가 된다면 GC 이후 살아남은 객체들에 대해 다른 Survivor 영역으로 이동시킨다.
- 이 과정을 통해서 1개의 Survivor 영역은 반드시 빈 상태
4. 이러한 과정을 반복하여 계속해서 살아남은 객체들은 Old 영역으로 이동(Promotion)
# Major GC 의 동작 방식
Major GC는 객체들이 계속 Promotion되어 넘어와 Old 영역의 메모리가 부족할 때 발생한다.
이 때 Old 영역은 Young 영역보다 크고, Young 영역을 참조할 수 있기 때문에 GC 과정이 오래 걸린다는 특징이 있다.
(반대로 Minor GC는 느리다.)
내용 참조 - https://mangkyu.tistory.com/118
이처럼 훌륭한 GC 지만, GC도 메모리를 회수해갈 수 없는 메모리 누수가 코드상으로 발생할 수 있다.
public class Stack{
private Object[] elements;
private int size = 0;
...
public Object pop(){
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
}
- 해당 스택은 pop 된 객체에 대해 가비지 컬렉터가 메모리를 회수하지 않는다.
- 이유는 pop된 객체들에 대해서도 Stack 이란 클래스가 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문
객체 참조 하나를 살려두게 된다면, 해당 객체가 참조하는 모든 객체, 그리고 또 연쇄적으로 참조하는 객체들...에 대해서도
가비지 컬렉터는 메모리를 회수해가지 못한다.
→ 잠재적 악영향의 원인
다 쓴 참조를 해제하는 pop의 예시 코드는 다음과 같다.
참조를 완료했다면 null 처리 (참조 해제)를 해주면 된다.
public Object pop(){
if(size ==0){
throw new EmptyStackException();
}
Object result = elements[--size];
elemens[size] = null; //해제하자!
return result;
}
null 처리를 통해서 null 처리한 참조를 사용하려고 시도할 때 NullPointerException 을 통해 종료시킬 수 있어
조기에 오류를 잡을 수 있다.
# Null 처리 or 메모리 누수에 집중해야 할 때
- Stack
스택은 본인의 메모리를 직접 관리하기 때문에 메모리 누수에 취약하다
GC는 비활성 영역에 대해 알 방법이 없기 때문에 null 처리를 통해서 해당 객체는 사용하지 않는다는 것을 GC에게 알려야한다.
- Cash
객체 참조를 캐시에 넣고, 해당 객체 사용이 마친 뒤에도 캐시에 놔두는 것도 메모리 누수
- 해법
- WeakhashMap 을 통한 캐시 생성 - 다 쓴 enrty는 자동 제거
- LinkedHashMap을 통해 removeEldestEntry() 로 사용하지 않는 엔트리 제거
- Linstener & Callback
- 콜백 등록 후 해지하지 않는다면 계속해서 콜백은 쌓여간다..
- 콜백을 weak reference(약한 참조)로 저장하여 GC가 수거해가도록 하는 것이 좋다
- WeakHashMap
메모리 누수는 표면적으로 잘 보이지 않기 때문에 해당 문제가 발생하지 않도록
코드를 작성할 때 메모리 누수에 취약점이 있는지 고려하고, 예방 방법에 대해 숙지하자
'책 > Effective Java' 카테고리의 다른 글
인터페이스가 추상 클래스보다 우선인 이유 (0) | 2022.02.06 |
---|---|
equals 재정의 (0) | 2022.01.16 |
불필요한 객체 생성 피하는 방법 (0) | 2022.01.09 |
다중 instance 방지 & 의존성 주입[Dependency Injection] (0) | 2022.01.04 |
Singleton 보장 방법 (0) | 2022.01.03 |