티스토리 뷰
이번 포스팅에서는 JVM에서의 Garbage Collection 동작 방식에 대해 정리하려고 한다.
안드로이드 컴파일 과정을 공부하던 중에 Dalvik은 GC 방법으로 CMS 알고리즘을 사용하고, ART는 Customed CMS 알고리즘을 사용한다는 것을 알게 되었다. 또한 CMS 알고리즘보다 Customed CMS 알고리즘이 2배 이상의 성능을 낸다고 하는데, 여기서 CMS 알고리즘은 무엇이고 Customed CMS 알고리즘은 기존 CMS 알고리즘의 어느 부분을 개선하였길래 기존CMS 알고리즘 방식의 2배 이상의 성능을 내는 것인지 궁금해졌다.
이번 포스팅은 이 중에서 CMS GC 방식을 알기 위해 전체적인 Garbage Collection 방식을 공부한 것을 정리한 내용이다.
Java 언어를 동작 시키려면 Java 엔진 & 시스템 라이브러리들이 필요한데 이런 역할을 하는 것이 Java Virtual Machine의 약자인 JVM이라고 한다.
JVM의 성능향상이라고 하면 2가지로 정할 수 있는데, 첫 번째는 GC 시간 단축이고 두 번째는 JVM 엔진 성능 향상이다.
JVM 엔진은 여러 개가 있는데 대표적으로 다음과 같이 3가지가 있다.
-
Sun Microsystem / Oracle의 Hotspot VM - 대부분 Hotspot VM 사용
-
IBM의 J9
-
Oracle의 JRocket - 서버 쪽 성능이 좋음
이 중에서 Sun 회사의 Hotspot VM 기준으로 GC를 정리해보려고 한다.
Hotspot VM은 Client VM과 Server VM으로 성격이 나뉜다.
-
Client VM의 컴파일러 - 프로그램 시작 시간 최소화에 집중해 성능 향상
-
Server VM의 컴파일러 - 전체적인 성능 최적화에 집중해서 성능 향상
Java의 GC는 2가지 가정에 기반해 설계된다. 이 2가지 가정을 weak generational hypothesis라고 한다.
- 대부분의 객체는 금방 unreachable 상태가 된다. (최근 생긴 객체일수록 앞으로 사용 되지 않을 확률이 크다.)
-
오래된 객체에서 최근 생긴 객체로의 참조는 거의 없다.
위의 2가지 가정을 기반으로 설계된 Heap의 메모리 구조는 Young 영역과 Old 영역으로 나뉜다.
Young Generation (= Young 영역)
-
새로운 객체가 생성되는 위치로, 이 영역에서 메모리 회수를 하는 것을 Minor GC가 발생했다고 한다.
Old Generation (= Old 영역)
-
Young 영역에서 Tenuring Threshold(임계치) 보다 오래 살아남은 객체들이 넘어오는 영역이다.
-
이 영역에서 일어나는 GC를 Major GC 또는 Full GC라고 한다. 그럴 경우에는 STW(=Stop The World)가 발생해 JVM이 GC 스레드 빼고 모두 일시적으로 멈추는 현상이 발생한다.
-
그렇기 때문에 일반적으로 GC 튜닝을 한다는 것은 Old 영역에서 발생하는 STW 시간을 단축시키는 것을 의미한다.
+ 추가 설명
-
Young 영역에서 Old 영역으로 객체가 이동하는 것을 Promotion 되었다고 표현한다.
-
Old 영역에서도 GC에서 오래 살아남은 객체들은 Permanent Generation으로 이동하게 되는데, 이 영역에서도 Major GC로 unreachable한 객체를 회수한다. Major GC이기 때문에 STW가 발생한다.
-
Old 영역에 512 바이트 chunk의 card table이 존재한다.
-
이 table의 역할은 old 객체에서 young 객체로의 참조 정보를 담고 있어 Minor GC 실행 시 메모리 회수 대상인지 아닌지를 빠르게 판단한다.
-
약간의 overhead가 발생하지만 전반적인 GC시간을 줄여준다.
-
아래 그림은 유명한 Naver D2의 글에서 GC 영역을 도식화한 이미지를 가져온 것이다.
Java 메모리 구조를 그림으로 나타내면 아래와 같다.
-
From과 To 영역을 Survivor spaces라고 한다.
-
Young 영역 + Old 영역 + 추가적인 확장 영역 = 최대 Heap size
본격적으로 Minor GC와 Major GC의 동작 방식을 알아보기 전에, Hotspot VM에서 메모리 할당을 빠르게 할 수 있도록 도와주는 기술을 먼저 알아보자.
bump-the-pointer
-
메모리 영역 상에 객체가 차례대로 쌓이면 pointer가 가장 마지막 객체(top 객체)를 가리키고 있는다. 그렇게 해서 남은 영역에 새로운 객체가 들어갈 수 있는지 판단하여 들어갈 수 있으면 해당 객체를 영역에 넣고 그 객체를 pointer로 참조하는 방식이다.
-
이 방식을 사용하면 새로운 객체를 추가할 때 빠르게 메모리 할당이 가능하다.
TLAB (Thread-Local Allocation Buffers)
-
멀티 스레딩 환경에서 동시에 Young 영역에 객체를 할당하려고 하면 Lock이 발생해 지연 시간이 발생한다.
-
이를 해결하기 위해 Thread 별로 Young 영역을 작은 Buffers로 나누고 하나의 Thread는 자신이 가진 Buffer 영역에만 객체를 할당하도록 하는 것이다.
-
여기서 할당할 때 Thread 하나의 Buffer에는 하나의 Thread만 접근해서 객체를 할당할 수 있기 때문에 자유롭게 bump-the-pointer를 사용해도 Lock이 발생하지 않는다.
PLAB (Parallel-Local Allocation Buffers)
-
TLAB과 동일한 개념이지만 PLAB은 Old 영역에서 발생하는 Lock을 막기 위해 나온 해결책이다.
-
Old 영역에서 객체의 promotion에 의한 메모리 할당이 있을 때 마찬가지로 bump-the-pointer 방식을 사용하는데, 이는 멀티 스레드 환경에서 Lock이 발생할 수 있기 때문에 TLAB과 마찬가지로 Old 영역을 작은 Buffers로 나누고 하나의 Thread는 자신이 가진 Buffer 영역에만 객체를 할당할 수 있다.
-
하지만 이 방법을 사용하면 많은 Thread가 할당 받은 Buffer가 있어도 사용 되지 않거나, Buffer에 자투리 영역이 생기면서 메모리 단편화가 심해질 수 있다. 이 문제는 thread 개수를 감소 시키거나 Old 영역의 크기를 증가 시키는 것으로 해결할 수 있다.
기본 Minor GC 동작 방식
이제 Young 영역에서의 Minor GC 동작 방식을 본격적으로 알아보자.
Minor GC 동작 방식을 Copy & Scavenge 알고리즘이라고도 하는데, 속도가 빠르고 작은 크키의 메모리의 GC에 효과적이다.
GC 과정 중에 Tenuring Threshold가 나오는데, 이는 Young 영역에 있는 객체가 GC에서 살아 남을 때마다 Tenuring Threshold 값이 1씩 증가하다가 살아 남은 횟수가 Max 값에 도달하면 Old 영역으로 Promotion되는 것이다. 기본 Max 값은 31이고, 이는 MaxTenuringThreshold 옵션을 통해 변경이 가능하다.
편의상 Max 값을 2라고 하고 동작 과정을 시뮬레이션 하겠다.
1. 객체가 새로 생기면 Eden 영역에 할당한다.
2. Eden 영역이 꽉 차면 살아있는(=reachable) 객체를 확인한다.
3. 살아있는 객체를 To 영역으로 복사(Copy)한다.
4. Eden 영역의 객체를 없애서 Eden 영역을 비운다. (Scavenge) 이 작업 후에는 Eden 영역이 깨끗하게 비워진 상태이고, To로 복사된 살아있는 객체의 Tenuring Threshold 값이 1이 된다.
5. Eden 영역이 꽉 차면 Eden 영역과 To 영역의 살아있는 객체를 확인한다.
6. Eden 영역과 To 영역의 살아있는 객체를 From 영역으로 복사한다.
7. Eden 영역과 To 영역의 객체를 없애고, From 영역과 To 영역의 이름이 바뀐다. Tenuring Threshold 값이 1 증가한다.
8. Eden 영역이 꽉 차면 Eden 영역과 To 영역의 살아있는 객체를 확인한다.
9. 살아있는 객체의 Threshold 값을 확인한 뒤 Max 값에 도달한 객체를 Old 영역으로 Promotion한다. Promotion이 일어나면서 해당 객체의 Tenuring Threshold 값이 1 증가해 3으로 변경 된다.
10. Eden 영역과 To 영역의 살아있는 객체를 From 영역으로 복사한다.
11. Eden 영역과 To 영역의 객체를 없애고, From 영역과 To 영역의 이름이 바뀐다. Tenuring Threshold 값이 1 증가한다.
이렇게 위의 과정을 반복하며 Minor GC가 동작하게 된다.
여러가지 GC 동작 방식
Old 영역은 기본적으로 데이터가 가득 차면 GC를 실행한다.
이 때 발생하는 GC가 Major GC(=Full GC)이고, Major GC가 발생하면 GC 스레드를 제외한 나머지 스레드의 동작이 멈추게 된다. 이 현상을 STW(=Stop The World)라고 하고 JVM 멈춤 현상 이라고도 한다. Major GC가 발생할 때 이 현상은 피할 수 없기 때문에 GC 성능 튜닝을 할 때 이 STW 시간을 줄이는 데에 집중한다.
GC 알고리즘은 jdk 7 기준으로 5가지가 존재한다.
-
Serial GC
-
Parallel GC
-
Parallel Old GC (= Parallel Compacting GC)
-
CMS(Concurrent Mark Sweep)
-
G1 GC(Garbage First GC)
참고로 Major GC를 수행할 때 어디에 초점을 맞추느냐에 따라 2가지 방식으로 구분할 수 있다.
-
Throughput Collector - GC를 수행할 때 모든 리소스를 투입해서 GC를 빨리 끝내자. STW 시간을 단축시키는 것이 목적.
-
Serial GC, Parallel GC, Parallel Old GC
-
-
Low Pause(Latency) Collector - STW가 발생을 분산시켜 체감 pause time을 줄이자. GC 수행과 동시에 작업도 수행할 수 있다.
-
CMS, G1 GC
-
[Serial GC]
-
Mark-Sweep-Compaction 방식
-
Mark - Old 영역에 살아있는 객체를 표시
-
Sweep - Heap의 앞부분부터 살아 있는 객체만 남기고 없애기
-
Compaction - 살아있는 객체들을 Heap의 앞 부분부터 채우기
-
-
Minor/Major GC 모두 Single CPU를 사용한다.
-
Client VM의 기본 GC로 현재는 거의 사용하지 않는다.
[Parallel GC]
-
Serial GC와 동작은 동일하지만 Minor GC를 멀티 스레드로 수행한다는 점이 다르다.
-
GC overhead를 줄이고 Throughput이 증가되었다.
-
Server VM의 기본 GC이다.
-
CPU 코어가 1개일 때는 Serial GC로 동작한다.
-
Minor GC를 멀티 스레드로 수행하면서 Promotion 작업이 여러 스레드에서 동시에 발생하면 Corruption이 발생할 수 있다.
-> PLAB으로 해결함. (PLAB에 대한 설명은 글 앞부분을 참고)
[Parallel Old GC]
-
이전 GC 방식은 Mark-Sweep-Compaction에서 단일 스레드가 Old 영역 전체를 훑는 방식이었다면, Parallel Old GC에서는 멀티 스레드가 Old 영역을 논리적으로 균일하게 나눈 Region(2KB 정도의 chunk) 단위로 GC를 수행한다.
-
Mark-Summary-Compaction 방식
-
Mark
-
Old 영역의 Region을 구분하고, GC 스레드들을 각각 Region 별로 살아 있는 객체를 체크
-
이 때 살아있는 객체의 크기, 위치 정보 등 Region 별 통계 정보를 만듦
-
-
Summary
-
하나의 스레드가 GC를 수행하고 나머지 스레드는 어플리케이션을 수행
-
Region 별 통계정보로 살아있는 객체의 밀도가 높다고 판단되는 Region이 어디까지인지 Dense prefix를 구분한다.
-
Dense prefix 오른쪽 객체를 대상으로 compaction 수행
-
오랜 기간 참조된 객체일수록 앞으로도 사용할 확률이 높다는 가정 하에 Dense prefix 왼쪽 객체(오래된 객체)들은 Compaction에서 제외시켜 Compaction 범위를 줄이는 것
-
-
Compaction
-
Heap을 일시 정지 상태로 만들고 스레드들이 각 Region을 할당 받아 Compaction을 수행한다.
-
Dense prefix 오른쪽 Region들이 Destination과 Source로 구분된다.
-
Region 별로 할당된 스레드가 Garbage Object를 Sweep하고 Source Region에 남겨진 살아있는 객체는 Destination Region으로 이동한다.
-
결과
-
-
[CMS GC]
-
Concurrent Mark & Sweep의 약자로, Low latency Collection에 해당한다.
-
총 4단계로 동작한다.
-
Initial Mark (STW 발생)
-
Old 영역에 있는 객체 중에 GC 루트 객체에서 직접 참조하는 객체 또는 Young 영역의 객체에서 참조하는 객체를 빠르게 marking함으로써 STW 시간을 최소화 한다. (Single Thread로 수행)
-
-
Concurrent Mark
-
Initial Mark 단계에서 marking된 객체가 참조하는 객체를 대상으로 살아있는 객체를 추가 확인한다.
-
애플리케이션 작업과 동시에 수행해서 STW가 발생하지 않는다.
-
-
Concurrent Preclean
-
이전 단계가 애플리케이션과 동시에 실행되는 동안 일부 참조가 변경 되었다면, 이러한 상황이 발생할 때마다 JVM은 힙 영역인 “Card” 중에 변형 된 객체를 포함하는 “Card”를 “Dirty Card”로 표시 한다.
-
이 단계에서는 “Dirty Card”에 객체와 객체가 참조하는 객체도 marking한다.
-
-
Remark (STW 발생)
-
Concurrent Mark 과정에서 새롭게 추가된 GC 객체가 있을 수 있기 때문에 를 검증하는 단계로 추가 GC 대상이 있는지 확인한다.
-
멀티 스레드로 검증해서 STW 시간을 최소화 한다.
-
일반적으로 Young 영역이 비어있을 때 Remark 단계를 실행해서 최대한 STW이 적게 발생하도록 한다.
-
-
Concurrent Sweep
-
Old 영역에 모든 살아있는 객체가 marking되었으므로 이제 GC 대상 객체를 메모리에서 제거한다.
-
-
Concurrent Reset
-
CMS 알고리즘의 내부 데이터 구조를 재설정하고 다음주기를 위해 준비하면서 동시에 실행되는 단계이다.
-
-
장점
-
STW를 나눠서 실행해서 체감 pause time을 줄일 수 있다.
단점
-
Compaction 작업이 없다. 단편화가 계속 일어나다가 연속적인 메모리 할당이 어려울 정도로 단편화가 된 경우 Compaction을 진행하는데, 이 작업을 할 때 STW가 더 오래걸릴 수 있다.
-
FreeList 방법
-
Young 영역에서 promotion된 객체 크기를 계속 통계화해서 Free Memory 블록들을 붙이거나 쪼개 적절한 Free Memory Chunk에 할당한다.
-
그냥 할당하는 것보다 Young 영역의 부담만 가중되고 할당 시간이 오래 걸린다.
-
-
-
Floating Garbage 문제
-
CMS GC 동작 과정 중에 Concurrent Mark 과정이 끝나기 전에 참조가 끊겨 Garbage가 된 객체는 GC 대상으로 인식되지 않는다.
-
이 객체는 다음 GC에서 제거된다.
-
해결책으로 CMS Garbage Collector는 통계 정보를 계속 수집해서 적당한 GC Scheduling 시간을 잡는다.
-
[G1 GC]
-
문제가 많은 CMS를 대체하기 위해 만들어진 방법으로, 하드웨어의 발전으로 큰 메모리에서 좋은 성능을 내는 것에 초점을 두어 CMS에 비해 Pause 시간을 개선했다.
-
물리적 Generation 구분을 없애고 전체 Heap을 1~32MB 단위의 Region들로 재편했다. (약 2048개의 Regions들로 나눌 수 있다.)
-
각 Region은 상태에 따라 역할이 동적으로 부여된다.
역할 구분
-
Eden - 새로 생긴 객체들이 할당되는 영역
-
Survivor - 살아있는 객체들이 할당되는 영역
-
Old - 오래 살아있는 객체들이 할당되는 영역
-
Humongous
-
기존에 없던 영역
-
Region 크기의 50%가 넘는 큰 객체를 저장하기 위한 영역
-
GC 동작이 최적으로 동작하지 않는다.
-
-
Available / Unused
-
기존에 없던 영역
-
아직 사용되지 않는 영역
-
GC 동작 방법
-
Young GC (STW가 발생)
-
STW 시간을 줄이기 위해 멀티 스레드로 작업한다.
-
Eden/Survivor 영역에서 수행된다. GC 후 살아있는 객체를 다른 Survivor Region으로 이동시키고, 비워진 Region을 Available Region으로 변경한다.
-
-
Old GC
-
Initial mark (STW 발생)
-
Old Region 객체가 참조하는 Survivor Region을 찾는다.
-
-
Root region scan
-
Initial mark에서 찾은 Survivor Region에 대한 GC 스캔 작업을 진행한다.
-
-
Concurrent mark (일부 STW 발생)
-
전체 Heap 영역에 대한 스캔으로, 살아있는 객체가 존재하는 Region만 식별한다. 살아있는 객체가 없는 Region은 앞으로의 작업에서 제외된다.
-
-
Remark (STW 발생)
-
최종적으로 살아남은 객체를 식별한다.
-
-
Cleanup
-
살아남은 객체가 가장 적은 Region의 GC 대상을 제거한다. 이 작업에서 STW가 발생한다.
-
STW를 끝내고 GC로 제거되어 빈 영역은 Available Region으로 변경한다.
-
-
Copy
-
살아남은 객체들을 Available Region에 복사하며 Compaction을 수행한다.
-
-
객체 할당 방법
-
몇 개의 Region을 Eden 영역으로 지정한다.
-
이 Region에 새로운 객체가 할당된다.
-
Eden Region이 가득 차면 GC를 수행한다.
-
GC 수행 후 살아남은 객체를 Survivor 영역으로 이동한다.
-
1~4단계 반복하다가 오래 살아남은 객체를 Old 영역으로 이동한다.
특징
-
장점 - Young 영역에 집중하면 효율이 좋다.
-
단점 - Old 영역에서 단편화가 발생하고, Free List 사용으로 Promotion 시 STW 시간이 길어진다.
-
Region 별로 순차적인 작업이 진행되고 Garbage로만 가득 찬 Region이 발견되자마자 GC를 진행한다. (그래서 이름이 Garbage First GC이다.)
-
Remember Set
-
Region의 참조 정보를 저장하는 공간.
-
전체 Heap의 5% 미만의 공간을 차지한다.
-
외부에서 들어오는 참조 정보를 가지고 있어 Marking 작업 중에 Trace 일량을 줄인다.
-
정리
Serial GC
- Minor GC & Major GC를 하나의 스레드로 작업한다.
Parallel GC
- Minor GC는 멀티 스레드로 작업하고 Major GC는 하나의 스레드로 작업한다.
Parallel Old GC
- Minor GC & Major GC 모두 멀티 스레드로 작업한다. 그렇기 때문에 Multi CPU에 유리하고 Old GC의 처리량이 증가한다.
CMS GC
- Low Latency GC로 Compaction을 기본적으로 수행하지 않다가 단편화가 심해지면 수행한다.
G1GC
- CMS의 문제점을 개선하기 위해 만들어진 GC로 위의 4가지 방법과 구조가 다르다.
[참고한 사이트]
- Java Garbage Collection
- concurrent-mark-and-sweep
- Java 의 GC는 어떻게 동작하나?
- SLiPP::위키 #2 GC 방식
- Garbage collection
- [GC] Log 분석 가이드 (HotSpot 계열 JDK)
- Java - Garbage Collection(GC,가비지 컬렉션) 란?
- 오랜만에 Garbage Collection 정리
'Programming' 카테고리의 다른 글
[GitHub] SSH 방식으로 통신하기 (0) | 2021.08.15 |
---|---|
[GitHub] personal access token으로 인증하기 (for MacOS) (0) | 2021.08.14 |
- Total
- Today
- Yesterday
- SQL Server
- gson
- Algorithm
- SQLiteOpenHelper
- Python
- 파이썬
- python3
- kotlin
- AndroidStudio
- SQLD
- 알고리즘
- SOCKET
- 프로그래머스
- 위험권한
- 부스트코스
- DiffUtil
- Java
- covariance
- MSSQL
- personal access token
- Android
- ViewHolder
- 내용제공자
- SQL
- GitHub
- RuntimeException
- AsyncListDiffer
- RecyclerView
- 안드로이드
- pecs
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |