본문 바로가기
학습/Java

[java] 가비지 컬렉션

by KKambi 2020. 7. 5.

DZone의 Java Memory Management 포스팅

YABOONG님의 자바 메모리 관리 - 가비지 컬렉션 포스팅

네이버 D2의 Java Reference와 GC 포스팅

PreamTree님의 가비지 컬렉션 소개 포스팅을 읽고 학습한 내용을 정리한 글입니다.

 

 

 

가비지 컬렉션 프로세스의 이해


스택 변수로부터 참조된 타입에 따라서, 힙의 객체들은 가비지 컬렉션이 적용되는 타이밍이 다르다.

그림1_Garbage-eligible objects (가비지로 회수될 객체들)

예컨대 해당 사진에서 붉은색 객체들은 가비지 컬렉터에 의해 회수된다 (eligible to be collected)

스택 변수로부터 Strong Referenced되고 있었지만, 참조가 사라져 더 이상 접근할 수 없는 객체들이 그렇다.

 

 

 

가바지 컬렉션 프로세스의 특징

1. Java에 의해 자동으로 시작되며, 가비지 컬렉션 프로세스를 시작할지 말지는 Java에 달려있다.

2. 비용이 큰 작업이다. GC 타입에 따라 차이가 있긴 하지만, 보통 가비지 컬렉터가 작동하면 모든 쓰레드가 정지된다.

3. 단순히 가비지를 찾아내고 메모리를 회수하는 작업보다 훨씬 복잡한 과정이 들어있다.

4. System.gc()의 호출은 가비지 컬렉터가 바로 실행됨을 보장하지 않는다.

 

 

 

가비지 컬렉션은 그럼 어떻게 작동할까?

"Mark and Sweep"이라는 작업이 사용된다.

자바는 스택 내의 변수들을 분석하고, 살아있을 필요가 있는 객체들을 표시한다 (Mark)

그러고 나서 사용되지 않는 객체들을 청소한다 (Sweep)

 

즉, 자바는 어떤 garbage도 표시하지 않는다. 살릴 객체들만 찾아내서, 나머지를 청소하는 것이다.

보통 가비지 객체가 더욱 많기에, 살릴 객체만 찾아내는 작업이 더 빠르다.

 

이러한 Mark and Sweep을 최적화하기 위해 Heap memory는 여러 파트로 구성되어 있다.

우리는 JVisualVM이라는 Java JDK 툴을 이용하여 메모리 사용량을 시각적으로 분석할 수 있다.

(Visual GC 플러그인 설치)

그림2_Heap Memory Generations

 

그림3_GC 영역 및 데이터 흐름도

 

 

 

Young Generation

Young Generation 영역은 Eden(1번) + Survivor(2번, 3번)으로 구성된다.

Young 영역에서 발생하는 GC를 Minor GC라고 부른다.

 

- 새로 생성한 대부분의 객체는 Eden 영역에 위치한다.

- Minor GC가 발생한 후 살아남은 객체들은 S0 또는 S1 중, 현재 사용되는 영역으로 이동한다.

- 다음 Minor GC가 발생했을 때 살아남은 모든 객체를 다른 Survivor 영역으로 이동시킨다.

기존에 사용하던 Survivor 영역은 아무 데이터도 없는 상태가 된다.

 

정리하자면 다음과 같다.

1. 객체 생성 후의 첫 Minor GC에서 살아남은 객체는 Eden에서 S0으로 이동

2. 다음 Minor GC -> Eden + S0에서 살아남은 객체들이 S1으로 이동

3. 다음 Minor GC -> Eden + S1에서 살아남은 객체들이 S0으로 이동

4. 반복...

(그림 2의 6번을 보면, GC가 발생할 때마다 빈 공간이 생겨나는 Eden 영역을 볼 수 있다)

 

여기서 Survivor 영역 중 하나는 반드시 비어 있는 상태로 존재해야 한다는 것을 볼 수 있다.

두 서바이버 영역에 모두 객체가 존재하거나, 두 영역 모두 사용량이 0이라면 시스템이 정상적인 상황이 아니라고 생각할 수 있다.

 

 

 

Old Generation

X라운드의 Minor GC가 발생한 후에도 살아남은 객체들은 오랫동안 사용하게 될 객체라고 판단된다.

(X는 JVM의 설정에 따라 변경할 수 있다)

이런 객체들은 Old(4번) 영역으로 이동하게 된다. Young에서 Old로 승급(Promotion)하게 되는 것이다.

 

Old 영역에서 발생하는 GC는 Major GC라고 부른다.

Eden 영역에 비해 메모리의 많은 부분을 차지하고 있고, 그만큼 자주 일어나진 않기 때문이다.

 

 

 

Metaspace

그림 2의 5번 영역 Metaspace는 JVM에서 클래스를 로드할 때 해당 메타데이터를 저장하는 공간이다.

자바 8 이전에는 메타데이터 + String Pool을 저장하는 공간으로 PermGen이라고 불렸으나

String 객체가 너무 많을 경우 크래쉬가 발생하는 문제 때문에

현재 메타데이터만 저장하는 공간이 분리되었다.

 

 

 

가비지 컬렉션의 타입


크게 3가지 방식으로 나눌 수 있으며, 세부적인 구현에 따라 여러가지 타입이 존재한다.

프로그래머는 이 중 1가지를 반드시 사용해야 한다. 자바는 기본적으로 하드웨어에 기반하여 타입을 선택한다.

 

 

 

싱글 스레드 사용 (Serial)

1번 - Serial GC

-XX:+UseSerialGC

Single Thread Collector를 사용한다. 즉, 데스크톱의 CPU 코어가 1개만 있을 때 만들어진 방식이다.

Young / Old 영역 GC가 모두 1개의 스레드를 이용해 순차적으로 처리된다 (Serial)

따라서 메모리와 CPU 코어 개수가 적을 때만 적합하다.

컬렉션 성능이 많이 떨어지기 때문에 대부분의 운영 서버에서 사용해선 안 된다.

 

Young 영역에서의 GC (Minor GC)는 위에서 설명한 방식을 사용한다.

Old 영역에서의 GC (Major GC)는 Mark-Sweep-Compact 알고리즘을 사용한다.

Old 영역에 살아 있는 객체를 식별(Mark)하고,

Heap의 앞 부분부터 확인하여 살아 있는 것만 남기고(Sweep),

각 객체들이 연속해서 쌓이도록 힙의 가장 앞 부분부터 채워서 객체가 존재하는 부분 / 없는 부분으로 나눈다 (Compact)

 

 

 

멀티 스레드 사용 (Parallel)

2번 - Parallel GC (Throughput GC)

-XX:+UseParallelGC

Throughput Collector를 사용한다. 멀티 쓰레드를 사용한다는 뜻이다.

다만 Minor GC만 병렬적으로 수행하고, 여전히 Major GC는 싱글 스레드로 Mark-Sweep-Compact 하게 된다.

기본적인 알고리즘은 Serial GC와 같다고 할 수 있다.

 

 

 

2-1번 - Parallel Old GC (Throughput Old GC)

-XX:+UseParallelOldGC

멀티 쓰레드를 Minor & Major GC 양쪽에 모두 사용한다.

이 경우 Old 영역의 GC는 Mark-Summary-Compact 알고리즘을 사용한다.

또한 효율성을 높이기 위해, 앞선 GC에서 Compact된 영역 또한 탐색한다.

 

 

 

동시성 - Mostly Concurrent GC

일반적으로 가비지 컬렉션은 작동할 때 모든 쓰레드를 멈추기 때문에 비싼 작업이다.

하지만 해당 타입으로 어플리케이션 구동과 동시에 GC가 발생할 수 있게 한다.

하지만 100% 동시성을 보장하진 않고, "mostly" concurrent 하다.

여전히 모든 쓰레드가 멈추는 타이밍이 존재한다는 것이다.

 

 

 

3-1번 - Garbage First (G1 GC)

-XX:+UseG1GC

적절한 stop-the-world 시간을 유지하며 멀티 쓰레드를 사용한다.

Young / Old 영역과는 상관없는 새로운 알고리즘을 사용한다.

그림4_Heap을 바둑판처럼 사용

G1 GC는 바둑판의 각 영역에 객체를 할당하고 GC를 수행한다.

그러다가 해당 영역이 가득 차면, 다른 영역에서 객체를 할당하고 GC를 실행한다.

바둑판 모양의 영역이 각각 Eden / Survivor / Old로 역할이 동적으로 바뀌어 나가는 것이다.

JDK 7 기준으로 성능이 매우 빠른 편에 속하지만 불안정한 측면도 있다고 한다. (최근에는 어떤지...?)

사용하는 알고리즘은 다음에 소개할 CMS GC와 유사하다.

 

 

 

3-2번 - Concurrent Mark Sweep (CMS GC)

-XX:+UseConcMarkSweepGC

stop-the-world 시간이 제일 짧은 타입.

Low Latency GC라고도 부르며, 응답 속도가 중요한 어플리케이션에서 주로 사용되었다고 한다.

하지만 메모리와 CPU를 많이 사용하며, Compact 단계가 기본적으로 제공되지 않는다는 단점이 있다.

그래서 조각난 메모리가 많을 경우 Compaction을 실행할 때 어플리케이션이 멈추는 시간이 더 길다.

결국 JDK 9부터 deprecated 되었다.

 

Minor GC는 Parallel GC와 동일하다.

Major GC는 다음과 같은 단계를 거친다.

1. Initial Mark

2. Concurrent Mark

3. Remark

4. Concurrent Sweep

그림5_Serial GC와 CMS GC 비교

1번 Initial Mark 단계에서는 클래스 로더에서 가장 가까운 객체 중, 살아 있는 객체만 찾는 것으로 끝난다.

따라서 stop-the-world 시간이 매우 짧다 (싱글 스레드 사용)

 

2번 Concurrent Mark 단계에서는 1번에서 찾은 객체들에서 참조하고 있는 객체들을 따라가면서 확인한다.

이 단계는 어플리케이션을 멈추지 않고 동시에 진행된다.

 

3번 Remark 단계에서는 2번 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다.

이 단계는 어플리케이션을 멈춘다 (멀티 스레드 사용)

 

4번 Concurrent Sweep 단계에서는 garbage object를 청소하는 작업을 수행한다.

이 단계는 어플리케이션을 멈추지 않고 동시에 진행된다.

 

 

 

Tips and Tricks


DZone에서 제공하는 메모리 관리 팁이다.

 

1. Memory footprint (메인 메모리 사용량)을 최소화하기 위해서, 변수의 scope를 가능한 한 줄여라.

스택으로부터 top scope가 pop될 때마다, 해당 scope에 대한 레퍼런스가 삭제되고, 이는 객체들을 가비지 컬렉션의 대상이 되도록 만든다.

-> 왜? 그런지 나는 이해가 잘 안된다. 답을 아시는 분은 댓글로 남겨주시면...!

 

2. 사용하지 않는 객체 참조는 null을 참조하도록 하라.

이는 더 이상 쓸모 없는 객체가 청소되도록 한다.

 

3. finalizer 사용을 피해라.

이펙티브 자바에 나왔던 것처럼, 이는 프로세스를 느리게 만들며 그렇다고 메모리 회수를 보장하지도 않는다.

Phantom reference로 대체하는 것이 좋다.

 

4. Weak & Soft Reference가 사용가능한 상황에서 Strong Reference를 사용하지 마라.

대부분의 메모리 부족은 해당 데이터가 다시 필요하지 않음에도 메모리에 캐싱하는 상황에서 발생한다.

 

5. JVisualVM은 특정 포인트의 Heap Dump를 뜰 수 있게 해준다.

클래스별 메모리 점유율 등과 같은 분석이 가능하다.

 

6. 어플리케이션 요구사항에 맞춰 JVM 튜닝을 해라.

초기 힙 사이즈 / 최대 힙 사이즈 / 쓰레드 스택 사이즈 / Young genration size 등과 같은 설정이 필요하다.

다음과 같이 다양한 옵션들이 있다.

 

-Xms512m : set the initial heap size to 512 megabytes.

-Xmx1024m : set the maximum heap size to 1024 megabytes.

-Xss128m : set the thread stack size to 128 megabytes.

-Xmn256m : set the young generation size to 256 megabytes.

 

7. OutOfMemoryError가 발생했을 때

메모리 누수를 분석하기 위해 –XX:HeapDumpOnOutOfMemory 실행 인자를 사용할 수 있다.

다음에 메모리 부족 에러가 발생했을 때 Heap Dump File을 생성해준다.

 

8. -verbose:gc

GC가 발생할 때마다 결과를 콘솔에 표시할 수 있다.

 

 

 

마무리


각 GC 타입마다 장단점이 있고, 어플리케이션에 적절히 활용해야 한다.

예컨대 특정 서비스에서 A라는 GC가 효율적으로 작동한다고 해서, 다른 서비스에서 해당 GC가 훌륭하게 적용되라는 법은 없다.

 

각 서비스의 WAS 특성에 맞춰 사용해야 한다는 것이다.

객체 크기와 생존 주기가 모두 다르고, 하드웨어 또한 다양하다.

WAS의쓰레드 개수 / 장비당 WAS 인스턴스 개수/  GC 옵션 등은 지속적으로 튜닝 & 모니터링하며 적합한 값을 찾아야 한다.

 

 

댓글