본문 바로가기
개발 관련 강의 정리/10분 테코톡

[10분 테코톡] 🤔 조엘의 GC 정리

by 코딩개발 2023. 5. 12.
728x90
반응형

● GC가 왜 필요할까?

- GC는 메모리 관리 기법 중 하나이며

동적으로 할당했던 메모리 영역 중에서 필요 없게 된 영역을 알아서 해제하는 기법이다.

*동적으로 할당했던 메모리 영역 : 프로그램 런타임에 사용되는 Heap영역 메모리를 뜻함

*필요없게 된 영역 : 어떤 변수도 가리키지 않게 된 영역을 의미

 

장점

수동으로 메모리를 관리하던 것에서 비롯된 에러 방직

1) 개발자 실수로 인한 메모리 누수 방지

2) 해제된 메모리에 접근하는 오류 방지

3) 해제된 메모리를 또 해제하는 이중 해제 방지

 

단점

1) 어떠한 메모리 영역이 해제의 대상이 될지 검사하고 해제하는 일은 우리의 프로그램이 해야하는 일을 못하도록 방해하는 오버헤드이다.

2) GC의 메모리 해제 타이밍을 개발자가 정확히 알기 어렵다.

 

 

● GC알고리즘

 Reference Counting

Root Space : 스택 변수, 전역 변수 등 Heap 영역 참조를 담은 변수

 

Heap 영역에 선언된 객체들이 각각 reference count라는 별도의 숫자를 가지고 있다고 생각하자

reference count는 몇 가지 방법으로 해당 객체에 접근할 수 있는지를 뜻한다.

reference count가 0이라면 GC 대상이 된다.

 

Root Space에서 Heap Sapce 접근을 모두 끊는다고 했을 때

노란 원안에 객체들은 서로가 서로를 참조하기 때문에 count가 1로 유지된다.

사용하지 않는 메모리 영역이 해제되지 못하고 Memory Leak이 발생한다.

 

 

Mark And Sweep

루트에서부터 해당 객체에 접근 가능한지를 해제의 기준으로 삼는다.

 

루트부터 그래프 순회를 통해 연결된 객체들을 찾아내고(Mark)

 

연결이 끊어진 객체들은 지우는 방식(Sweep)이다.

 

루트로부터 연결된 객체는 Reachable, 연결되지 않았다면 Unreachable이라고 부른다.

위처럼 Sweep 이후에는 분산되어 있던 메모리가 예쁘게 정리된 것을 볼 수 있다.

 

이를 메모리 파편화를 막는 Compaction이라고 한다.

하지만 Mark And Sweep에서 Compaction은 필수가 아니다.

이렇게 Mark And Sweep 방식을 사용하면 순환 참조되는 객체들도 모두 지울 수 있다.

자바와 자바스크립트가 이 방식으로 메모리를 관리한다.

 

Mark And Sweep 특징

1) 의도적으로 GC를 실행시켜야 한다.

2) 어플리케이션 실행과 GC실행이 병행된다.

-> 어플리케이션의 사용성을 유지하면서 효율적이게 GC를 실행하는 것이 꽤나 어려운 최적화 작업이다.

 

 

JVM의 GC

JVM 구조

바이트코드를 읽고, 클래스 정보를 메모리의 Heap/Methode Area에 저장하는 클래스 로더

실행 중인 프로그램의 정보가 올라가 있는 메모리,

바이트 코드를 네이티브 코드로 변환시켜주고, GC를 실행하는 실행 엔진

이렇게 3가지 영역으로 구성되어 있다.

 

JVM 실행 엔진이 어떻게 GC를 돌리는지 알지 위해선 JVM메모리를 더 자세히 볼 필요가 있다.

 

JVM은 OS로 부터 메모리를 할당 받은 후, 해당 메모리를 용도에 따라 여러 영역으로 나누어서 관리한다.

총 5가지로 나뉘는데 2가지로 분류할 수 있다.

모든 쓰레드가 공유하는 영역으로 Methode Area와 Heap 영역이 있고,

 

각 쓰레드마다 고유하게 생성하며, 쓰레드 종료 시 소멸되는 Stack, PC Register, Native Method Stack 영역이 있다.

 

 

각각의 역할

Methode Area : 프로그램의 클래스 구조를 메타데이터처럼 가지며, 메서드의 코드들을 저장

Heap : 어플리케이션 실행 중에 생성되는 객체 인스턴스를 저장하는 영역(GC에 의해 관리되는 영역)

Stack : 메서드 호출을 스택 프레임이라는 블럭으로 쌓으며 로컬 변수, 중간 연산 결과들이 저장되는 영역

PC Register : 쓰레드가 현재 실행할 스택 프레임의 주소를 저장

Native Method Stack 영역 : C/C++ 등의 Low level 코드를 실행하는 스택

 

 

JVM의 GC는 기본적으로 Mark-And-Sweep 방식으로 돌아가는데

JVM GC의 Root Space는 JVM 메모리의 Stack의 로컬 변수, Method area에 저장된 static 변수, Native Method Stack의 C/C++로 작성된 JNI 참조이다.

이제 JVM GC의 Root Space가 어디인지 알았으니 Mark-And-Sweep 방식으로 메모리를 관리할 수 있다.

 

 

Mark-And-Sweep 방식의 첫 번째 특징인

"의도적으로 GC를 실행시켜야 한다."

가 있었다.

 

즉, JVM GC는 GC를 실행시키는 기준이 있는 것이다.

이것을 알기위해서는 Heap 영역을 자세히 볼 필요가 있다.

 

JVM의 Heap 영역은 Young Generation과 Old Generation으로 나뉜다.

Young Generation에서 발생하는 GC는 minor gc로 불리고, Old Generation에서 발생하는 GC는 major gc라고 불린다.

 

Young Generation은 Eden, Survival 0, Survival 1영역으로 나뉜다.

  Eden : 새롭게 생성된 객체들이 할당되는 영역

  Survival 영역 : minor gc로부터 살아남은 객체들이 존재하는 영역

  (Survival 영역은 Survival 0 또는 Survival 1 중 하나는 꼭 비어 있어야 한다는 규칙이 있다.)

 

새로운 객체가 계속 생성되다 보면 Eden 영역이 꽉 차는 순간이 오는데, 이 때 minor gc가 일어난다.

 

앞에서 살펴본 Mark-And-Sweep이 진행된다.

 

루트로 부터 Reachable이라 판단된 객체는 Survival 0 영역으로 옮겨진다.

Survival 0 으로 옮겨진 객체들의 숫자가 0에서 1로 증가했다.

이 숫자는 age-bit를 뜻하며 minor gc에서 살아남을 때마다 1씩 증가한다.

 

Eden 영역이 또 꽉차게 되면 또 다시 minor gc가 발생하다.

 

이번에는 Reachable이라 판단된 객체는 Survival 1 영역으로 이동된다.

또 Eden이 차게되고 minor gc가 발생하고 Survival 0 영역으로 이동된다.

 

 객체 하나가 오래 살아남아 age-bit가 3이 되었다.

 

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보다 더 오래 걸린다.

 

Young Generation과 Old Generation을 나눈 이유는 GC설계자들이 어플리케이션을 분석해보니 대부분 객체의 수명이 짧다는 것을 깨달았다. GC도 결국 비용인데, 메모리 특정 부분만을 탐색하며 해제하면 더 효율적이라 생각해서이다.

 

 

Mark-And-Sweep의 두 번째 특징인

"어플리케이션 실행과 GC실행이 병행된다.

-> 어플리케이션의 사용성을 유지하면서 효율적이게 GC를 실행하는 것이 꽤나 어려운 최적화 작업이다."

인데, JVM은 어떤 방식으로 어플리케이션 실행과 GC실행이 병행할까?

 

이해하기 위해 먼저 Stop-The-World를 알아 보자

Stop-The-World는 GC를 실행하기 위해 JVM이 어플리케이션 실행을 멈추는 것을 의미한다.

어플리케이션 실행을 멈추는 것어려운 최적화 작업이다.

 

 

Serial GC

- 하나의 쓰레드로 GC를 실행하는 방식

- 하나의 쓰레드로 GC를 실행시키다 보니 Stop-The-World 시간이 긺

- 싱글 쓰레드 환경 및 Heap 영역이 매우 작을 때 사용하기 위해 나온 방식

 

 

Parallel GC

- 여러개의 쓰레드로 GC를 수행하여 Serial GC 보다 Stop-The-World 시간이 짧음

- 멀티코어 환경에서 어플리케이션 처리 속도를 향상시키기 위해 사용됨

- Java 8에서 기본으로 쓰이는 GC 방식

 

 

CMS GC (Concurrent-Mark-Sweep)

- Stop-The-World 시간을 최소화하기 위해 고안됨

- 대부분의 가비지 수집 작업을 어플리케이션 쓰레드와 동시에 수행해서 Stop-The-World 시간을 최소화 시킴

  하지만 메모리와 CPU를 많이 사용하고 Mark-And-Sweep 과정 이후 메모리 파편화를 해결하는 Compaction이 기본적으로 제공되지 않는 단점 존재

G1 GC의 등장에 따라 Deprecated

 

 

G1 GC (G1 : Garbage First)

- Heap을 일정크기의 Region으로 잘게 나누어 어떤 영역은 Young Generation, 어떤 영역은 Old Generation으로 활용

- 런타임에 G1 GC가 필요에 따라 영역별 Region 개수를 튜닝하여 그에 따라 Stop-The-World를 최소화 할 수 있음

- Java 9이상 부터는 G1 GC를 기본 GC 실행방식으로 사용함

 

 

JVM GC 튜닝 맛보기

 

[목표]

- Old Generation으로 넘어가는 객체 최소화하기

- Major GC 시간을 짧게 유지하기

 

[튜닝과정]

- GC 상태 모니터링 하기

- 알맞은 GC 방식과 메모리 크기 설정

- 적용하기

 

InitialHeapSize는 64Mb 이고, 필요에 따라 MaxHeapSize는 1Gb까지 사용하도록 설정하였고

GC 방식은 Java 8의 기본 GC방식인 ParallelGC 사용한다.

 

jdk 설치시 기본으로 제공되는 jstat 툴 사용하여 jvm을 모니터링할 수 있다.

현재 실행중이 8844번 프로세스에 대해 1초에 한 번 씩 총 10번 gc와 관련된 정보를 출력하도록 설정했다.

 

S0 Survival 0 영역의 사용률
S1 Survival 1 영역의 사용률
E Eden 영역의 사용률
O Old 영역의 사용률
YGC Young Generation 영역의 GC 이벤트 수
YGCT Young Generation의 총 가비지 컬렉션 시간
FGC Full GC 이벤트 수
FGCT Full GC 시간
GCT 모든 가비지 컬렉션 총 시간

*Full GC는 Major GC라고 생각하면 된다.

 

정보를 출력하는 10초 사이에 어플리케이션에 글 작성 요청을 보내서 새로운 객체가 할당되면서 Eden 영역의 사용률이 늘어난 것을 볼 수 있다.

Young Generation 영역에서 청 19번의 GC가 0.314초 동안 실행되었는데 산술적으로 나누면(0.314 / 19) minor gc 한번에 0.016초가 소요되었다고 볼 수 있다.

Major GC는 총 3번 일어났고 0.291초가 소요되었는데 단순히 나누면(0.291 / 3) major gc는 0.097초 소요된 것을 알 수 있다.

 

Major gc가 1초 넘게 소용된다면 어떻게 될까?

DB Connection이 1초가 넘어가면 타임아웃 되는 상황이라면 GC가 장애의 원이이 될 수도 있다.

 

jstat gccapacity 명령을 통해 프로세스가 heap영역을 얼마나 사용 중인지 정확한 수치를 확인할 수 있다.

NG로 시작하는 지표들은 New Generation으로 Young Generation 영역을 뜻하고

OG로 시작하는 지표들은 Old Generation 영역을 뜻한다.

뒤에 붙은 CMN은 영역의 최소 할당 크기, CMX는 영역의 최대 허용 크기, C는 영역의 현재 크기를 각각 KB로 나타내고 있다.

 

Young Generation 영역의 최대 허용 크기와 가까운 약 323Mb정도, Old Generation은 최소 할당 크기와 가까운 약 68Mb정도를 사용중에 있다.

 

이와 같이 GC에 대한 분석 과정을 거친 후 JVM Option을 다르게 설정해볼 수 있다.

Heap 영역 크기 조절, Young Generation 영역 크기 조절 또는 GC 실행 방식들을 변경하면서 가장 최적의 성능을 찾으면 된다.


참고

https://www.youtube.com/watch?v=FMUpVA0Vvjw&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC

 

 

 

728x90
반응형

댓글