Garbage Collector 제대로 알기

recordsbeat·2021년 7월 15일
22
post-thumbnail

서문

현업하다보면 GC튜닝이라는 용어를 간혹 듣곤 했는데
그때마다 동공지진 숨기며 회피하곤 했다.

튜닝은 커녕 gc도 제대로 모름

그러나 Java 개발자라면 기본적으로 알아야할 내용이라는 말에 뼈맞고 아파하다가 지금이라도 제대로 써보려고한다.


어떻게 접근?

쳐맞은 바로 다음날.
고맙게도 스터디 자리에서 팀원 한 분이 G1 GC에 대한 발표를 해주셨다.
자료 정리가 매우 잘 되어있어서 무임승차하듯 편하게 G1에 대해 습득을 하였는데

애초에 GC를 모르니 G1이 다른 GC들보다 뭐가 좋다라는지 이해를 못했다.
(아쉽게도 사내 Wiki에 적어둔 내용이라 노출은 못하고.. 이 포스팅을 통해서라도 배운점을 살짝 써볼까한다.)

그리고 이걸 친구 개발자와 이야기하다가 Gc가 어쩌구 저쩌구 참조유형이 어쩌구 저쩌구...또 몰라서 쳐맞았다.


제대로 알아보자

GC는 뭐하는 놈이냐?

Java 이전의 C나 C++같은 언어에서는 사용자(개발자)가 직접 메모리 할당과 해제를 컨트롤 해야했다. (C의 malloc과 free같은.)
잦은 메모리 이슈가 까다로운 개발환경에서 벗어나기 위해 GC가 등장하였다.

GC는 더 이상 사용하지 않는 메모리 영역을 알아서 해제, 가용상태로 돌려주는 착한 녀석인데, 문제는 이걸 센스있게.. 눈치껏.. 작동하기 떄문에
실행 조건이 일정하지 않다. 즉, 내 맘대로 실행되게 못 한다라는 어려움은 계속해 존재한다.
(System.gc() 같이 메소드를 직접 호출해도 실제 gc가 작동할지는 미지수라는 이야기다. 소위말해 야 GC 이것 좀 치워줘! 해도 '응 나중에~ 응 안해~' 이럴수 있다는거다.)

??? : 너 바둑 개못하잖아

그렇기에 각 GC의 메커니즘을 이해하고 코딩간 행위를 예측하여
센스 없는 GC가 알아서 잘치워줄 수 있게 코드로 잘 정리해놓아야한다.

내가 아는 개발 아저씨 중 제일 설명 쉽게 해주는 사람 (얄코)
요리사와 조리대로 비교를 해주었다. 역시 이해 잘 된다.
가비지 콜렉터가 뭔가요?


그래서 어떻게 한다는건데 ?

워낙 여기저기 설명 글이 많은 관계로
간단한 요약과 좋은 포스팅들로 대체한다.

GC HEAP 구조

Jvm에는 heap 메모리 영역이 존재한다.
간단히 설명해 객체가 할당되는 영역인데 이 영역을 위 그림과 같이 쪼개놓는다.

그리고 각 영역을 간단하게 설명하자면 아래와 같다

  • young - 비교적 젊은 reference가 살아있는 곳
    o eden - young영역중에서도 특히 방금 막 생성된 녀석들이 있는 곳
    o survivor - 영역이 두개 존재하는데 eden에서 생존된 녀석들이 당분간 생존해 있는 곳

  • old - 특정 횟수 이상을 살아남은 reference가 살아있는 곳

  • permanent - Method Area의 메타정보가 기록된 곳


Minor GC / Major GC

Minor GC

Minor GC에서 발동되는 알고리즘을 Stop And Copy알고리즘이라고 부른다.
stop and copy 알고리즘은 GC의 빈도를 높여서 자잘한 청소작업을 여러번하여
사용자로 하여금 프로그램이 정지되는 경험을 주지 않게하고 단편화역시 처리하는 아주 좋은 알고리즘이다.
1. 대부분의 객체는 생성되고 나서 얼마 안되서 unreachable하게 된다.
2. 오래된 객체는 젊은 객체를 적게 참조한다.

Major GC
old영역에서 발생한다. 메모리가 커서 성능이슈 발생

Major/Minor GC에 대하여 잘 정리된 글
Java에서의 가비지컬렉터(Garbage Collector:GC)돌아가는 원리 파해치기**
[Java] Garbage Collection(가비지 컬렉션)의 개념 및 동작 원리


누가 쓰레기인가?

치우긴 치워할텐데 뭘 치워야할지 어떻게 알 것인가?
그래서 GC는 '아 이놈은 치워야겠다' 라고 하는 것과 '이건 치우면 안 되겠다.' 하는 것으로 나눈다. 그리고 이를 판단하는 방법이 몇가지 있다.

Reachable /Unreachable

Reachable - 치우면 안 되는 거
아직 누군가 쓰고있는데 맘대로 버렸다간 욕먹기 쉽상이다.

Unreachable - 치워야 하는 거
아무도 안쓰는데 덩그러니 자리만 차지하고 있는 것들 분명 있다.
(애물단지)


(출처 - Java Reference와 GC)

Heap 메모리를 참조하는 영역을 흔히 Root set영역이라 하는 것 같다. 위 그림에서는 세세하게 stack, jni, method area로 나누었다.
그림에 화살표로 이어진 Object들은 참조가 이루어진 것으로 Reachable 상태다. 반대로 참조 없이 둥둥 떠다니는 애들이 Unreachable 단어 그대로 직관적이다.

가비지 컬렉션(Garbage Collection)은 특정 객체가 가비지인지 아닌지 판단하기 위해서 도달성, 도달능력(Reachability) 이라는 개념을 적용한다. 객체에 유효한 레퍼런스가 없다면 Unreachable로 구분해버리고 수거해버린다. 레퍼런스가 있다면 Reachable로 구분된다.
자바 레퍼런스와 가비지 컬렉션(Java Reference & Garbage Collection)

이렇게 Reachable과 Unreachable 상태로 나뉘어진 오브젝트(메모리 영역)들은 GC의 알고리즘과 구현방식에 의해 이동, 압축 및 삭제를 거치게 된다.

이 내용은 뒤에서 좀 더 자세히 나온다.


Reference Type

Reachable / Unreachalbe 을 판단하는데 이를 사용자(개발자)가 컨트롤할 순 없을까? 라고 하면 아래와 같이 Reference Type을 사용할 수 있다.

(각 유형 별 텍스트에 링크 걸어두었습니다. 짧으면서 예시코드까지 잘 정리한 글이니 시간되시면 한 번씩 고고)

Strong Reference

Object obj = new Object();

위 처럼 Thread Stack이 직접적으로 Object에 접근 가능한 상태
null을 대입하지 않는 이상 gc대상으로 취급 되지않음

Soft Reference

SoftReference<Object> softReference = new SoftReference<>(new Object());

GC 구현에 따라 대상 될 수도 안 될 수도 있다. - 애매한 놈

Weak Reference

WeakReference<Object> weakReference = new WeakReference<>(new Object());

항상 GC의 대상이 된다. 맘 편한 놈

Phantom Reference

올바르게 삭제하고 삭제 이후 작업을 조작하기 위한 타입
위 두가지와 다르게 사용자가 finalize 메소드에 의해 컨트롤 할 수 있다.
Object의 finalize 메소드가 말을 안들어 강한참조가 역으로 일어날수 있는 경우의 위 참조방식을 사용하면 유용하다.
(자세한건 링크 참조..)

난 사용해본적 없는데... 누가잇냐?

Phantom Reference 사용해본사람????

-Phantom Reference 사용 예제

Have you ever used PhantomReference in any project? - Stack OverFlow


Reference Counting & Mark and Sweep

이제서야 GC가 어떻게 Reachable과 Unreachable을 판단할 것인지 간단히 들여다 볼 수 있게 되었다.

물론 아래 Reachable/ Unreachable 판단 외 부수적인 GC알고리즘을 소개하지만
지금은 쉬워보이는(?) 두 가지를 보고간다.

Reference Counting


단순하다. 참조하는 수를 센다. 그러나 이 방법은 치명적인 단점을 지니는데, 바로 순환참조를 해결할 수 없다는 것이다. 가운데 둥둥 떠다니는 회색 object들은 죽어도 GC로 해결할 수 없게된다.

Mark and Sweep

그래서 나타난게 Mark and Sweep. 표시하고 쓸어버리겠다(?)라는 거다.
각 thread의 stack, method area 등 heap 영역 참조가 가능한 영역을 Root Set이라고 한다. 이 Root Set부터 시작하여 하나씩 따라가며 참조상태를 확인하는 것이다.
분명 Reference Counting 의 순환참조는 말끔하게 해결하였지만 Root Set의 크기에 따라 Marking 시간이 늘어남은 어쩔 수 없다.
(참고로 GC의 발전따라 Concurrent하게 수행한다.)

Reference Counting & Mark and Sweep 잘 정리된 글
Reference Counting과 Mark and Sweep
다 쓴 메모리를 자동으로 수거해주는 가바지컬렉터(Garbage Collector:GC), 기본 원리 파해치기


GC 구현 방식

사실 이부분에서 좀 헛갈리는게있다.
블로그를 찾아보면 글마다
default, serial, parallel, cms와 같은 GC들을 칭하는 용어가 다르다는 거다.
문제는 이 내용들이 GC의 알고리즘, 종류, 방식, 정책 등으로 GC의 어느 부분을 기준으로 나눈건지 알 수가 없다.

그래서 나름 정리를 하건데..
GC의 종류는 Major, Minor GC
그리고 GC 알고리즘으로는 위에서 언급한 Reference Counting, Mark-and-Sweep 가 있지 않은가?

GC 알고리즘에 대하여 잘 정리된 글
[JVM] Garbage Collection Algorithms

그래서 나는 아래 내용들을 GC 구현 방식이라고 칭하겠다.

Serial

싱글스레드 애플리케이션을 위한 GC 다.
실행 간에는 무조건 STW가 발생하기 때문에 멀티 스레드 환경에서는 사용하지 않는 것이 좋다. low-latency(pause) 를 추구하는 애플리케이션 또한 적당하지않다.

Parallel

병렬처리가 가능한 gc이며 thoughput gc라 하기도 한다.
(병렬처리라고해서 STW 발생이 없는 것이 아니다. GC 처리를 병렬로 한다는 내용이니 유의)
멀티스레드로 Young genereation 처리하고 싱글스레드로 Old genereation 을 처리한다.
(이후 Parallel Old GC 등장하여 Old generation 도 병렬로 처리하게 된다.)

배치 프로젝트와 같은 긴 pause가 허용되는 애플리케이션에 사용할 수 있다.

CMS

low pause 의 시초가 되는 GC다.
parellel GC 와 같이 young generation 을 병렬 처리하고 Old generation 또한 병렬처리한다. 그러나 parellel 과 다른 점은 애플리케이션 백그라운드에서 진행 가능하다는 점이다.
(이 역시 STW 발생이 없다는 뜻이 아닌, STW 발생을 최소화 했다는 측면으로 보는게 맞다.)

그러나 백그라운드로 실행(concurrently)되는 GC의 초기버전이라 메모리 파편화가 발생한다. 만약 CPU 리소스가 부족해진다거나, 메모리 파편화가 너무 심해서 메모리 공간이 부족해지면 Serial GC가 하던 짓을 똑같이 따라한다.

G1

64비트 컴퓨터 최적화된 GC로 4GB 이상의 Heap size 에 적합한 GC다.
메모리 리전 분할 정책으로 리전내부 gc 영역 존재한다.
mark-sweep and compact 알고리즘으로 CMS의 메모리 단편화 이슈를 해결하였다. 그래도 모든 GC와 같이 STW가 존재한다.

compact 알고리즘의 의해 살아남은 객체들을 새로운 Region으로 대피시키는 데 이 과정에서 pause가 발생한다.

Shenandoah

low-pause GC 짧은 주기의 GC를 사용하여 최소한의 pause(Stop-The-World)만 발생한다. 강력한 Concurrency 지원으로 heap 사이즈에 영향을 받지 않고 일정한 pause 시간을 보장한다. 아주 짧은 STW 시간으로 거의 대부분의 시간동안 애플리케이션 백그라운드에서 작동하므로, 빠른 응답을 요구하는 프로젝트에 적합하다.

OpenJdk 기반. jdk8부터 개발을 시작하여 jdk12에 release하였지만 oracle jdk는 지원하지 않는다.

G1을 기반으로 개발되었으며,G1의 compact 과정의 pause를 conrruent 하게 해결한 것이 가장 큰 특징이다.

본인이 Shenandoah에 대해서 기고한 글
Low-Pause ! Shenandoah GC

ZGC

이 역시 low-pause GC 며 heap 영역을 region으로 나누고 concurrent 하게 처리한다는 것이 Shenandoah와 같은 개념이다. (10ms 이하의 pause를 목표로 한다.)
Oracle에서 만들었는데 Shenandoah와 라이벌 격인듯 하다.
jdk15에 release 되었다.


(성능이 아주 좋다고 자랑한다.)

GC종류 참고
Garbage Collection in Java – What is GC and How it Works in the JVM


그래서 누가 GC King?

개발공부를 하면 가장 많이 보게되는게 말이 상황에 맞는 스펙(스킬)을 사용 하라는 말이다.
(군대용어 : 유도리있게)

그래서 GC중에서 짱 ? 그런 건 없다.

latency(지연시간)을 낮추면 througput(처리량)이 줄어들 것이고, throughput을 늘리면 latency가 늘어날 것이다.
결국 많이 처리하고 싶으면 좀 기다려야되고, 적게 기다리려면 좀 적게 처리해야된다.(세상의 이치가 아닐까..)

그래도 간단히 설명을 해보자면..

Serial

작고 소중한 프로젝트에 사용하면 된다. 싱글스레드 기반으로 heap size가 약 100MB 정도 되는 애플리케이션이면 적당하다.

Parallel

GC와의 공존을 버리고 최대치의 애플리케이션이 성능을 이끌어내야한다면 Parallel을 사용하는 것이 좋다. 혹은 Pause 소요 시간에 대해 크게 신경쓰지 않는 프로젝트여도 좋다.

CMS/G1

응답시간이 처리량보다 중요한 경우 적합하다. API서버처럼 약 1초보다 짧은 pause 시간을 계속해 유지해야한다면 해당 GC를 사용 권장한다.

Shenandoah/ZGC

이 두 가지도 응답시간이 우선적일 때 사용하기 적합하다. 그러나 CMS/G1의 경우와 다른 점은 큰 heap size에서도 low-pause를 보장하기 때문에 heap size가 매우 크다면 고려해볼만하다.


GC튜닝이란 어떤행위?

이제까지 GC란 무엇이고 어떤 짓을 하는 놈이며, 어떤 놈들이 있는지 알아봤다.
그럼 다시 처음으로 돌아가서... GC 튜닝은 어떤 짓이냐?

GC를 사용하기 위해서는 몇몇 값을 설정 해야 한다.. 이 값들을 이리저리 만져서 애플리케이션 성격에 맞게 GC를 최적화 하는 것인데, 사실 GC 튜닝은 권장되지 않는다.

GC는 앞 서 이야기 하였듯 지 맘대로 움직인다. 그렇기 때문에 GC 성능을 최적화 하려면(GC튜닝 하려면) 상세한 동작원리부터 실질적인 벤치마킹까지 섬세하게 따져봐야 할 것들이 너무 많다.
그래서 실상은 GC를 건드리는 것보다 애플리케이션 코드로써 메모리 최적화를 권장한다.(공간복잡도라던가..)

설정

힙(heap)

  • -Xms JVM 시작 시 힙 영역 크기
  • -Xmx 최대 힙 영역 크기

New 영역의 크기

  • -XX:NewRatio New영역과 Old 영역의 비율
  • -XX:NewSize New영역의 크기
  • -XX:SurvivorRatio Eden 영역과 Survivor 영역의 비율

JVM에서 -Xms 와 -Xmx 는 필수로 지정해야하는 값이다.
그리고 NewRatio 값이 GC성능에 많은 영향을 끼친다고 한다.
대부분의 GC는 Old Generation 의 부하가 크기 때문에 어찌보면 당연한 것이기도 하다.

JVM Xms, Xmx default 값은 어떻게 결정될까?
JVM의 default Heap Size가 궁금하세요?

사실 위의 설정 값 외에도 GC에 따라 많은 설정 값들이 존재한다.
기대성능에 따라 여러 값들을 조정하고 이에 맞게 성능 측정을 하는 과정이 GC 튜닝 이라는 행위로 볼 수 있을 것이다.


(인터넷 서비스 자바 애플리케이션 권장 튜닝 절차)

이 과정에는 수 많은 모니터링과 벤치마킹 결과를 가지고 결정해야하는 고생길이 있어 보인다. 아직 내공이 부족하여 여기는 추후에 좀 더 조사를 해보는 것으로 하고 이만 마무리..

GC를 모니터링하는 방법
Garbage Collection Monitoring

슬쩍 찾아보니 Spring actuator 의 Metric 에서도 gc 모니터링을 가능하게 해주는 설정값이 있어보인다.
Spring Boot default metrics
Spring Boot Actuator - 49.1 System metrics

GC튜닝 참고링크
자바 GarbageCollection 튜닝 기초 개인정리
자바 애플리케이션 성능 튜닝의 도(道)

마치며

기본적인 스터디를 거치며 바로 이 다음 포스트 Shenandoah 까지 조사하게 되었는데 역시나 깊이가 어마무시한 영역이라 이해하는 것이 쉽지 않았다.
글 쓰는 재주가 좋지 않아 처음 시작에 비해 용두사미 꼴이 난 것 같아 좀 아쉽지만.. 이따금씩 읽어보며 어색한 부분을 조금씩이라도 수정해나갈 예정이다.

사실 이 글을 쓰기 시작한건 꽤 되었는데 계속해 미루다가 이제서야 마무리 한다. (역시 믿을 건 미래의 나뿐이다.)

열심히 공부 안한 나 자신을 탓하며 오늘도 미래의 나를 믿어본다 이히힝히이힝힝

profile
Beyond the same routine

4개의 댓글

comment-user-thumbnail
2021년 7월 16일

Reference Type 설명 친절하게 되어있는 글 처음봤어요 ㅋㅋㅋ
너무 좋네요 감사합니다~

1개의 답글
comment-user-thumbnail
2022년 6월 15일

gc 관련글 구글링하다가 우연히 발견했는데 너무 유익하게 잘 봤습니다. 감사해요~!!

답글 달기
comment-user-thumbnail
2022년 7월 29일

잘 읽고 갑니다. 좋은 글이네용

답글 달기