🧐 GC(Garbage Collection)란?
- Java 애플리케이션 구동 시
Heap
영역에서 사용되지 않는 객체를 청소하여 메모리를 확보하는 작업
- Garbage Collector(가비지 콜렉터)가 메모리 해제를 수행
- C/C++ 같은 언어에서는 메모리를 직접 관리 하지만 Java에서는 GC가 대신 관리(Unmanaged)
GC 동작 과정
- gc의 동작 방법을 이해하기 위해 우선 Java의 Heap 메모리 구조를 이해 해야 한다.
Java Heap 구조
Heap
영역은 객체가 할당 되는 영역이고, 각 영역을 간단하게 설명하자면 아래와 같다.
-
Young
- Eden : 객체가 처음 생성되면
Eden
영역에 저장된다.
- Survivor
Eden
영역에 메모리가 꽉차게 되면 Survivor 1
또는 Survivor 2
로 옮겨진다.
- 이 두개의 영역 중 한 영역은 반드시 비어 있어야한다.(Survivor 1 <-> Survivor 2로 이동)
- 해당 공간으로 옮겨지는 객체들은 어디선가 참조(Reference)되어지는 객체들이다.
-
Old
- Young 영역에서 오랫동안 살아남은 객체들은 Old 영역으로 이동한다.
- Old 영역은 Young 영역보다 메모리를 크게 할당하며, 이러한 이유로 Old 영역의 GC는 Young 영역보다 적게 발생한다.
- Eden 영역에서 바로 Old 영역으로 넘어가는 객체도 존재하는데, 이는 객체의 크기가 아주 클 경우 발생한다.
-
Permanent
- Class, Method 등의 코드가 저장되는 영역
- Java8부터는 Metaspace 영역으로 대체되어 Heap이 아닌 Native Method 영역으로 JVM이 아닌 OS에 의해 관리되도록 변경되었다.
GC의 종류
Major GC(Full GC)
Minor GC
- Eden 영역이 꽉찬 경우 발생
- 속도가 Major GC에 비해 빠르다.
- GC가 발생하거나 객체가 각 영역에서 다른 영역으로 이동할 때 애플리케이션의 병목이 발생하면서 성능에 영향을 준다.
- 그래서 핫 스팟(Hot Spot) JVM에서는 스레드 로컬 할당 버퍼(TLABs)라는 것을 사용한다.
- 이를 통해 스레드별 메모리 버퍼를 사용하면 다른 스레드에 영향을 주지 않는 메모리 할당 작업이 가능해진다.
GC 방식
- JDK 5.0 이상에서 지원하는 GC에는 아래와 같이 5가지 방식이 존재
- 명시된 다섯 가지의 GC 방식은 애플리케이션 수행시 옵션을 지정하여 선택할 수 있다.
- Serial Collector
- Parallel Collector (Throughput Collector)
- CMS Collector (Concurrent Mark-Sweep Collector)
- G1 Collector (Garbage First Collector)
- Z Garbage Collector
STW(Stop The World)
STW는 Stop The World의 약자로 자바 어플리케이션은 GC를 실행하기 위한 Thread를 제외하고 이외의 모든 Thread는 멈추고 GC가 완료된 이후에나 다시 Thread가 실행 상태로 돌아가게 하는데 이 멈춤 현상을 말한다.
1. Serial Collector
- 하나의 CPU로 Young과 Old영역을 연속적(serial)으로 처리한다.
- 컬렉션이 수행될 때 애플리케이션이 정지된다.(운영 서버에서 절대 사용하면 안되는 방식)
- MinorGC 뿐 아니라 Major GC인 경우도 올스탑(stop-the-world)
- 메모리가 적고, CPU 코어 개수가 적을 때 적합한 GC 방식
- Old 영역의 GC는 mark-sweep-compact 알고리즘을 사용
- 명시적 지정: -XX:+UseSerialGC
GC 진행 흐름
- 살아있는 객체는 Eden영역에 올라간다.
- Eden영역이 꽉차면 To Servivor영역으로 '살아있는 객체'를 이동시킨다.
- To Servivor영역이 꽉 찰경우 Eden, FromServivor영역에 남은 객체를 Old영역으로 이동시킨다.
Mark-sweep-compact 알고리즘
-
Old 영역으로 이동된 객체들 중 살아 있는 개체를 식별합니다. (Mark)
-
Old 영역의 객체들을 훑는 작업을 수행하여 쓰레기 객체를 식별합니다. (Sweep)
-
필요 없는 객체들을 지우고 살아 있는 객체들을 한 곳으로 모은다 (Compaction)
2. Parallel Collector
- Serial GC와 기본적인 알고리즘은 같으나, Parallel GC는 GC를 처리하는 쓰레드가 여러 개
메모리가 충분하고 코어의 개수가 많을 때 적합한 GC 방식
- MinorGC 뿐 아니라 Major GC인 경우도 올스탑(stop-the-world)
- Old 영역의 GC는 mark-sweep-compact 알고리즘을 사용
- 명시적 지정: -XX:+UseParallelGC
- JDK 5.0 업데이트 6부터 Parallel Old GC 등장하여 Old generation 도 병렬로 처리하게 된다.
- 명시적 지정: -XX:+UseParallelOldGC
3. CMS Collector
- Initial Mark 단계 : class loader에서 가까운 객체 중 매우 짧은 대기 시간으로 살아 있는 객체만 찾는다.
- Concurrent Mark 단계 : 서버 수행과 동시에 살아 있는 객체에 표시를 해 놓는 단계
- Remark 단계 : Concurrent Mark 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다.
- Concurrent Sweep 단계 : 표시되어 있는 쓰레기를 정리한다.
- 2개 이상의 CPU를 사용하는 서버에 적합한 GC 방식이다.
- 장점
- 단점
- 다른 GC 방식보다 CPU 리소스를 많이 사용
- Compaction 단계가 기본적으로 제공되지 않는다.
- 메모리 파편화로, Compaction 수행 시 다른 GC 방식보다 stop-the-world 시간이 더 길어진다.
- GC 대상을 파악하는 과정이 복잡한 여러단계로 수행되기 때문에 다른 GC 대비 CPU 사용량이 높다
- CMS GC는 Java 9부터
deprecated 되었고 결국 Java 14에서는 사용이 중지
- 명시적 지정: -XX:+UseConcMarkSweepGC
4. G1 Collector
- 바둑판의 각 region(영역)에 객체를 할당하고 GC를 실행한다. 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행한다.
- CMS Collector의 CPU리소스 및 메모리 파편화의 단점을 해결하기 위해 만들어진 방식이다.
- 가장 많은 공간이 있는 곳 부터 메모리 회수 진행하기 때문에 Garbage First 라는 이름이 붙었다
- G1 Collector의 가장 큰 장점은 어떤 GC 방식보다도 빠르다는 점이다.
- Young의 세가지 영역 (Eden 영역, 2개의 Survivor 영역)에서 데이터가 Old 영역으로 이동하는 단계가 사라진 GC 방식이다.
- JDK 6에서는 early access라고 부르며 시험삼아 사용할 수 있고, JDK 7에서 정식으로 사용 가능한 방식이다.
- 명시적 지정: -XX:+UseG1GC
5. Z Garbage Collector
- GC 일시 정지 시간이 10ms 미만의 애플리케이션을 위한 GC
- 대량의 메모리(8MB ~ 16TB)를 low-latency로 잘 처리하기 위해 디자인 된 GC
- 객체를 가리키는 변수의 포인터에서 64bit를 활용하기 때문에 64bit 운영체제에서만 사용가능
- JDK 11 부터 실험적으로 도입 되었으며 Java 15에 release 되었다.
- region으로 간단한 힙 메모리 구조가 G1과 유사하다.
- 명시적 지정: -XX:+UnlockExperimentalVMOptions
코드를 통한 GC 확인
예제 코드
public static void main(String [] args) {
List<String> list1 = new ArrayList<>();
for(int i = 0; i < 10000; i++) {
list1.add("Random" + Math.random());
}
list1 = null; // remove the reference to list1
Runtime.getRuntime().gc();
}
GC 로그
// 메모리 할당을 하려는데 부족해서 GC를 실행했다는 뜻
[0.330s][info ][gc,start ] GC(5) Pause Young (Allocation Failure)
[0.332s][info ][gc,heap ] GC(5) DefNew: 4928K(4928K)->512K(4928K) Eden: 4416K(4416K)->0K(4416K) From: 512K(512K)->512K(512K)
[0.333s][info ][gc,heap ] GC(5) Tenured: 5089K(10944K)->6626K(10944K)
[0.333s][info ][gc,metaspace] GC(5) Metaspace: 9681K(9984K)->9681K(9984K) NonClass: 8549K(8704K)->8549K(8704K) Class: 1131K(1280K)->1131K(1280K)
[0.333s][info ][gc ] GC(5) Pause Young (Allocation Failure) 9M->6M(15M) 2.138ms
[0.333s][info ][gc,cpu ] GC(5) User=0.01s Sys=0.00s Real=0.00s
// System.gc() 실행
[0.334s][info ][gc,start ] GC(6) Pause Full (System.gc())
[0.334s][info ][gc,phases,start] GC(6) Phase 1: Mark live objects
[0.338s][info ][gc,phases ] GC(6) Phase 1: Mark live objects 4.063ms
[0.338s][info ][gc,phases,start] GC(6) Phase 2: Compute new object addresses
[0.340s][info ][gc,phases ] GC(6) Phase 2: Compute new object addresses 1.225ms
[0.340s][info ][gc,phases,start] GC(6) Phase 3: Adjust pointers
[0.342s][info ][gc,phases ] GC(6) Phase 3: Adjust pointers 2.094ms
[0.342s][info ][gc,phases,start] GC(6) Phase 4: Move objects
[0.342s][info ][gc,phases ] GC(6) Phase 4: Move objects 0.714ms
[0.342s][info ][gc,heap ] GC(6) DefNew: 1949K(4928K)->0K(4928K) Eden: 1437K(4416K)->0K(4416K) From: 512K(512K)->0K(512K)
[0.342s][info ][gc,heap ] GC(6) Tenured: 6626K(10944K)->5514K(10944K)
[0.342s][info ][gc,metaspace ] GC(6) Metaspace: 9681K(9984K)->9681K(9984K) NonClass: 8549K(8704K)->8549K(8704K) Class: 1131K(1280K)->1131K(1280K)
[0.342s][info ][gc ] GC(6) Pause Full (System.gc()) 8M->5M(15M) 8.287ms
[0.342s][info ][gc,cpu ] GC(6) User=0.01s Sys=0.00s Real=0.01s
GC 옵션 및 튜닝
- 메모리 크기가 큰 경우
- GC 발생 횟수 감소, GC 수행 시간은 길어진다.
- 메모리 크기가 작은 경우
- GC 발생 횟수 증가, GC 수행 시간 증가한다.
메모리 크기를 크게할지 작게 할지에 대한 정답은 없다.
GC 튜닝 절차
- GC 상황 모니터링 -> 결과 분석 후 -> 튜닝 여부 결정 -> GC방식/메모리 크기 지정 -> 결과 분석 -> 만족스러운 결과가 나올때까지 반복
마치며
- GC가 자주 수행 되는 경우 STW로 인하여 애플리케이션의 성능이 저하된다는 것을 알 수 있다.
- 애플리케이션 분석 또는 모니터링을 통해 GC 튜닝의 목표를 설정하는 것이 중요하다.
- 메모리를 적게 사용 또는 GC 횟수를 줄이는 것이 목표인지를 정하고 목표치에 근접하도록 JVM 파라미터를 조정하는 것이 필요하다.
참고 링크