자바를 쓸 때 JVM 은 OS 에서 메모리를 할당받아 에플리케이션에 필요한 자원을 사용한다.
그 중 우리가 자주 쓰는 참조 타입들은 메모리의 힙 영역에 저장된다고 공부했었는데, 매번 쌓이는 힙 영역을 개발자가 관리하지 않아도 괜찮은 이유가 Java Garbage Collection 때문이라고 배웠다. (Execution Engine 부분의 GC.)
이번에는 이 Garbage Collection 에 대해 자세히 알아보도록 한다.
Garbage Collection
java 에서는 코드에서 명시적으로 메모리 할당을 해제하지 않는다.
자바의 가비지 컬렉터는 자동으로 다음의 동작을 한다
- 메모리 힙 영역의 객체 중, garbage(더 이상 참조하지 않는 객체) 를 찾아낸다.
- 찾아낸 garbage 를 처리해서 힙 메모리를 회수한다.
그러면 가비지 컬렉터는 이런 가비지(더 이상 사용하지 않는 메모리 참조) 를 어떻게 컬렉션(판별) 하는 것일까?
GC 의 Garbage 판단 : Reachability
GC 가 해당 객체가 가비지인지 아닌지 판별하는 방법으로 Reachability 라는 개념을 사용한다.
- reachable : 객체에 유효한 참조가 있음.
- unreachable : 객체에 유효한 참조가 없음. 가비지 컬렉션의 대상.
객체에서 한 객체는 다른 여러 객체를 참조(의존) 하고, 그 객체들은 또 다른 객체를 참조하는 참조 사슬구조를 이루는데, 이런 사슬구조에서 유효한 참조 여부를 결정하기 위해서는 유요한 최초의 참조가 있어야 한다.
이를 root set 이라 한다.
Runtime Area 의 구조이다.
Heap 영역의 객체에 대한 참조는 다음 4가지가 있다.
- 힙 내 다른 객체에 의한 참조 (사슬)
- Java 스택 영역, 즉 메서드 실행 시의 지역 변수와 파라미터에 의한 참조
- JNI 에 의해 생성된 객체에 대한 참조
- 메서드 영역의 정적 변수에 의한 참조
이들 중 처음의 다른 객체에 의한 참조를 뺀 3가지가 Root Set 으로, Reachable, UnReachable 을 판가름하는 기준이다.
위의 그림처럼 빨간색들은 객체들끼리의 참조는 있지만, 나머지 3가지의 참조는 없다.
오른쪽 아래처럼 reachable 객체를 참조하더라도, root set 이 유효하지 않으므로 Unreachable 이다.
이런 빨간색 객체들이 Unreachable 객체이며, GC 의 대상이 된다.
java.lang.lef
이 패키지의 클래스들을 사용하면, GC 의 동작을 조정할 수 있다.
예를 들어, WeakReference 클래스는 객체를 캡슐화한 WeakReference 객체를 생성한다.
이 객체는 root Set 으로 시작해도, GC 동작시 UnReachable 로 간주되어 회수된다.
GC 가 동작하여 어떤 객체를 WeakReference 로 판단하면, 해당 객체의 참조를 null 로 설정하기 때문이다.
강한 참조 (Strong Reference) Integer prime = 1; 와 같은 가장 일반적인 참조 유형이다. prime 변수 는 값이 1 인 Integer 객체에 대한 강한 참조 를가진다. 이 객체를 가리키는 강한 참조가 있는 객체는 GC 대상이 되지않는다. |
부드러운 참조 (Soft Reference) – SoftReference<Integer> soft = new SoftReference<Integer>(prime); 와 같이 SoftReference Class 를 이용하여 생성이 가능하다. 만약 prime == null 상태가 되어 더이상 원본(최초 생성 시점에 이용 대상이 되었던 Strong Reference) 은 없고 대상을 참조하는 객체가 SoftReference만 존재할 경우 GC대상으로 들어가도록 JVM은 동작한다. 다만 WeakReference 와의 차이점은 메모리가 부족하지 않으면 굳이 GC 하지 않는 점이다. 때문에 조금은 엄격하지 않은 Cache Library 들에서 널리 사용되는 것으로 알려져있다. |
약한 참조 (Weak Reference) – WeakReference<Integer> soft = new WeakReference<Integer>(prime); 와 같이 WeakReference Class를 이용하여 생성이 가능하다. prime == null 되면 (해당 객체를 가리키는 참조가 WeakReference 뿐일 경우) GC 대상이 된다. 앞서 이야기 한 내용과 같이 SoftReference와 차이점은 메모리가 부족하지 않더라도 GC 대상이 된다는 것이다. 다음 GC가 발생하는 시점에 무조건 없어진다. |
Effectice Java 3E 의 아이템 7 을 보면, 캐시같은 경우 WeakHashMap 을 사용하여 메모리 누수를 막아야 한다고 한다.
더 복잡한 캐시 기능의 구현에서는 위 ref 패키지의 클래스를 활용할 수 있다고 한다.
GC 의 가비지 컬렉션 과정
Mark & Sweep & Compact
가비지 컬렉션의 기본 프로세스는 Mark & Sweep & Compact 이다.
1. Marking
객체를 검사하여 참조되는 객체와 참조가 없는 객체를 마킹한다.
2. Sweep
체크된 UnReachable 객체들을 메모리에서 해제시키고 삭제한다.
3. compact
Sweep 이후 분산되어있는 객체들을 Heap 의 시작주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 나눈다.
이 과정에서 STOP-THE-WORLD 가 일어난다.
Stop-The-World
GC 가 일어났을 때, GC 의 실행을 위해 JVM 이 에플리케이션의 실행을 멈추는 것을 STOP-THE-WORLD 라고 한다.
STOP-THE-WORLD 가 발행하면 GC 를 실행하는 스레드 외에는 모든 스레드가 작업을 멈춘다.
GC 작업이 끝난 후에야 에플리케이션의 동작이 다시 실행된다.
어떤 GC 를 사용하든 STOP-THE-WORLD 는 발생하며, GC 튜닝이란 보통 이 시간을 줄이는 작업을 말한다.
명시적인 GC ?
System.gc() 를 사용하거나 객체에 null 을 넣는 행위는 명시적으로 GC 를 호출할 것 처럼 보인다.
하지만 이는 단지 GC 의 확률을 높이는 역할만 하기에, 생각한대로 동작하지 않는다.
또한, System.gc() 는 시스템의 성능을 저하시키기에 쓰면 안된다.
Weak Generation Hypothesis
개발자들이 GC 를 만들 때, 다음의 두 가지 가설을 가지고 만들었다고 한다.
- 대부분의 객체는 금방 UnReachable 상태가 된다.
- 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.
이 가정에 의거하여 Heap 메모리는 아래와 같이 설계된다.
Old 영역과 Young 영역으로 나눠진 것을 볼 수 있다.
아래의 그림과 설명들은 모두 선 마이크로 시스템 사 (자바 개발한 회사, 오라클에 합병됨) 의 JVM인 HotSpot JVM 을 기준으로 설명되었다.
그리고 Java 8 버전 이후임을 알아두자.
- Young Generation
- 새로 생성한 객체의 대부분이 위치한다.
- 대부분의 객체가 금방 접근 불가 상태가 되기 때문에 매우 많은 객체가 Young 영역에서 생성되고 사라진다.
- 이 영역에서 객체가 사라질 때 Minor GC 가 일어난다고 말한다.
- Old Generation
- 접근 불가 상태가 되지 않아 Young 영역에서 살아남은 객체가 Old 객체로 복사된다.
- 대부분 Young 영역보다 크게 할당한다.
- 크기가 큰 만큼 Young 영역보다는 GC 의 발생 빈도수가 적다.
- 이 영역에서 객체가 사라질 때 Major GC (Full GC) 가 발생했다고 말한다.
가비지 컬렉션의 과정
1. 새로운 객체가 생성되면, Heap 메모리의 Eden 영역 (Young Generation)에 할당되어 위치한다.
2. Eden 영역에 객체가 가득 차면, Minor GC 가 일어난다. 살아남은 (아직 사용중인) 객체는 Survivor 영역 중 한 곳으로 이동하며, Eden 영역은 비워지게 된다.
이 때, 어느 Survivor 이든 한곳으로만 이동한다.
3. 다음 Minor GC 가 일어나면 Survivor 0 (그림상) 과 Eden에서 살아남은 객체들은 다른 Survivor 영역인 Survivor 1 로 이동한다. 그리고 Survivor 0과 Eden 영역은 빈상태가 된다.
이 과정에서 알 수 있듯, Survivor 의 한쪽은 항상 빈 상태여야 한다.
두 Survivor 영역에서 객체가 존재하는 것은 정상적인 상황이 아니다.
4. Minor GC가 일어날때 마다 살아남은 객체들은 각 Survivor 영역을 이동하게된다.
5. 위와 같은 과정이 반복되면서 계속적으로 Survivor(Young Generation) 영역에서 살아남은 객체들은 Old 영역으로 이동된다.
이를 Promotion 이라한다.
6. Promotion의 반복으로 Old 영역이 가득차게 되면 Major GC가 일어난다.
Java 7 까지의 Sun HotSpot Heap Structure
위 자료들은 Java 8 부터의 자료이다.
Java 8 이전에 Hotspot JVM 이 만드는 Heap 영역은 아래와 같이 Permanent 영역이 있었다.
Permanent 영역은 Method Area 로, 이 영역에서 발생하는 GC 도 Major GC 의 횟수에 포함됬다고 한다.
자세한 설명은 아래
johngrib.github.io/wiki/java8-why-permgen-removed/
Old 영역에 있는 객체가 Young 영역의 객체를 참조하는 경우가 있을 때
- 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.
위의 가설은 GC 를 개발할 때의 가설이라고 했다.
하지만 이 말은 즉, 오래도니 객체에서 젊은 객체로의 참조가 존재하긴 한다는 소린데, 이 때는 어떻게 처리할까.
Old 영역에는 512바이트의 덩어리(chunk)로 되어 있는 카드 테이블(card table)이 있다.
카드 테이블에는 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때마다 정보가 표시된다.
Young 영역의 GC를 실행할 때에는 Old 영역에 있는 모든 객체의 참조를 확인하지 않고, 이 카드 테이블만 뒤져서 GC 대상인지 식별한다.
GC 의 방식
JDK 7 을 기준으로 5가지의 GC 방식 종류가 존재한다.
- Serial GC
- Parallel GC
- Parallel Old GC (Parallel Compacting GC)
- Concurrent Mark & Sweep Gc (CMS)
- G1 (Garbage First) GC
Serial GC
Young 영역에서의 GC는 앞 절에서 설명한 방식을 사용한다. Old 영역의 GC는 mark-sweep-compact을 사용한다.
Serial GC는 CPU 가 한개 즉 싱글스레드에서 적합하게 동작하도록 설계되었다.
GC 과정이 싱글스레드로 동작하기 때문에 Stop The World 시간이 길다.
Prarallel GC
GC 과정은 위의 Serial GC 와 같다.
그러나 Serial GC는 GC를 처리하는 스레드가 하나인 것에 비해, Parallel GC는 GC를 처리하는 쓰레드가 여러 개이다.
Parallel GC 는 Young Generation 의 Minor GC 에 대한 GC 를 병렬로 처리한다.
따라서 Serial GC보다 빠르게 객체를 처리할 수 있다.
아래 그림처럼 Stop the World 시간이 짧아지므로 성능이 비교적 좋다.
기본적으로 CPU 가 다수인 요즈음 컴퓨터에서는 Serila GC 보다 무조건 유리하다.
Parallel Old GC
이 방식은 JDK 5 부터 제공한 GC 방식이다.
Young Generation 뿐만 아니라 Old Generation 의 GC 즉 Major GC 도 병렬로 처리할 수 있다.
Parallel GC와 Old 영역의 GC 알고리즘만 다르다.
Mark-Sweep-Compact 대신 Mark-Summary-Compaction 단계를 거친다.
Summary 단계는 앞서 GC를 수행한 영역에 대해서 별도로 살아 있는 객체를 식별한다는 점에서 Mark-Sweep-Compact 알고리즘의 Sweep 단계와 다르다.
CMS GC
위 사진은 Serial GC 와 CMS GC 의 비교사진이다.
CMS 의 C 는 Concurrent 동시 라는 의미다.
위 다른 GC 에 비해 STW 시간을 최소로 하기 위해 나왔으며, GC 대상을 찾는 작업을 작업 스레드와 동시에 함으로써 가능하다.
하지만 동시에 하느라 CPU 사용량이 높다는 단점이 있다.
다음과 같은 과정으로 진행되며 동시라는 의미가 무엇인지 알아볼 수 있다.
- Initial Mark : 클래스 로더에서 가장 가까운 객체들, 즉 GC Root 에서 가까운 객체들 중 Reachable 한 객체들만 찾아 GC 대상을 판별한다. 탐색 범위가 한정되어 있기 때문에, 판별된 UnReachable 객체들의 GC에 대한 STW 시간이 매우 짧다.
- Concurrent Mark : initial Mark 단계에서 살아남은 객체들의 참조를 따라가며 GC 대상을 추적한다. 이 스레드는 다른 스레드가 실행되고 있을 때 같이 실행된다.
- Remark : Concurrent Mark 단계에서 새로 추가되거나 연결이 끊긴 객체가 있는지 검증한다. 이 과정은 STW 를 일으킨다. 병렬로 처리되어 STW 시간이 짧다.
- Concurrent Sweep : Remark 단계에서 확정된 GC 대상들을 메모리에서 해제시킨다. 멀티 스레드로 다른 스레드와 동시에 실행되기 때문에 STW 가 발생하지 않는다.
위에서 보는 것과 같이, STW 시간이 매우 짧기 때문에, 에플리케이션의 응답속도가 중요할 때 사용한다.
CMS GC 를 사용할 때에는 다음과 같은 단점이 있다.
- CMS GC 는 동시에 진행되는 특징 때문에, CPU 사용량이 높다는 단점이 있다.
- Compaction 과정이 기본적으로 제공되지 않고, 정말 필요할 때에만 진행된다. 따라서 흩어진 메모리들이 더 많으며, Compaction 과정이 다른 GC 보다 더 오래 걸린다. CMS GC 를 이용할 때에는 이 Compaction 과정이 어떤 빈도수로 실행되는지를 잘 파악하여 사용해야한다.
G1 GC
위의 GC 알고리즘들로는 큰 메모리에서 좋은 성능(짧은 STW)을 내기 힘들었기 때문에 CMS GC 를 대신하여 큰 메모리에서 짧은 시간으로 동작 가능한 G1 GC 가 만들어졌다.
G1 GC는 앞서 살펴본 GC와는 다른 방식으로 힙 메모리를 관리한다.
Eden, Survivor, Old 영역이 정해진 위치가 아니라 논리적인 위치에 존재한다.
전체 힙 메모리 영역을 Region 이라는 특정한 크기로 나눠서 각 Region의 상태에 따라 그 Region에 역할(Eden, Survivor, Old)이 동적으로 부여되는 상태이다.
G1 GC 에서는 메모리를 약 2000 개의 논리적인 단위(Region)로 나누어 사용한다.
각 Region 은 1 ~ 32 Mb 크기이다.
위를 보면 Eden, Survivor, Old 영역으로 나뉘고, 아직 사용되지 않은 영역이 있다.
추가적으로 Humongous Region 도 있는데 이는 Region 크기의 50%를 초과하는 큰 객체를 저장하기 위한 공간이다. Oracle 공식문서를 보면 글이 쓰여질 당시에는 이 Humongous 영역에서 큰 객체의 수집이 최적화되지 않았으니 큰 객체를 만들지 말라고 한다.
www.oracle.com/technetwork/tutorials/tutorials-1876574.html
G1 GC - Young 영역
위에서도 언급했듯, 각 Region 은 물리적으로 연속적인 메모리에 있을 필요가 없다.
Young 영역의 GC 가 실행되면, 살아남은 객체들이 Survivor 영역으로 이동하며 일정 임계값을 넘은 객체는 Old 영역으로 promotion 된다.
이 과정에서 STW 가 발생하는데, 다음 있을 Young 영역에서의 GC 를 위한 Eden 영역과 Survivor 영역의 크기 재계산이 같이 진행된다.
G1 GC 의 Young 영역에서의 GC 는 멀티 스레드를 이용하여 병렬적으로 처리되기 때문에 STW 시간이 짧다.
G1 GC - Old 영역
Old 영역에서 일어나는 GC 를 Major GC 또는 Full GC 라고 했다.
G1 GC 의 Major GC 에서는 어떤 일들이 일어나는지 알아보자.
총 5개의 단계가 존재한다.
1. Initial Marking
Old Region 의 객체들을 참조하는 Survivor 객체들을 마킹한다.
Young GC 에 piggyback 된다. (Young GC 와 같이 이루어진다는 말인것 같다.)
따라서 STW 를 유발한다.
2. Root Region Scanning
Initial Marking 영역에서 찾은 Survivor 영역 객체에 대해 GC 의 대상인지 스캔한다.
멀티 스레드로 다른 스레드와 함께 동작한다.
Young GC 가 끝나기 전에 끝나야한다.
3. Concurrent Marking
힙의 모든 영역에서 GC 대상을 찾는다. (살아있는 객체를 찾는다.)
멀티 스레드로 동작하며, Young GC 에 의해 중단될 수 있다.
그림에서 X 로 표시된 것과 같이, Region 의 객체들이 Garbage 라고 판단되면 다음의 Remark 단계에서 즉시 제거된다.
4. Remark
Concurrent Marking 에서 진행중이던 마킹작업이 완료되고, 모든 객체가 Garbage 라고 판단된 Region 들이 메모리에서 해제된다.
STW 가 발생한다.
이후 모든 Region liveness Region 이 계산된다.
CMS GC 보다 훨씬 빠른 snapshot-at-the-beginning (SATB) 알고리즘을 이용한다.
SATB 는 STW 이후 살아남은 객체를 마킹하는 알고리즘이다. 즉 살아남을 객체를 찾는 알고리즘이다.
5. Copying/Cleanup Phase
살아남은 객체와 메모리가 해제된 Region 의 계산을 수행한다. (STW 발생)
Scrubs the Remembered Sets. (STW 발생)
빈 영역을 재설정하고 사용 가능 Region 으로 되돌린다. (Concurrent)
"liveness" 이 가장 낮은 Region 을 선택(collect)한다. 즉 가장 빨리 collect 할 수 있는 Region 을 선택한다.
그림에서 보듯, 선택 범위는 Young 영역과 Old 영역 모두가 될 수 있다.
선택된 영역의 객체들은 새로운 Region 으로 복사된다.
Copying/Cleanup Phase 이후
위 단계에서 collect 된 객체들이 녹색과 파란색 영역으로 compact 된 것을 볼 수 있다.
참고자료
www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
www.oracle.com/technetwork/tutorials/tutorials-1876574.html
d2.naver.com/helloworld/329631
mirinae312.github.io/develop/2018/06/04/jvm_gc.html
'Java' 카테고리의 다른 글
List.of() vs Arrays.asList() vs Collections.unmodifiableList() (0) | 2021.02.08 |
---|---|
Enum 활용(람다식 사용하기) (0) | 2020.12.18 |
함수형 인터페이스 정리 (0) | 2020.12.18 |
인터페이스 vs 추상 클래스 (0) | 2020.12.18 |
JAVA8) 스트림 API (0) | 2020.12.11 |