티스토리 뷰
개요
Garbage Collector(GC)는 Heap 메모리에서 참조되지 않는 객체를 식별하여 메모리에서 삭제하는 기능이다. JVM은 GC를 수행하기 아래의 과정을 거친다.
GC Steps
-
Marking
메모리 중 어떤 부분이 사용되지 않고 있는지 체크하는 단계이다. 아래 그림 1을 참고하자.
모든 메모리를 체크해야한다면 GC에게 큰 부담이 될 수 있다. JVM은 이를 해결하기 위해 세대별로 메모리를 구분하여 GC를 따로 적용하는 아이디어를 채택하였다. 이는 뒤에서 설명하겠다.
-
Normal Deletion
이 단계는 Marking으로 찾아낸 비참조 객체를 삭제하는 단계이다. 객체 삭제 후 메모리는 아래 그림 2와 같다. 삭제 후에 생기는 빈 공간은 Memory Allocator가 참조하고 있어서, 메모리를 할당해야 할 일이 생기면, 빈 공간을 찾아준다.
-
Deletion with Compacting
GC 수행 후 효율적이고 빠른 메모리 사용을 위해 Compaction을 고려할 수 있다. Compaction을 하면 아래 그림 3처럼 메모리가 압축되어 Memory Allocator가 메모리를 할당하기 훨씬 쉬워진다.
Generational GC
GC를 할 때 모든 객체를 참조해야 한다면 JVM에 큰 부담이 된다. 그러나 GC 개발자들은 통계적으로, 생성된 객체 대부분은 수명이 짧은 것을 알아냈다. (아래 그림 4)
이를 이용해 JVM의 성능을 향상할 수 있는 방법으로, 세대별로 메모리를 나누어 GC를 적용하는 방법을 채택했다. 아래 그림 5는 Hotspot JVM의 힙메모리 구조이다.
Hotspot JVM의 Heap Memory는 크게 3개의 세대로 나뉘는데 각각 아래와 같은 특징을 가진다.
-
Young Generation
Young Generation은 다시 Eden, Survivor0, Survivor1 영역으로 나뉜다.
객체가 생성되면 Eden 영역에 생성된다. Eden 영역이 꽉차게되면 Minor GC가 발생한다. Minor GC에서 살아남은 객체는 Survivor 영역 중 하나로 이동한다. Minor GC가 발생할때마다 살아남은 객체의 Age가 증가하는데, Age가 임계치를 넘은 객체는 Old 영역으로 이동하게 된다.
-
Old Generation
Young Generation에서 일정 Age가 될때까지 살아남으면 그 객체는 Old Generation으로 옮겨지게 된다. Old 영역은 가득차게 되면 Major GC가 발생한다. Major GC는 보통 Minor GC보다 훨씬 느린데, 그 이유는 Young 영역을 제외한 모든 살아있는 객체를 검사해야하기 때문이다. GC 수행 시간은 종류에 따라 다르다.
-
Permanent Generation
Permanent 영역은 클래스와 메서드의 메타데이터가 저장되는 공간이다.
JVM은 런타임에 참조하는 클래스의 메타데이터를 Permanent 영역에 적재하게 된다. 만약 적재된 클래스가 더이상 필요 없고, 다른 새로운 클래스를 위한 Permanent 영역의 크기가 모자르다면, GC는 적재된 클래스를 수집하게 된다. Permanent 영역의 GC는 Full GC(Major GC를 포함한 Heap 영역 전체에 대한 GC) 안에 포함 되어있다.
Garbage Collector Process
객체 생성과 소멸에 따른 GC 시나리오를 살펴보자. 오라클 GC 설명을 그대로 가져왔다.
-
객체가 생성되면 Eden 영역에 할당된다.
위 그림에선 Survivor 영역에 객체가 존재하지만, 초기엔 Survivor 영역은 모두 비어있는 상태이다.
-
Eden 영역이 가득차게되면 Minor GC가 실행된다.
Minor GC가 발생하게 되면 참조되고 있는 객체(살아있는 객체)는 Survivor 영역 중 하나로 옮겨지게 되고, 비참조 객체는 삭제된다. 그 후엔 Eden 영역은 완전히 비어있게 된다.
-
Minor GC의 반복
Eden, Survivor 영역 모두에서 이루어지며, 살아있는 객체는 비어있는 Survivor 영역으로 옮겨지게 된다.
이 때, 살아있는 객체들은 각자 Age를 가지고 있는데, Minor GC가 발생할 때마다 살아있는 객체 각자의 Age가 1씩 증가된다.
위 그림 8을 보면, Minor GC 후에 Survivor 0 영역(S0)이 비게 된다. 그 다음 Minor GC 때는 아래 그림 9처럼 Survivor 1 영역(S1)이 비게 된다.
이처럼 Minor GC가 발생할 때마다 S0, S1 영역을 번갈아가며 사용한다. (Survivor 영역 중 하나는 항상 비어있게 된다.)
JVM 옵션으로 Eden 영역과 Survivor 영역의 비율을 조정할 수 있다.
-XX:SurvivorRatio=n
-
Old 영역으로 이동
Minor GC가 반복되고 객체들의 Age가 증가하여 임계값(threshold)을 넘어가면 Old 영역으로 이동하게 된다.
위 그림 10에선 임계값이 8로 가정한 그림이다. 임계값은 아래의 JVM 옵션으로 설정할 수 있다.
-XX:MaxTenuringThreshold=n
-
Old 영역(Tenured)이 가득차게 되면 Major GC가 발생한다.
GC Algorithms
Minor GC와 Major GC가 발생할 때 모든 애플리케이션 스레드가 정지하는데, 이를 Stop-The-World(STW)라고 부른다.
GC 개발자들은 위에서 설명한 GC 프로세스를 구현하는 것 외에도, Stop-The-World 시간을 줄이기 위한 다양한 알고리즘을 개발했다.
GC 알고리즘을 Hotspot JVM을 기준으로 알아보자.
-
Serial GC
JDK 5, 6에서 기본으로 사용되는 GC이다. 싱글 스레드로 Minor GC와 Major GC를 수행하기 때문에 Stop-The-World 시간이 다른 GC에 비해 상대적으로 긴 편이다.
따라서, Serial GC를 서버 환경에서 사용하는 것은 바람직하지 않다. 하지만 빠른 응답속도가 필요하지 않거나 클라이언트 스타일의 장비에선 사용할 수도 있다.
Serial GC를 활성화하려면 아래의 옵션을 사용하면 된다.
-XX:+UseSerialGC
-
Parallel GC
Minor GC를 수행할 때 멀티 스레드를 사용하는 GC이다. 기본적으로 JVM이 구동되는 환경의 CPU 코어 개수에 맞춰서 N개의 GC가 수행된다. 이는 아래 옵션으로 스레드 수를 변경할 수 있다.
-XX:ParallelGCThreads=스레드수
Parallel GC를 활성화 하기 위해 2가지 옵션이 있다. 참고로 Parallel GC를 활성화 해도 장비의 CPU가 단일코어면 Serial GC로 동작한다.
-XX:+UseParallelGC
위 옵션을 사용하면 Minor GC만 멀티 스레드로 동작하고, Major GC는 싱글 스레드로 동작한다.
-XX:+UseParallelOldGC
위 옵션을 사용하면 Minor GC, Major GC 둘 다 멀티 스레드로 동작한다. 또한 Mark, Sweep 뒤에 이루어지는 Compaction도 멀티 스레드로 이루어진다.
-
Concurrent Mark Sweep(CMS) GC
CMS GC는 Old 영역을 대상으로 이루어진다.(Young 영역은 Parallel GC)
CMS GC는 애플리케이션 스레드와 병렬로 동작함으로써 Stop-The-World의 시간을 줄인다. CMS GC는 이 시간을 더 줄이기 위해서 Compaction도 수행하지 않는다.
다른 GC에 비해서 Stop-The-World 시간이 짧다. 하지만 Compaction을 수행하지 않으면 당연히 메모리 단편화가 일어나게 되고, 어느 순간 메모리 할당이 불가능해질 수 있다.
CMS GC는 이 때 Concurrent mode failure 경고가 나면서 Compaction 작업을 수행하는데, 다른 GC의 Stop-The-World 시간보다도 긴 시간이 소요돼서 서버 환경에서는 위험할 수 있다.
이런 CMS GC의 단점을 해결하고자 G1 GC가 나오게 됐고, CMS GC는 JDK 9부터 deprecated 되었다.
CMS GC를 활성화하려면 아래 옵션을 사용하면 된다.
-XX:+UseConcMarkSweepGC
위 그림 12를 보면 CMS GC는 4단계로 나뉘는 것을 알 수 있으며, 각각 아래와 같다.
- Initial Mark: GC Root에서 참조 Tree에서 제일 상단에 있는 객체만 선별하는 과정. 탐색 깊이가 얕아서 STW 시간이 매우 짧다.
- Concurrent Mark: 애플리케이션 스레드와 병렬로 동작하며, Initial Mark 단계에서 선별된 객체부터 Tree 탐색을 시작하여 GC 대상을 선별한다.
- Remark: Concurrent Mark 단계의 결과를 검증한다. GC의 대상으로 추가된 객체를 선별하는 작업을 한다. STW가 발생하는데, 이 시간을 줄이기 위해서 멀티스레드로 동작한다.
- Concurrent Sweep: GC 대상인 객체들을 메모리에서 제거한다.
위 4가지 단계를 그림으로 나타내면 아래 그림 13과 같다.
(출처: https://mirinae312.github.io/develop/2018/06/04/jvm_gc.html) -
G1 GC
JDK 7 Update 4부터 공식적으로 도입된 서버형 GC로, 대량의 힙과 멀티 프로세서 환경에서 사용되도록 만들어진 GC이다.
CMS GC와 다른 점은 G1 GC는 메모리를 페이징 하듯이 논리적인 단위(Region)로 나눠서 관리한다는 것이다. Region이라는 논리적인 단위로 메모리를 관리하여 CMS와 달리 Compaction 단계를 진행하고 메모리 단편화 문제를 없앴다. 또한 STW의 시간을 예측할 수 있다는 것이 G1 GC의 큰 장점 중 하나이다.
G1 GC는 Initial Mark 단계를 멀티 스레드로 수행한다. 이 단계 후에 G1 GC는 Region 중 어떤 Region이 GC후에 메모리를 많이 반환할지 알게된다. G1 GC는 이런 Region을 먼저 수집하기 때문에 Garbage First GC라고 이름지어졌다.
G1 GC는 목표한 STW 시간에 최대한 맞도록 얼마만큼의 Region을 수집할지 산정한다.
그 후 G1 GC는 비참조 객체를 수집하며 살아있는 객체는 하나의 Region에 이동하는 작업을 진행하는데, 이 과정에서 Compaction이 이루어진다. G1 GC는 이 시간을 최대한 줄이기 위해 멀티 스레드로 작업을 수행한다.
G1 GC를 활성화하려면 아래 옵션을 사용하면 된다.
-XX:+UseG1GC
이제 G1 GC가 단계별로 어떻게 동작하는지 자세히 알아보자
-
G1 GC의 힙 구조
G1 GC는 위 그림 15처럼 Heap 메모리를 Region 단위로 나눈다. Region의 크기는 JVM이 시작될 때 정해지며, 1~32mb의 약 2000개 지역으로 Heap이 나뉜다.
그 후 Region은 Eden, Survivor, Old 영역으로 나뉜다. GC에서 살아남은 객체들은 이 영역에 따라 복사, 이동된다.
-
G1의 Young GC
위 그림 16은 Region을 Young 영역과 Old 영역에 따라 색깔별로 나눠놓은 그림이다.
G1의 Young GC가 발생하면 아래 그림 17처럼 Young GC에서 살아남은 객체들이 한개 이상의 Survivor Region에 차곡차곡 이동된다.
이 과정에서 STW가 발생한다. 위에서 설명했듯이 멀티 스레드로 병렬로 처리되기 때문에, STW 시간이 최소화된다. 또한 이 과정에서 G1이 Eden과 Survivor 사이즈를 계산해서 어딘가에 저장해놓는데, STW 목표시간과 Region의 크기를 조정하는데 사용된다. (자세히 어떤식으로 사용되는지는 알아내지 못했다.)
G1의 Young GC가 끝난 후에는 위 그림 18처럼 살아남은 객체가 Survivor 혹은 Old Region으로 이동되어 Compaction된 상태가 된다.
-
G1의 Old GC
G1의 Old GC는 CMS GC처럼 STW를 최소화하는 것을 목표로 두고 있다. 아래는 G1 GC가 이루어지는 과정을 단계별로 나눠놓은 것이다.
앞에서 소개한 다른 GC와 다르게 G1 GC는 살아있는 객체(live object)를 찾아서 비어있는 Region에 이동 후 남은 객체들을 삭제하는 방식을 사용한다.
-
Initial Mark
STW가 발생한다. 보통 Young GC의 Mark 단계에서 같이 이루어지는데, Survivor Region 중 Old 영역의 객체를 참조하고 있는 객체가 존재하는 Region을 식별한다.
-
Root Region Scanning
Initial Mark 단계에서 식별한 Region에서 Old 영역의 객체를 참조하고 있는 Young 영역의 객체를 식별한다. 이 과정은 애플리케이션 스레드와 함께 동작한다.
-
Concurrent Marking
Heap 전체에 걸쳐 참조되고 있는 객체(live object)를 찾는다. 이 과정은 애플리케이션 스레드와 함께 동작하는데, Young GC에 의해 interrupt 될 수 있다.
이 과정에서, 완전히 비어있는 Region을 찾게되면 Unused 상태로 만든다.
-
Remark
STW가 발생한다. Concurrent Mark 단계의 결과를 검증한다. CMS GC의 Remark 단계와 달리, G1 GC의 Remark 단계에선 snapshot-at-the-beginning 이라는 알고리즘을 사용하는데, CMS GC의 것보다 훨씬 빠르다.
-
Cleanup & Copying
참조되고 있는 객체(live object)를 비어있는 영역으로 옮기고, 참조되지 않는 객체들을 삭제 후에 비게 된 영역을 Unused 상태로 만든다.
이 때, 참조되고 있는 객체들을 비어있는 영역으로 옮기는 과정에서 STW가 발생한다.
특이한 점은 G1 GC는 살아있는 객체가 아주 적은 Old 영역(liveness가 아주 낮은 영역)을 [GC pause (mixed)]로 로그만 표시해놓고 Young GC가 이루어질 때 수집되게 한다.
-
-
JVM GC Options
GC와 관련된 JVM 주요 옵션들을 몇가지만 살펴보자.
-
-XX:NewRatio
Young 영역과 Old 영역의 비율을 조정한다. NewRatio = Old 영역의 크기 / Young 영역의 크기
NewRatio의 값이 너무 작으면 Out Of Memory가 발생하기 쉽고, 너무 크면 Old 영역으로 옮겨지는 객체의 수가 많아진다.
보통 서버 애플리케이션은 Young : Old == 1 : 2 정도로 잡고, 클라이언트 애플리케이션은 1 : 3~5 정도로 잡는다고 한다.
오라클 JVM의 기본값은 2이다.
-
-XX:SurvivorRatio
Eden 영역과 Survivor 영역의 비율을 조절한다. SurvivorRatio = Eden 영역의 크기 / (Survivor 영역의 크기 / 2)
Survivor 영역이 너무 작아지면 객체의 age가 threshold에 도달하기 전에 Old 영역으로 넘어가 버리며, Survivor 영역이 너무 크면, Minor GC가 너무 자주 발생하게 된다.
오라클 JVM의 기본값은 8이다. -
-XX:MaxTenuringThreshold
Young 영역에 있는 객체가 얼마만큼의 Minor GC가 발생한 후 Old 영역으로 옮겨질 것인지에 대한 threshold를 설정한다.
오라클 JVM의 기본값은 15이다.
참고
GC 기초
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
https://mirinae312.github.io/develop/2018/06/04/jvm_gc.html
GC 종류별 특징
https://d2.naver.com/helloworld/1329
https://www.baeldung.com/jvm-garbage-collectors
JDK 11 ZGC
https://www.opsian.com/blog/javas-new-zgc-is-very-exciting/
JDK 9 CMS GC Deprecated
https://blog.gceasy.io/2019/02/18/cms-deprecated-next-steps/
'프로그래밍 > Java' 카테고리의 다른 글
JVM 구조 및 메모리 (0) | 2019.02.11 |
---|---|
Log 남기기 (0) | 2018.08.09 |
Stream (0) | 2018.07.24 |
Java thread-safe singleton (0) | 2018.07.18 |
- Total
- Today
- Yesterday
- Tasklet
- @Bean
- JavaScript
- spring batch
- Express
- @Component
- @Autowired
- Bin
- Check point within polygon
- chunk
- mybatis
- Barycentric coordinates
- Linux
- thymeleaf cannot resolve
- npm
- @Qualifier
- MySQL
- 클로저
- spring
- nodejs
- Bean
- Closure
- unity
- thymeleaf 변수 인식
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |