2018년 3월 8일 목요일

Java GC 튜닝

GC? 프로그램은 동작 과정에서 데이터를 메모리에 저장하게 된다. 저장할 데이터가 생기면 메모리의 일정 공간을 할당받아 사용하는 것이다. 메모리는 한정적이므로 저장하고자 하는 모든 데이터를 메모리에 기록할 수 없다. 따라서 더 이상 사용(참조)되지 않는 데이터가 있다면 사용하던 메모리 공간을 회수하는 과정이 필요하다. 사용되지 않는 메모리에 대한 회수를 Garbage Collection이라고 하고, Garbage Collector(이하 GC)가 이러한 작업을 수행한다.

Java에선 개발자가 할당한 메모리를 명시적으로 해제하지 않기 때문에 GC가 이를 대신한다. GC는 더 이상 필요없는 객체를 찾아 지우는 작업을 수행하는 것인데, Java엔 이 작업을 효율적으로 동작시키기 위한 여러 장치와 방법이 존재한다.

GC가 동작하면 JVM에 의해 애플리케이션의 실행이 잠시 중단된다. 이를 stop-the-world라 하는데 이 시간이 길어지면 그만큼 애플리케이션의 성능에 안좋은 영향을 준다. GC 튜닝이란 바로 stop-the-world 시간을 줄이는 과정이다. 우선 JVM 메모리 구조에 대해 알 필요가 있을 것이다.

//

# Java Application의 동작 방식과 JVM 메모리 레이아웃.
Java Compiler는 *.java 코드를 *.class(ByteCode)로 변환한다. 컴파일된 *.class 파일의 정보는 Class Loader에 의해 JVM Memory 공간으로 옮겨지게 되는데, 이 공간을 Runtime Data Area라 한다. 마지막으로 Runtime Data Area에 적재된 ByteCode는 Execution Engine에 의해 기계어로 변환되어 실행된다.

Runtime Data Area는 목적에 따라 5개의 영역으로 나뉜다. 1) PC Register, 2) Native Method Stack, 3) JVM Stack, 4) Heap, 5) Method Area로 1~3은 스레드 마다 할당되며 4~5는 모든 스레드가 공유한다.
1) PC Register: OS의 Register와 유사한 것으로 현재 수행중인 JVM 명령의 주소를 기억한다.
2) Native Method Stack: 자바 외 언어로 작성된 네이티브 코드를 위한 스택이다.
3) JVM Stack: 스레드의 제어를 위해 사용되는 스레드의 메서드 실행에 대한 스택 프레임이다.

4) Heap: 인스턴스 또는 객체를 저장하는 공간. GC 대상이다.
5) Method Area: JVM이 시작될 때 생성되는 공간으로 JVM이 읽어들인 클래스, 인터페이스에 대한 상수, 필드 및 메서드, static 변수, 메서드의 바이트 코드 등을 보관한다. Permanent Area 또는 PermGen이라 불리기도 하고 GC 대상 여부는 선택사항이다. (* Java 8 부터는 metaspace라는 이름으로 불린다.)

//

# 기본적인 GC의 동작 방식.
HotSpot VM은 Young 영역과 Old 영역으로 물리적 공간을 구분해서 객체를 관리한다. 이는 생성된지 얼마 안되는 객체와 생성된지 오래된 객체를 구분함으로써 GC를 효율적으로 처리하기 위함이다. Young 영역에서 발생하는 GC를 Minor GC라 하고, Old 영역에서 발생하는 GC를 Major GC라 한다.

Young 영역은 다시 Eden 영역과 2개의 Survivor 영역으로 나뉜다. 새로 생성된 객체는 Eden 영역에 위치하고, Minor GC 후 살아남으면 Survivor 영역 중 하나로 이동한다. 하나의 Survivor 영역이 가득차면 그 중에서 또다시 살아남은 객체를 다른 Survivor 영역으로 이동하고, 이러한 과정이 반복하다 또다시 살아남은 객체는 Old 영역으로 이동하게 된다.

Old 영역은 기본적으로 Young 영역보다 큰 공간으로 잡힌다. Old 영역은 영역이 곽 찰 때 GC를 실행한다. (Major GC). GC엔 여러가지 알고리즘이 있다.

1. Serial GC (-XX:+UseSerialGC) : Mark - Sweep - Compaction 단계를 거친다. 적은 메모리와 코어 개수가 적을 때 쓴다.
2. Parallel GC (-XX:+UseParallelGC)  : Serial GC의 멀티코어 버전. Young Gen에서의 작업을 병렬로 처리한다. Throughput GC라고도 하며 메모리가 충분하고 코어수가 많은 경우에 유리하다. Parallel GC는 Java 7과 Java 8의 기본 GC 수행 방식이다.
3. Parallel Old GC (-XX:+UseParallelOldGC) : Mark - Summary - Compaction 단계를 거친다. Compaction 단계 이전에 Summary 단계를 거치는데, GC 이후의 메모리를 인덱싱한 후 compact 작업을 수행한다. 공간에 대한 인덱싱 작업 때문에 Parallel GC에 비해 메모리를 더 사용하게 된다.
4. CMS GC (-XX:+UseConcMarkSweepGC) : Concurrent Mark and Sweep. 애플리케이션의 응답 속도가 중요할 때 주로 이용되며 Low Latency GC라고도 부른다. stop-the-world 시간이 짧다는 장점이 있지만 메모리와 CPU를 더 많이 쓰는 단점이 있다. Compaction 단계가 기본적으로 제공되지 않아 메모리 조각이 많이 생길 수 있다. 이로 인해 Full GC가 자주 발생하는 등의 안정성 문제가 있다.
5. G1 GC : Garbage First. G1 GC는 메모리 공간을 region이란 이름의 고정 크기로 나눈다. region이 Eden이거나 Survivor, Old 영역일 수 있다. 앞서 Young / Old 컨셉관 조금 다르게 동작하며 구조적으로 Parallel, Concurrent 하게 동작한다. (참고: http://initproc.tistory.com/50)

+ Parallel GC의 경우 CMS에 비해 안정성이 높다고 알려져있다. CMS 알고리즘의 경우 Compaction 단계가 없기 때문에 메모리 단편화에 따른 Full GC 발생 등의 문제가 생길 수 있다. (CPU 자원도 꽤 필요하다고 함.) G1의 경우 Xmx 2G 이하에서는 오버헤드가 발생할 수 있으므로 Xmx 2G 이상의 환경에서 사용하도록 하자.

댓글 없음:

댓글 쓰기