이번 챕터는 JVM 튜닝의 실제 사례를 분석하고 알아보는 시간이다. 비록 실제 GC 튜닝 경험은 없지만 사례를 생각하면서 어떻게 하면 우리 서비스에 적용시킬 수 있을까에 대한 고민을 해본다.
대용량 메모리 기기 대상 배포 전략
상황 : 하루 페이지 뷰가 15만의 웹 사이트. 모든 하드웨어 자원을 온전히 혼자 사용하고 있기 때문에 여유로운 상황. 관리자는 -Xms 와 -Xmx 매게 변수를 지정하여 자바 힙 크기를 12GB 로 고정. 하지만 서버 실행 효율이 기대에 한참 못 미치고, 장시간 응답하지 않는 일이 발생했다.
원인 : 원인은 GC에 있었다. 핫스팟 가상 머신을 서버 모드로 실행했고, Parallel GC 를 사용 중이다. 하지만 12GB 에 달하는 힙 메모리를 FULL gc 하기 위해서 최장 14초까지 정지가 된 상황이다. 서비스 특성 상, 사용자가 웹 페이지를 요청하면 해당 파일을 디스크 -> 메모리로 올리는데 이때 직렬화 하는 과정에서 메모리에 거대 객체가 쌓였다. 거대 객체는 구세대에 만들어졌고, 힙을 12GB 로 준비했지만 금방 차게 되어서 FULL gc 가 발생했고 10초씩 일시 정지되는 사태가 발생했다.
분석 : 소스 코드 상관 없이 힙 메모리를 너무 크게 잡아서 회수하고 재활용하는데 오랜 시간이 걸린 것이다. 간단히 힙 크기를 1.5GB 나 2GB 로 줄여도 일시 정지 시간은 줄일 수 있다. 하지만 하드웨어 값을 낭비한 셈이 된다.
해결 : JVM 의 모든 GC 는 특정한 애플리케이션 타입과 동작 시나리오를 목표로 설계되었다. 그렇기 때문에 애플리케이션을 이 시나리오에 알맞게 설정하는 것이 중요하다. 현재 시나이오의 문제를 해결한 방식은 32비트 가상 머신 다섯 개로 논리 클러스터를 구축하는 방식으로 해결 되었다. 메모리를 프로세스당 2GB씩 할당해서 (힙 크기는 1.5GB로 고정) 총 10GB를 활용하게 했다. 그리고 아파치 서비스를 두어 웹 사이트로 들어오는 부하를 분산하는 역할을 맡겼다. 추가적으로, 고객들은 응답 속도를 더 중요하게 생각함으로 CMS GC 로 변경해서 서비스가 장시간 정지하는 문제가 사라졌다.
내 생각 : 서비스를 분할해서 하드웨어 자원을 나누는것도 좋은 접근이 될 수 있다. 왜냐면 한 서버에서 너무 큰 GC 가 발생하게 되면 단일 장애 포인트가 생길 수 있고 이 사례처럼 지연시간이 길어질 수 있다. 하지만 멀티 스레드와 같이 멀티 프로세스로 나뉘게 되면 역시 동시성과 같은 문제 등은 여전히 생각해야 하는 과제일거 같다.
클래스 로딩 시간 최적화
JVM 에서 클래스 로딩을 하는 시간은 생각보다 오래 걸린다. 로딩 과정에서 안전한 바이트코드인지 검증하는 단계를 거치기 때문에 이렇게 오랜 시간이 걸릴 수 있다. 하지만 이클립스와 같은 환경에서 JVM 어플리케이션을 실행 시킬 때는 이런 바이트코드 검증이 따로 필요 없음으로 가상 머신에 -Xverify:none 옵션을 설정하여 바이트 코드 검증을 건너뛰어서 클래스 로딩 속도를 개선시키는 방법도 있다.
컴파일 시간 최적화
JVM 에 컴파일 과정에서 핵심 적인 부분은 JIT 컴파일러라고 할 수 있다. 간단하게 말해, 컴파일 시간이 많이 소요되는 코드 (핫 코드) 를 가상 머신의 JIT 컴파일러에 캐싱하는 것이다, 그리고 이것을 컴파일 시간이라 볼 수 있다.
자바는 C 나 C++ 과는 다르게 바이트코드로 변환 된 .class 파일을 해석하여 실행하는 구조다. 그리고 이 속도는 상당히 느리다는 단점이 있다. JDK 1.2 이후에 JIT 컴파일러를 제공하기 시작하며, 일정 횟수 이상 호출 되는 자바 메서드를 핫 코드로 분류하여 JIT 컴파일러에 넘긴다. 그리고 자바 프로그램의 코드 자체에 문제가 없는 한 프로그램을 오래 실행할 수록 코드가 꾸준히 최적화 되어서 점점 빨라질 수 있다.
JVM 은 JIT 컴파일러를 억제하는 -Xint 매개 변수도 제공해준다. 즉, 가상 머신을 순수한 인터프리트 방식으로 수행할 수 있게 도와주지만 총 구동 시간으로 보면은 더 오래 걸릴 것이다.