0 + 프로그래밍/0 + Java

[10분 테코톡] 나는 GC를 모르고 개발했다.

힘들면힘을내는쿼카 2023. 3. 7. 00:24
728x90
반응형

[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-worldGC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것을 의미 합니다.
GC를 사용하는 스레드를 제외한 나머지 스레드는 모두 작업을 멈춥니다.
stop-the-world는 어떤 GC알고리즘을 사용하더라도 발생합니다.

대게의 GC 튜닝이란 stop-the-world의 시간을 줄이는 것을 의미 합니다.

GC가 왜 필요해?

GC란 애플리케이션이 동적으로 할당했던 메모리 영역(Runtime Data AreaHeap 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 count0이면 GC의 대상
  • 의도적으로 GC를 실행 하지 않아도 됨
  • 순환참조가 발생 가능성 존재(메모리 누수)

 

Mark And Sweep

JavaJavascriptMark 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 Count0이 되면 객체를 소멸!
  • 애플리케이션 실행과 GC 실행이 병행

Heap 영역 살펴보기

앞에서 우리는 Young Generation에서 발생하는 GCMinor GC ,
Old Generation 에서 발생하는 GCMajor 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 GCMinor 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 GCminor GC에 대해서만 멀티 스레딩을 수행하고, major GC는 싱글 스레딩으로 수행 합니다.
Parallel GC는 메모리가 충분하고 코어의 개수가 많을 때 유리합니다.
Parallel GCThroughput GC라고도 부릅니다.


또한, Java 8에서 기본으로 사용하는 GC 방식 입니다.
Parallel GCGC의 오버헤드를 상당히 줄여주었지만, 애플리케이션이 멈추는 stop-the-world는 피할 수 없기 때문에 다른 알고리즘이 등장하게 되었습니다.

 

Parallel Old GC

Parallel Old GCJDK 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 GCstop-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 GCJava 9 버전부터 deprecated되었고 Java 14 버전부터는 사용이 중단되었습니다.)

 

G1 GC

G1Garbage First의 줄임말 입니다.
G1 GC를 이해하기 위해서는 지금까지의 Young, Old 영역에 대해서는 잊는 것이 좋습니다.
(Heap 영역을 다르게 사용)😯

G1 GCHeap 영역을 일정 크기의 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을 만드는 엔지니어들이 한 말이다. ㅎㅎ

이말을 명심하며 좋은 개발자가 되길 소망합니다.! 🙏

 

 

 

 

728x90
반응형