TODAY 0
JAVA 시스템 Tip

#컴퓨터 자원과 운영체제에 따라 달라지는 GC 성능
Tomcat의 인스턴스 개수를 정하여 효율적으로 컴퓨터의 자원을 활용하기 위해서는 CPU의 코어의 개수, 운영체제가 32bit인지 64bit인지, JVM에서는 어떤 Garbage Collector를 사용하는지에 따라 달라질 수 있기 때문에 단순하게 접근하기는 힘들다고 볼 수 있다.

#CPU 코어의 수
보통 하나의 인스턴스를 운용하는데 1개 정도의 CPU를 사용하는게 최적화된 환경이다. 예를 들면 2CPU 머신의 경우 2개의 Tomcat 인스턴스가 적정하다. CPU 수보다 많은 인스턴스를 사용할 경우에는 각각의 인스턴스에 CPU가 배정 되는 시간이 느려지기 때문에 성능 저하로 이어질 가능성이 높다.

#메모리의 크기, 운영체제의 비트 체계
64bit JVM은 32bit보다 30~40%의 Heap을 더 사용한다. 따라서 더 많은 메모리 할당이 필요하고, GC할 때 더 많은 시간이 걸린다. 하지만 32bit의 JVM은 아래와 같은 제약사항을 가진다.

운영체제      제약사항
리눅스       최대 2GB Heap, hugemem 커널의 경우 3GB
윈도우       최대 1.5GB Heap
Mac OS X   3.8GB
G1 GC를 제외한 GC에서는 JVM Heap을 무한정 늘리면 Full GC 시간 증가로 인해 오히려 성능 병목이 될 수 있다. 32bit JVM을 사용하고 2-4GB 이하의 Heap 설정을 사용하는게 나을 수 있다. JVM의 Heap을 증가시키기 보다는 JVM의 인스턴스를 늘려 클러스터링이나 로드밸런서로 가용성을 확보하는 방법을 권장한다.

32bit의 운영체제에서 2GB의 메모리를 활용하는 JVM의 권장 Option
-Djava.awt.headless=true -server -Xmx1024m ?Xms1024m -XX:NewSize=384m -XX:MaxNewSize=384m -XX:MaxPermSize=128m

#Garbage Collector
JDK 7부터 본격적으로 사용할 수 있는 G1 GC를 제외한, Oracle JVM에서 제공하는 모든 GC는 Generational GC이다. 즉 객체는 처음 생성되면 Eden(Young) 영역으로 들어간다.
Old 영역에서 일어나는 ‘Major GC’는 Full GC 라고도 하는데, JVM에서 Full GC가 일어나면 모든 Thread가 멈추는 Stop the world 현상이 벌어진다. Full GC는 전체 객체들의 참조를 확인하면서 사용되지 않는 객체를 표시하여 삭제한다. 메모리 영역에 대한 compact가 필요하여 속도가 매우 느리다. 이렇게 활용되는 GC 알고리즘은 Mark & Compact 이라고 한다.
JVM을 튜닝한다는 의미는 Old 영역으로 넘어가는 객체의 수를 최소화하는 것과 Full GC의 실행 시간을 줄이는 노력이다.

Full GC 시간 줄이기
Full GC의 실행 시간은 상대적으로 Minor GC에 비하여 길다. 그래서 Full GC 실행에 시간이 오래 소요되면(1초 이상) 연계된 여러 부분에서 타임아웃이 발생할 수 있다. 그렇다고 Full GC 실행 시간을 줄이기 위해서 Old 영역의 크기를 줄이면 자칫 OutOfMemoryError가 발생하거나 Full GC 횟수가 늘어난다. 반대로 Old 영역의 크기를 늘리면 Full GC 횟수는 줄어들지만 실행 시간이 늘어난다. Old 영역의 크기를 적절하게 ‘잘’ 설정해야 한다.
이는 정답이 정해져있는 것이 아니라 시스템에 따라 지속적으로 모니터링하면서 수치를 정해야 한다는 뜻으로 지금까지의 내용을 JVM Options으로 예를 들면 아래와 같다.

JVM Options 예시
-Djava.awt.headless=true -server ?Xms2048m -Xmx2048m -XX:NewSize=768m -XX:MaxNewSize=768m -XX:NewRatio=2 -XX:PermSize=128m -XX:MaxPermSize=256m -XX:USeParNewGC
ParallelGC, UseConcMarkSweepGC와 같은 옵션을 볼 수 있는데 구체적으로 각기 다른 GC 알고리즘을 살펴보도록 하겠다.

#(1)Garbage Collector - The Serial GC
Serial GC는 가장 단순한 GC이지만 사용하지 않는 것을 추천한다. 싱글 쓰레드 환경을 위해 설계 되었고 아주 작은 Heap영역을 가진다. Full GC가 일어나는 동안 애플리케이션 전체가 대기해야하는 현상이 발생하기 때문에 서버 애플리케이션에 적당하지 않다.

#(2)Garbage Collector - The Parallel GC Threads
Java 8의 디폴트 GC인 Parallel GC는 문자 그대로 병렬로 GC한다. 메모리가 충분하고 CPU의 성능과 코어 개수가 많아 순간적으로 트래픽이 몰려도 일시 중단을 견딜 수 있고 GC에 의해 야기된 CPU 오버 헤드에 대해 최적화할 수 있는 애플리케이션에 가장 적합합니다.
-XX:+UseParallelGC 옵션을 사용하여 Minor GC 에서 활성화 할 수 있다.
-XX:+UseParallelOldGC 옵션을 사용하여 Major GC에서 활성화 할 수 있다.

#(3)Garbage Collector - The Concurrent Mark & Sweep GC
간단히 CMS GC라고도 하는데, Class Loader로 부터 최초의 객체 참조가 발생하는 Root를 시작으로 객체의 참조 상태를 관리한다.

#(4)Garbage Collector - The G1(Garbage First) GC
G1 GC는 JDK 7u4 부터 도입되었으며 4GB이상의 더욱 큰 자원을 제공하고 장기적으로 CMS를 대체하기 위해 설계되었다. G1 GC를 이해하려면 지금까지의 Young 영역과 Old 영역에 대해서는 잊는 것이 좋다.
GC GC는 Generational 한 알고리즘과는 다르게 백그라운드의 멀티 쓰레드를 활용해 1MB에서 32MB까지의 수 많은 리젼으로 Heap을 분할한다.

G1 GC는 위와 같이 바둑판의 각 영역에 객체를 할당하고 GC를 실행한다. 그러다가, 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행한다. 즉, 지금까지 설명한 Young의 세가지 영역에서 데이터가 Old 영역으로 이동하는 단계가 사라진 GC 방식이라고 이해하면 된다.
G1 GC의 가장 큰 장점은 성능이다. G1은 지연 시간을 줄이기 위해서 지금까지 설명한 어떤 GC 방식보다도 빠르다.

하지만 이와 같이 4GB 이상의 큰 Heap을 가지는 것은 요즘과 같이 마이크로 서비스 아키텍쳐에서는 논쟁 거리가 될만하다. 지난 몇 년동안 많은 개발자들이 거대한 시스템을 작은 마이크로 단위로 옮기는 노력을 해왔기 때문이다.
이는 다양한 애플리케이션을 서로 격리하고 효율적인 배포 프로세스를 통해 거대한 애플리케이션 클래스를 메모리에 로드하는데 소요되는 비용을 절감하는 등 많은 요인을 포함하고 있다. 이는 애플리케이션을 동일한 물리적 머신에 배포할 수 있도록 하는 Docker와 같은 컨테이너 기술에 의해 가속화 되어 왔다.
Class Unloading에 대한 이슈 : http://openjdk.java.net/jeps/156

Hot Deploy(Hot Swapping)를 많이 할 경우 Java 7의 G1 GC에서는 Perm Generation 영역에 문제가 발생할 수 있다.
- JDK 7의 G1 GC는 Class Unloading을 Full GC가 발생했을 시에만 수행하게 된다.
- 이 문제는 JDK 8u40 버전에서 Perm Generation을 없애고 Metaspace 방식으로 바꾼 후에 해결되었다.

-XX:+UseLargePagesInMetaspace
JDK 8에서는 Perm 영역이 아니라 Metaspace에 클래스 정보가 올라가는데 이때 그 영역이 크면 GC 시간이 오래 걸릴 수 있는데 이럴 때는 Metaspace에 Large Page를 사용하여 접근하도록 JVM 옵션을 주면 대부분 문제가 해결될 수 있다는 것

JVM Options 예시
-Djava.awt.headless=true -Dfile.encoding=UTF-8 -server -Xms2048m -Xmx2048m -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -XX:+DisableExplicitGC -XX:+UseStringDeduplication

# JVM 튜닝 꼭 해야할까?
- JVM 튜닝은 가장 마지막에 고려하는 것이 좋다.

#GC 모니터링
GC 로그를 위한 JVM Options
XX:-PrintGC -XX:-PrintGCDetails -XX:-PrintGCTimeStamps -XX:-TraceClassUnloading -XX:-TraceClassLoading

#스레드 덤프 획득
스레드 덤프를 획득하는 방범은 여러 가지가 있지만 기본적으로 JVM의 옵션을 통해 Out Of Memory 에러 발생시 아래와 같이 쓰레드 덤프를 획득할 수 있다.
-XX:-HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./java_pid<pid>.hprof
애플리케이션의 현재 프로세스를 확인하고 실시간으로 쓰레드 덤프를 얻기 위해서는 획득할 당시의 스레드 상태만 알 수 있기 때문에 스레드의 상태 변화를 확인하려면 5초 정도의 간격으로 5 ~ 10회 정도 획득하는 것이 좋다.

#jstat 명령을 통한 GC 모니터링
현재 JVM의 메모리 상태를 확인할 수 있다.
$JAVA_HOME/bin/jstat

#Memory Analyzer(MAT)
이클립스를 사용한다면 MAT 플러그인도 도움이 된다. MAT은 hprof 파일을 분석해서 메모리 분석, 통계를 내는 기능을 제공한다.