[10분 테코톡 정리] 나는 GC(Garbage Collection)를 모르고 개발했다.
Java
개발자면서 Java
의 핵심인 GC
에 대해서도 잘 모르고 있었다…(JVM
을 모르니 모를수 밖에…)
좋은 Java
개발자가 되기 위해서는 Java
를 구성하는 핵심 기능인 JVM
과 그안에서 열심히 일하는 GC
에 대해서 알아야 한다고 생각합니다.
GC
튜닝은 성능 개선의 최종단계 라고 합니다.
객체 생성 자체를 줄이려는 코드 레벨에서의 개선이 선행 되어야 합니다.
나도 어느 정도 규모가 있는 서비스를 운영하게되면 반드시 GC
튜닝을 할 일이 생길 것이다.!!!!
이번 기회에 GC에 대해서 공부하고 정리해보자!😄
참고:
https://www.youtube.com/watch?v=FMUpVA0Vvjw
NAVER D2
NAVER D2
NAVER D2
Java HotSpot VM G1GC - 기계인간 John Grib
Java GC(Garbage Collection)이란?
stop-the-world
stop-the-world
를 들어본 적이 있으신가요?
stop-the-world
란 GC
를 실행하기 위해 JVM
이 애플리케이션 실행을 멈추는 것을 의미 합니다.GC
를 사용하는 스레드를 제외한 나머지 스레드는 모두 작업을 멈춥니다.stop-the-world
는 어떤 GC
알고리즘을 사용하더라도 발생합니다.
대게의 GC
튜닝이란 stop-the-world
의 시간을 줄이는 것을 의미 합니다.
GC가 왜 필요해?
GC
란 애플리케이션이 동적으로 할당했던 메모리 영역(Runtime Data Area
의 Heap Area
) 중 필요 없게 된 영역(어떤 변수도 가리키지 않는 상태)을 알아서 해제하는 기법 입니다.
장점
개발자가 메모리를 수동으로 관리하던 것에서 발생한 에러들을 방지할 수 있음
- 메모리 누수 예방
- 해제된 메모리 접근 예방
- 메모리 이중해제(해제된 메모리 다시 해제) 예방
단점
- ⏰ 오버헤드(GC 작업은 순수한 오버헤드)
- 🤷🏻♂️ 개발자는 언제 GC가 메모리를 해제하는지 알수 없음
정리Java
에서는 개발자가 프로그램 코드로 메모리를 명시적으로 해제하지 않기 때문에 가비지 컬렉터(Garbage Collector
)가 더 이상 필요 없는 (쓰레기
) 객체를 찾아 지우는 작업을 한다.!
weak generational hypothesis(약한 세대 가설)
GC
는 2가지 가설(전제 조건)으로 만들어졌다고 합니다.
- 대부분 객체는 금방 접근 불가능한 상태가 된다.
- 오래된 객체에서 젋은 객체로의 참조는 아주 적게 존재한다.
이러한 가설(전제 조건)의 장점을 최대한 살리기 위해 HotSpot VM
에서는 크게 2개로 물리적 공간을 나누었습니다.
- Young Generation Area 👶🏻
- 새롭게 생성한 객체의 대부분이 해당 영역에 위치
- 매우 많은 객체가
Young 영역
에 생성되었다가 사라짐- 대부분 객체가 금방 접근 불가한 상태가 되기 때문
- 여기서 객체가 사라질때
Minor GC
가 발생했다고 함
- Old Generation Area 👵🏻
- 접근 불가능한 상태로 되지 않아
Young 영역
에서 살아남은 객체가 여기로 복사됨 - 대부분
Young 영역
보다 크게 할당 - 크기가 큰 만큼
Young 영역
보다GC
는 적게 발생 - 여기서 객체가 사라질때
Major GC(Full GC)
가 발생했다고 함
- 접근 불가능한 상태로 되지 않아
GC
알고리즘을 살펴본 후 Heap 영역
에 대해서 자세히 살펴보자.
GC 알고리즘
Root Space
알고리즘에 대해서 먼저 설명하기 전에 Root Space
를 설명하겠습니다.Root Space
는 간단하게 설명하면 heap 영역의 참조를 담은 변수라고 생각하시면 됩니다.
- Stack 로컬 변수
- Method Area의 Static 변수
- Native Method Stack의 JNI 참조
Reference Counting
GC
의 초기 알고리즘으로 Garbage
를 발견하는 것에 초점이 맞추어져 있습니다.
힙 영역에 선언된 객체들이 각각 reference count
라는 별도의 숫자를 가지고 있습니다.reference count
는 몇가지 방법으로 해당 객체에 접근할 수 있는지를 의미 합니다.
해당 객체에 접근할 수 있는 방법이 하나도 없다면,
즉 reference count
가 0 이면, GC
의 대상이 되는 것 입니다.
(의도적으로 GC
를 실행하지 않아도 됩니다. 👍)
단순하고 좋아보이지만 치명적인 단점이 존재합니다.
바로 순환 참조 문제 입니다.
객체(메모리)들이 서로 순환하며 참조한다는 의미 입니다.
A 객체는 C 객체가 참조해서 reference count
는 1
C 객체는 B 객체가 참조해서 reference count
는 1
B 객체는 A 객체가 참조해서 reference count
는 1 입니다.
ABC 객체(메모리)가 서로 참조해서 참조횟수가 0이 아니기 때문에 해제가 불가능해 집니다.
결국 메모리 누수(Memory Leak
)가 발생 합니다… 😱
정리
reference count
는 객체에 접근할 수 있는 방법의 갯수를 의미reference count
가0
이면GC
의 대상- 의도적으로
GC
를 실행 하지 않아도 됨 - 순환참조가 발생 가능성 존재(메모리 누수)
Mark And Sweep
Java
와 Javascript
는 Mark And Sweep
방식으로 메모리 관리를 합니다.!!
Mark And Sweep
은 루트에서 부터 해당 객체에 접근이 불가능하면 해제의 대상으로 정합니다.
Mark: 루트로 부터 그래프 순회를 통해 연결된 객체(Reachable
)들을 찾습니다.
Sweep: 연결이 끊어진 객체들(Unreachable
)은 지움
Mark And Sweep
을 사용하면 순환 참조된 객체들도 모두 지울수 있습니다. 👍
Sweep
이후 흩어져있는 메모리가 정리된것을 확인할 수 있는데
이러한 것을 메모리 파편화를 막는 Compaction
이라고 합니다.
(다만 Mark And Sweep
에서 Compaction
은 필수는 아님)
하지만, Mark And Sweep
도 단점이 존재합니다. ㅠ0ㅠ
- 의도적으로 GC 실행
GC
에게 컴퓨터 리소스를 제공해야함.. (stop the world)Reference Counting
방식은Reference Count
가0
이 되면 객체를 소멸!
- 애플리케이션 실행과 GC 실행이 병행
Heap 영역 살펴보기
앞에서 우리는 Young Generation
에서 발생하는 GC
는 Minor GC
,Old Generation
에서 발생하는 GC
는 Major GC(Full GC)
라고 했습니다.
Young Generation
은 3 영역으로 나뉩니다.
- Eden
- 새롭게 생성된 객체들이 할당되는 영역
- Survivor 0 ,1
Minor GC
로 부터 살아남은 객체들이 존재하는 영역Survivor 0, 1
중 하나는 반드시 비어있어야 합니다.
GC 동작 과정
새로운 객체들이 마구 마구 생성 되면 Eden
영역에 할당 됩니다.
그러다가 Eden
영역이 꽉 차는 순간이 옵니다.
이때 Minor GC
가 발생합니다.Mark And Sweep
이 진행됩니다.(stop the world
)
Mark: 루트로 부터 그래프 순회를 통해 연결된 객체(Reachable
)들을 찾음
Sweep: 연결이 끊어진 객체들(Unreachable
)은 지움
이때 루트로 부터 Reachable
이라고 판단된 객체는 Survivor 0
으로 이동하게 됩니다.
(연결이 끊어진 객체들(Unreachable
)은 삭제 됩니다.)
여기서 숫자가 0에서 1로 증가하신것을 보셨나요?
이 숫자는 age-bit
를 의미합니다.Minor GC
에서 살아 남을 때마다 1씩 증가합니다.
시간이 지나 다시 Eden
영역이 꽉 찼습니다.
다시 Minor GC
가 발생합니다.
이번에는 Reachable
이라고 판단된 객체는 Survivor 1
로 이동하게 됩니다.
시간이 지나 다시 Eden
영역이 꽉 찼습니다.Minor GC
가 발생합니다.
이번에는 Reachable
이라고 판단된 객체는 Survivor 0
로 이동하게 됩니다.
JVM GC
에서는 일정 수준의 age-bit
를 넘기면 해당 객체를 Old-Generation
에 넘겨줍니다.
이 과정을 Promotion
이라고 합니다.
(Java 8
에서 Parallel GC
방식 사용 기준 age-bit
가 15 이상이면 Promotion
이 진행 됩니다.)
시간이 오래 지나면 언젠가 Old Generation
영역도 꽉 차는 순간이 있겠죠?
이때 Major GC
가 발생하면서 Mark And Sweep
방식을 통해 메모리를 비워 줍니다.Major GC
는 Minor GC
보다 시간이 더 오래 걸리게 됩니다.(stop the world
시간이 길다는 의미)
Heap 영역을 Young, Old로 나눈 이유
Heap 영역
을 Young
, Old
로 나눈 이유는 무엇일까요? 🤔
그것은 바로 GC
설계자들이 애플리케이션을 분석해 보니 대부분의 객체가 수명이 짧다는 것을 알았기 때문입니다.
GC
도 결국 비용이 발생하는 작업인데, 메모리의 전체가 아닌 특정 부분만 탐색하여 해제해야 효율적 입니다.
그래서 다수의 객체는 어차피 금방 사라지니 Young 영역
에서 최대한 메모리를 해제(Minor GC
)하도록 설계 한 것 입니다.
GC의 실행방식
- Serial GC
- Parallel GC
- Parallel Old GC
- CMS GC
- G1 GC
Serial GC
Serial GC
는 하나의 스레드로 GC
를 실행하는 방식 입니다.
하나의 스레드로 GC
를 실행하다 보니 stop-the-world
시간이 긴 것을 알 수 있습니다.
일반적으로 싱글 스레드 환경 및 Heap 영역
이 매우 작을 때 사용 되는 방식 입니다.
(Mark And Sweep
이후 Compaction
과정도 진행)
Parallel GC(hroughput GC)
Parallel GC
의 기본적인 처리 과정은 Serial GC
와 유사합니다.
하지만, Parallel GC
는 여러개의 스레드로 GC
를 실행하기 때문에 Serial GC
보다 stop-the-world
시간이 짧아집니다.!!! 👍
이러한 점으로 주로 멀티 코어 환경에서 애플리케이션 처리 속도를 향상시키기 위해 사용 됩니다. ㅎㅎ
일반적인 Parallel GC
는 minor GC
에 대해서만 멀티 스레딩을 수행하고, major GC
는 싱글 스레딩으로 수행 합니다.Parallel GC
는 메모리가 충분하고 코어의 개수가 많을 때 유리합니다.Parallel GC
는 Throughput GC
라고도 부릅니다.
또한, Java 8
에서 기본으로 사용하는 GC
방식 입니다.Parallel GC
가 GC
의 오버헤드를 상당히 줄여주었지만, 애플리케이션이 멈추는 stop-the-world
는 피할 수 없기 때문에 다른 알고리즘이 등장하게 되었습니다.
Parallel Old GC
Parallel Old GC
는 JDK 5 update 6
부터 제공한 GC
방식으로 Parallel GC
의 업그레이드된 버전입니다.Major GC(Old 영역)
도 멀티 스레딩으로 수행하고 기존 Mark And Sweep-Compation
의 개선 버전인 Mark And Summary Compaction
을 사용합니다.
(Summary
단계는 앞서 GC
를 수행한 영역에 대해서 별도로 살아 있는 객체를 식별한다는 점에서 Mark And Sweep-Compaction
알고리즘 단계와 살짝 다르며, 약간 더 복잡합니다.)
사실상 Java 7 Update 4
버전부터는 Parallel GC
를 설정해도 Parallel Old GC
가 동작합니다.🤗
엄밀히 말하면 Java 8
의 디폴트 버전은 Parallel Old GC
입니다.^^
CMS GC(Concurrent-Mark-Sweep GC, Low Latency GC)
CMS GC
는 stop-the-world
를 최소화 하기 위해 개발되었습니다.
(대부분의 가비지 수집 작업을 애플리케이션 스레드와 동시에 수행해서 stop-the-world
시간을 최소화 합니다.)
총 4단계로 구성됩니다.Initial Mark
, Concurrent Mark
, Remark
단계는 가비지 수집 작업을 하는 단계이고Concurrent Sweep
단계에서는 가비지를 정리하는 단계 입니다.
Initial Mark 단계
초기 Initial Mark
단계에서는 클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾는 것으로 끝이 납니다.
따라서 stop-the-world
시간이 매우 짧습니다.
Concurrent Mark 단계
그리고 Concurrent Mark
단계에서는 방금 살아있다고 확인한 객체에서 참조하고 있는 객체들을 따라가면서 확인 합니다.
이 단계의 특징은 다른 스레드가 실행 중인 상태에서 동시에 진행된다는 점 입니다.
Remark 단계Remark
단계에서는 Concurrent Mark
단계에서 새로 추가되거나 참조가 끊긴 객체를 확인 합니다.
Concurrent Sweep 단계Concurrent Sweep
단계에서는 쓰레기를 정리하는 작업을 실행 합니다.
장점
stop-the-world
시간이 짧다.- 대부분의 가비지 수집 작업을 애플리케이션 스레드와 동시에 수행하기 때문
단점
- 다른
GC
방식보다 메모리와CPU
를 더 많이 사용한다. Compaction
단계가 기본적으로 제공되지 않는다.
⚠️CMS GC는 신중하게 검토한 후에 사용해야 합니다.GC
작업을 애플리케이션 스레드와 동시에 수행하여, stop-the-world
의 시간을 최소화 하고 있습니다.
하지만, 메모리와 CPU
를 많이 사용하고, Mark And Sweep
과정 이후 Compaction
이 기본적으로 제공되지 않는 단점이 있습니다.
이 때문에 시스템이 장기적으로 운영되다가 조각난 메모리들이 많아 Compaction
단계를 수동으로 진행하면 오히려 stop-the-world
시간이 길어지는 것을 알 수 있습니다.
(CMS GC
는 Java 9
버전부터 deprecated
되었고 Java 14
버전부터는 사용이 중단되었습니다.)
G1 GC
G1
은 Garbage First
의 줄임말 입니다.G1 GC
를 이해하기 위해서는 지금까지의 Young
, Old
영역에 대해서는 잊는 것이 좋습니다.
(Heap 영역
을 다르게 사용)😯
G1 GC
는 Heap 영역
을 일정 크기의 Region
으로 잘게 나누어
어떤 영역은 Young
영역, 어떤 영역은 Old
영역으로 사용 합니다.
(바둑판의 각 영역에 객체를 할당하고 GC를 실행 합니다.)
런타임에 G1 GC
가 필요에 따라 영역 별 Region
개수를 튜닝함으로써 stop-the-world
을 최소화할 수 있습니다.
해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC
를 실행합니다.
즉, 지금까지 설명한 Young
의 세가지 영역(Eden
, Survivor 0
, Survivor 1
)에서 데이터가 Old
영역으로 이동하는 단계가 사라진 GC
방식이라고 이해하면 됩니다.
(G1 GC
는 장기적으로 말도 많고 탈도 많은 CMS GC
를 대체하기 위해서 만들어 졌다고 합니다.)
G1 GC
의 가장 큰 장점은 성능이다. 지금까지 설명한 어떤 GC 방식보다도 빠릅니다.Java 9
이상부터는 G1 GC
를 기본 실행 방식으로 사용 합니다.
GC 튜닝 맛보기 😋
GC 설정 확인
$ java -XX:+PrintCommandLineFlags -version
GC 상태 모니터링
$ jstat -gcutil -t 10364 1000 10
로그인 요청을 보낸 후 Eden
영역의 사용률이 늘어 난것을 확인할 수 있습니다.Young
영역에서 1.137(YGCT)
초 동안 31
번의 Minor GC(YGC)
가 일어난 것을 확인할 수 있습니다.
이를 단순하게 계산하면 1.137 / 31 = 0.0366초 Minor GC
1번에 36ms가 걸린다는 것을 알 수 있습니다.
Heap 영역 사용률 모니터링
$ jstat -gccapacity -t 10364 1000 10
마치며
어떤 서비스에서 A라는 GC 옵션을 적용해서 잘 동작한다고
그 GC 옵션이 다른 서비스에서도 훌륭하게 적용되어 최적의 효과를 볼 수 있다고 생각하면 안된다!GC
옵션은 지속적인 튜닝과 모니터링을 통해서 해당 서비스에 가장 적합한 값을 찾아야 합니다.
그게 개발자가 해야하는 일이다!
2010년 JavaOne에서 Oracle JVM을 만드는 엔지니어들이 한 말이다. ㅎㅎ
이말을 명심하며 좋은 개발자가 되길 소망합니다.! 🙏
'0 + 프로그래밍 > 0 + Java' 카테고리의 다른 글
제어할 수 없는 코드가 포함된 로직을 메소드 변경 없이 테스트 코드를 작성하는 방법(오버라이딩) (0) | 2023.11.20 |
---|---|
[10분 테코톡] 나는 제너릭을 모르고 개발했다. (0) | 2023.04.19 |
[10분 테코톡] 나는 JVM를 모르고 개발했다. (0) | 2023.03.03 |