[JVM] GC 기본개념 - JVM메모리 구조 / Minor GC / Full GC

2021. 12. 16. 11:54 JAVA/JVM

● GC (Garbage Collection)
Java Application에서 사용하지 않는 메모리를 자동으로 수거하는 기능
C언어의 경우 malloc, free등을 이용해서 메모리를 할당하고, 일일이 그 메모리를 수거해줘야했다. 그러나 Java 언어에서는 GC가 알아서 해준다. 

● Stop-the-world
GC를 실행하기 위해 JVM이 어플리케이션의 실행을 멈춘다. 이를 Stop-the-world라고 한다. GC 튜닝이란, 이 Stop-the-world의 시간을 줄이는 것이다. 

● Generational GCs
대부분의 객체는 오랜시간동안 살아있지 않는다. 
오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다. 
이러한 두 가지에 조건하에  가비지 컬렉터를 효율적으로 동작시키기 위해 HotSpot VM에서는 메모리를 크게 2개의 물리적 공간으로 나누었다. 그 영역이 Young / Old이다. 이러한 방식의 운영을 Generational GC라고 한다. 

● Hotspot Heap Structure
GC의 동작방식을 이해하기 위해서는 자바의 메모리 영역에 대한 이해가 필요하다
자바의 메모리영역은 크게 Young, Old, Perm 3가지로 구분된다.

- Young : 생성된지 얼마 안된 객체들이 저장되는 장소, 대부분의 객체가 이 영역에서 생성되었다가 Minor GC를 통해 사용되지 않는 객체가 제거된다.

- Old : 생성된지 오래된 객체들이 저장되는 장소. Young 영역에서 살아남은 객체가 이곳으로 옮겨지고 Full GC를 통해 사용되지 않는 객체가 제거된다.

- Perm : 프로그램 코드가 올라가는 부분. Code가 모두 로딩되고 나면 거의 일정한 수치를 유지. Old 영역에서 살아남은 객체가 영원히 남아 있는 곳은 절대 아님. 여기서 GC 가 발생하면 Major GC에 포함된다. 

 JVM Memory Structure 
힙을 포함한 전체 메모리 구조는 다음과 같다. 



 Card Table
Old영역의 객체가 Young영역의 객체를 참조하는 경우 어떻게 판별하는가? Old영역에 Card table을 두고 Old영역의 객체가 Young 영역의 객체를 참조할 경우, 카드 테이블에 표시한다.
Minor GC 를 실행할 때에는 Old영역의 모든 객체의 참조를 확인하지 않고, 카드 테이블을 확인한다. 

 Minor GC
Young 영역에 GC가 발생할 경우 이를 Minor GC라고 한다.
Young영역은 Eden과 Survivor라는 영역으로 또 나뉘어진다. 
- Eden : 객체가 생성되자 마자 저장되는 곳
- Survivor : 한번의 Minor GC를 경험한 객체들이 저장되는 곳. Survivor 1, Survivor 2 존재.
Minor GC가 발생하면 Eden과 Survivor1에 살아있는 객체를 Survivor2로 복사한다. 그리고 Survivor1과 Eden을 Clear한다. 결과적으로 한번의 Minor GC에서 살아남은 객체만 Survivor2영역에 남는다. 그리고 다음번 Minor GC가 발생하면 같은 방식으로 Eden과  Survivor2영역에서 살아있는 객체를 Survivor1로 복사하고 클리어한다. 결과적으로 Survivor1에만 살아있는 객체가 남게된다. 이렇게 반복적으로 Survivor1, Survivor2를 왔다갔다하다가, Survivor 영역에서 오래 살아남은 객체는 Old영역으로 옮겨진다. 
==> Survivor 두 영역중 하나는 반드시 비어있는 상태다. 만약 두 영역에 모두 데이터가 존재하거나, 사용량이 0이라면 정상적인 상황이 아니다. 
Q. Minor GC도 stop-the-world가 발생하나? => YES (아래 글 참조)

https://plumbr.io/blog/garbage-collection/minor-gc-vs-major-gc-vs-full-gc

 

● Full GC
Old 영역에서 발생하는 GC를 Full GC라고 한다. GC를 수행하는 알고리즘도 여러가지가 존재하는데, GC 알고리즘에 따라서 Full GC의 절차가 달라진다. 아래와 같은 알고리즘들이 존재한다.
Serial GC
Parallel GC
Parallel Old GC(Parallel Compacting GC)
Concurrent Mark & Sweep GC(이하 CMS)
G1(Garbage First) GC...등

대표적으로 Mark & Compact 알고리즘을 살펴보자. 이 알고리즘은 말 그대로, 객체들의 레퍼런스를 한번 쭉~ 따라가면서 사용하지 않는 객체들을 Mark한 뒤, 다음번 작업에서 사용하는 객체들만 압축해서 모아놓고 나머지는 Clear시킨다. 
Full GC는 속도가 매우 느리고, Full GC가 발생하는 순간, 자바 어플리케이션이 멈춘다. (Stop-the-world). 따라서 Full GC는 성능과 안정성에 아주 큰 영향을 미친다. 

● GC와 성능
Minor GC의 경우 보통 0.5초내에 끝나기 때문에 큰 문제가 되지 않는다. 하지만 Full GC는 조심해야 한다. Full GC가 발생해서 3초정도 멈춰있는 동안 사용자의 요청이 큐에 쌓이게 된다. 그리고 Full GC 가 끝난 후 요청을 한꺼번에 처리하게 되면 과부하가 발생할 수 있다. 그래서 Full GC 를 관리하는 방법이 중요한데... 

● 소프트웨어 품질특성
소프트웨어 품질특성에서 나타나듯이, 어떠한 품질을 높히기 위해 하나의 방식을 선택하면 다른 품질이 낮아지기 마련이다. GC도 마찬가지로 Throughput, Pause Time등 여러가지 품질 요소가 있다. 자기 시스템에 적합한 품질요소를 찾고 그에 맞춰서 알맞는 GC알고리즘을 선택하는 것이 중요하다. 모든 시스템에서 잘 동작하는 GC 설정이란 없다.

● GC 로그 수집 & 분석
GC를 튜닝하려는 시스템의 성질을 파악하는 과정이다. 이 과정이 제일 중요하다. 시스템의 특성을 파악하고, 향상시키고자 하는 소프트웨어 품질특성을 찾는다. 예를들어 throughput이 부족할 경우, 이를 올리는 것을 목표로 한다. 로그를 보고 분석하여 힙사이즈 및 GC  알고리즘을 조정해주자. 로그 분석방법은 추후 포스팅 예정...

● GC 관련 파라미터
1. Heap 사이즈 조절
Xms : 최소 힙 사이즈 / Xmx : 최대 힙 사이즈
Xms, Xmx 를 같게두면 일정한 크기의 힙 사이즈를 유지한다. 일반적으로 server application인 경우에는 ms와 mx 사이즈를 같게 하는것이 Memory의 growing과 shrinking에 의한 불필요한 로드를 막을 수 있어서 권장할만하다.ms와mx사이즈를 다르게 하는 경우는 Application의 시간대별 memory 사용량이 급격하게 변화가 있는 Application에 효과적이다
2. Perm 사이즈 조절
자바 어플리케이션 클래스가 로딩되는 영역
어플리케이션 시작 시 Out Of Memory 에러가 발생할 경우 Perm사이즈를 의심하자. 
PermSize는 -XX:MaxPermSize=128m 식으로 지정할 수 있다.
3. New / Old 영역 크기 비율 조정
4. Survivor 영역 조정
5. -server / -client 옵션
-server : 서버용 어플리케이션에 적합한 JVM옵션. 부팅 시간보다는 요청에대한 응답시간을 줄이는 것이 중요하다. 메모리 역시, 서버의 경우 New 객체들이 많이 발생한다. 왜냐하면 세션이 끊기면 특정 사용자에 대한 객체는 사라지기 때문이다. 그래서 상대적으로 Old영역이 작고 New영역이 크다. 
-client : 클라이언트용 어플리케이션에 적합한 JVM옵션. 부팅시간이 빨라야 한다. 그리고 하나의 클라이언트에서는 객체가 상대적으로 오랫동안 살아있는다. 따라서 Old영역이 상대적으로 크다. 
6. GC 알고리즘 선택

● GC 튜닝 절차
STEP 1. Application의 종류와 튜닝목표값을 결정한다.
JVM 튜닝을 하기위해서 가장 중요한것은 JVM 튜닝의 목표를 설정하는것이다. 메모리를 적게 쓰는것이 목표인지, GC 횟수를 줄이는것이 목표인지, GC에 소요되는시간이 목표인지, Application의 성능(Throughput or response time) 향상인지를 먼저 정의한후에. 그 목표치에 근접하도록 JVM Parameter를 조정하는것이 필요하다.

STEP 2. Heap, Perm size 설정
STEP 3. 테스트 & 로그 분석
변경한 파라미터가 제대로 작동하는 지 로그를 분석한다. 명확한 테스트 시나리오 작성은 매우 중요하다. 스텝2~3을 반복하면서 파라미터를 설정한다.
 
- GC 분석
STEP 1에서 정한 튜닝 목표값을 참고해야 한다. Full GC가 너무 길어서 수행 시간을 줄이고자 Old영역을 줄이면 Full GC가 발생하는 횟수는 오히려 늘어난다. 반대로 Full GC가 발생하는 횟수가 많아서 Old영역을 늘리면 횟수는 줄지만 Full GC수행시간이 늘어난다. 

서버 어플리케이션에서는 보통 LVS(로드밸런싱)을 이용해서 서버 부하를 분산시킨다. 그리고 Old 영역을 작게잡아서 Full GC 수행시간을 감소시킨다. 하나의 서버가 Full GC가 발생해서 멈춰있어도 LVS가 부하를 분산시키기 때문에 영향을 최소화할 수 있다.

GC 튜닝은 어플리케이셔의 성격, 구조 그리고 사용자의 패턴에 따라서 크게 좌우된다. 따라서 얼마만큼의 파라미터를 많이 아느냐보다, 테스트와 로그를 통해 시스템의 성격을 파악하고 목표값을 설정해서 접근하는 것이 가장 중요하다!