본문으로 건너뛰기

20.4 JVM 튜닝

개요

Java 언어에서 오브젝트에 대한 메모리 할당과 해제는 자바 가상 머신이 자동으로 관리한다. 메모리에서 사용된 오브젝트를 자동으로 제거하는 메커니즘을 ‘가비지 컬렉션’이라 한다. 가비지 컬렉션은 Java 프로그램 성능에 결정적인 영향을 주기 때문에 그 동작방법을 이해하고 튜닝하는 것은 매우 중요하다.

가비지 컬렉션 역할

자바언어에서는 생성된 오브젝트를 메모리에 할당하기 위해 코드에서 명시적으로 메모리 공간을 확보하거나 제거할 필요가 없다. 이것은 C나 C++ 등의 언어와 자바 언어를 비교하였을 때 가장 큰 차이점 중의 하나이다. C나 C++에서는 명시적으로 프로그램이 사용하는 메모리 영역을 할당하고 사용한 영역을 해제하도록 프로그램 코드로 작성해야 한다.

자바 가상 머신은 실행되고 있는 자바 프로그램 내에서 어디에도 참조되지 않는 불필요한 자바 오브젝트를 찾아 해당 영역의 메모리를 자동으로 해제한다. 이렇게 자바 오브젝트를 자동으로 제거하는 방법을 ‘가비지 컬렉션’이라고 한다. 자바 가상 머신 내부에서 가비지 컬렉션은 별도의 스레드로 주기적으로 동작한다. 이 가비지 컬렉션이 동작하는 주기는 자바 가상머신 힙의 크기와 애플리케이션에서 힙 메모리를 어떻게 사용하는지에 따라 달라진다.

Java 애플리케이션이 실행되면 오브젝트는 메모리에 로드된다. 사이즈가 큰 객체를 사용하거나 사용하는 오브젝트 개수가 많으면 그만큼 메모리 사용량이 증가한다. 자바에서 사용 가능한 메모리 영역을 힙 영역이라고 한다. 힙 영역 이외에도 Permanent 영역이 있다. 새롭게 생성된 오브젝트들을 계속 로드하면 자바가 사용할 수 있는 메모리 공간이 가득 찬다.

메모리가 가득 차면 새 객체를 로드 할 수 없어서 프로그램을 실행할 수 없다. 그래서 이를 방지하기 위한 구조가 가비지 컬렉션, GC(Garbage Collection)이다. GC는 힙에 여유가 없을 때 사용되는 객체와 사용되지 않는 객체를 판별하여 사용되지 않은 객체를 청소 대상으로 하여 메모리에서 삭제하는 방법으로 메모리 공간을 확보한다.

그림 3. 자바에서 GC란 메모리에 대한 분리 수거

자바 프로그래머들이 GC와 같은 JVM의 내부 작동 방법에 관해서 관심을 기울이지 않을 수도 있다. 하지만 신뢰성이 높고 성능이 좋은 자바시스템을 구축하기 위해서는 JVM 내부 메모리 관리 기능인 GC에 대해 그 구조를 파악하는 것이 매우 중요하다. 메모리 관련(OutOfMemoryError 등) 장애가 발생하였을 때 신속하게 파악하고 조치를 취하기 위해서는 GC 에 대한 지식은 도움이 된다.

가비지 컬렉션 장점

C 나 C++ 언어에서 프로그래머가 객체에 할당된 메모리 영역이 불필요하게 되면 프로그래머가 책임을 지고 해당 오브젝트가 사용한 메모리를 명시적으로 해제해야 한다. 만약 개발자가 이를 잊고 해제하지 않은 경우 메모리 누수와 잘못된 메모리 해제로 인하여 애플리케이션이 크래쉬 되어 장애의 원인이 된다.

이런 메모리 문제로 인한 버그는 잘못된 프로그램 코드와 실제 문제가 발생한 부분이 달라서 재현과 디버깅이 매우 어렵다. 장시간 동안 안정되게 운영해야 하는 웹 애플리케이션 서버에서는 큰 문제가 될 수 있다.

자바 런타임 환경에서는 메모리 공간을 확보하기 위해 프로그래머가 명시적으로 메모리 해제에 대해 신경 쓸 필요가 없다. 메모리 해제에 대한 개발자 실수가 없어서 신뢰성, 안전성이 높은 애플리케이션 개발을 할 수 있다. 물론 자바에서도 잘못된 코딩으로 메모리 누수가 발생하는 경우가 있다.

가비지 컬렉션으로 메모리 관리의 장점이 있지만 반면 이 때문에 애플리케이션의 성능이 감소하고, 응답 시간이 지연되는 등 성능 병목 현상의 원인이 될 수 있다.

특히 메모리가 크고, 많은 CPU를 사용하는 대규모 시스템에서는 가비지 컬렉션으로 인한 성능 저하도 비례하여 증가하게 된다. 가비지 컬렉션이 큰 메모리 영역을 여러 CPU로 효율적으로 처리하는 것이 과제이다. 이것을 해결하기 위해서 가비지 컬렉션 작업을 여러 스레드로 동시에 진행하는 병렬 가비지 컬렉션과 애플리케이션 스레드와 가비지 컬렉션 작업을 동시에는 하는 Concurrent 가비지 컬렉션 방법 등이 있다.

자바 힙 메모리 이해

자바 가상 머신에서 힙 영역은 신세대(Young Generation : New 영역)와 구세대 세대(Tenured Generation 혹은 Old Generation : Old 영역) 그리고 영구 세대(Permanent Generation 영역)로 세대(Generation)라는 개념으로 나누고 각각 다른 알고리즘으로 가비지 컬렉션을 수행한다.

자바에서 새로운 오브젝트를 생성하면 New 영역에 저장되고, 자주 참조되는 오래된 오브젝트들은 Old 영역에 저장된다. Permanent 영역에는 클래스나 메소드 등의 정보가 저장된다.

가비지 컬렉션은 신세대와 구세대 중 어떤 힙 영역을 대상으로 하느냐에 따라 다음 두 가지 종류가 있다. 첫 번째로 ‘Scavenge GC’는 신세대(Young / New Generation) 공간이 부족하면 실행되며, 비교적 자주 발생하고 짧은 시간에 처리가 끝난다. 두 번째로 ‘Full GC’는 Old 영역과 Permanent 영역이 부족하면 실행되며, 비교적 부하가 많은 작업이기 때문에 처리 시간이 오래 걸린다.

그림 4. 자바 메모리 구조

각 메모리영역에 대하여 간략하게 설명한다.

(1) New 영역(Young Generation)

New 영역은 Eden과 Survivor 로 나누어지고, Survivor는 다시 From, To로 나누어져 총 세 개의 영역으로 나뉜다. 프로그램이 실행된 후 새로운 오브젝트는 먼저 Eden 영역에 생성된다. Eden 영역이 새로운 오브젝트들로 가득 차게 되면, Scavenge GC(New / Young 영역 GC)에 의해 사용되지 않은 객체는 삭제되고, 사용 중인 객체는 From 영역과 To 영역으로 이동한다. Scavenge GC는 번갈아 한번은 Eden영역과 From영역에 오브젝트들을 조사하여 사용하지 않은 오브젝트는 삭제하고 사용된 오브젝트는 To 영역으로 복사하고, 다음번에 Eden 영역과 To 영역을 조사하여 사용하지 않은 오브젝트는 삭제하고 사용된 오브젝트는 From 영역으로 복사한다. 이런 방법으로 오브젝트들의 From 영역과 To 영역을 이동하는 횟수를 세어 사용 빈도가 높다고 판단된(MaxTenuringThreshold :기본값은 32회) 오브젝트들을 OLD 영역으로 이동하여 Full GC 전까지 살아남을 수 있다. New 영역이 부족하면 Scavenge GC가 자주 발생한다.

(2) Old 영역(Tenured Generation 혹은 Old Generation)

New 영역에서 살아남은 오브젝트가 저장되는 영역이다. 즉 수명이 긴 오브젝트들이 이 영역에 배치된다. 자바 가상머신은 Old 영역이 가득 차면 Full GC 를 수행한다.

(3) Permanent 영역(Permanent Generation)

클래스나 메서드 정보가 저장되는 영역이다. 많은 클래스를 로드하거나 JSP를 많이 사용하는 경우 Permanent 영역이 부족할 수 있어 보통 크기를 늘려 사용한다. 자바 가상머신은 Permanent 공간이 부족해도 Full GC를 수행한다.

힙 영역과 Permanent 영역을 설정하려면 다음과 같이 Java 옵션을 지정한다. m은 MB이다.

-Xms1024m -Xmx1024m -XX:NewSize=128m -XX:MaxNewSize=128m -XX:PermSize=64m -XX:MaxPermSize=256m

다음은 힙 영역의 가비지 컬렉션 종류별 대상 영역에 대한 그림을 참조한다.

그림 5. 메모리 세대별 가비지 컬렉션

또한 Full GC가 수행되는 동안 다른 작업을 할 수 없어서 시스템은 정지 상태가 되며 이를 ‘Stop The World’라고 한다. Full GC는 시스템 성능에 큰 영향을 주기 때문에 가능한 발생하지 않도록 조정해야 한다.

다음은 자바 실행 옵션 중 메모리와 관련한 주요 옵션들을 정리한 것이다.

항목설명
-Xms : 힙 메모리 초기값 -Xms : 힙 메모리 최대값-Xmx와 -Xms는 각각 Young 영역과 Old(Tenured) 영역을 더 한 Heap 전체의 초기 메모리와 최대 메모리 크기이다. 초기값과 최대값이 다를 경우 힙 메모리의 변화에 따라 조정하는 작업으로 성능이 느려질 수 있다. 하드웨어 자원이 충분한 경우 성능 감소를 막기 위해서 초기값과 최대값을 같게 설정한다.
-XX:NewSize : New 영역의 초기값 -XX:MaxNewSize : New 영역의 최대값NewSize와 MaxNewSize는 각각 Young 영역의 초기값과 최대값이다. 초기값과 최대값이 다를 경우 힙 메모리의 변화에 따라 조정하는 작업으로 성능이 느려질 수 있다. Young 영역이 작으면 Scavenge GC가 빈번하게 발생되며 동시에 비교적 수명이 짧은 오브젝트가 Tenured 영역으로 이동하여 Full GC의 빈도도 높아진다. Young 영역을 크게 잡으면 Scavenge GC 비용이 증가한다.
-XX:PermSize Permanent영역 초기값 -XX:MaxPermSize Permanent 영역 최대값Permanent 영역은 자바 오브젝트의 클래스 정보가 저장되는 영역이다. 클래스 수는 애플리케이션 로직과 구조에 따라 결정되며, 클래스 수가 늘어나면 Permanent 영역의 크기가 증가한다. 클래스는 동적으로 JVM에서 로드되기 때문에, 애플리케이션의 크기에 따라 Permanent 사이즈도 증가한다. Permanent 영역이 부족하면 Full GC도 자주 일어난다. JBoss EAP 6에서 배포 스캐너를 적용한 상태에서 JBoss 실행 중에 재배포를 하는 경우, 클래스를 교체해야 해서 Permanent 영역을 충분히 크게 확보해야 한다.
-Xss개별 스레드의 스택 사이즈를 지정한다. 스택을 어느 정도 사용하는지는 애플리케이션 로직에 따라 다르다. 예를 들어 스레드 스택 사이즈가 1M 이고, 스레드가 최대 100개 사용된다면 최대 100m의 메모리를 사용하게 된다. 많은 수의 스레드를 사용하는 애플리케이션은 스레드 스택 메모리도 많이 사용한다. 스택 저장영역이 부족하면 OutOfMemory 또는 프로세스 장애가 발생한다.

표 3. 주요 자바 메모리 옵션

자바 애플리케이션에서 ‘Stop-The-World’

Java 플랫폼은 가비지 컬렉션으로 인한 치명적인 문제를 내장하고 있다. 자바 애플리케이션은 Full GC를 수행하는 동안 모든 애플리케이션 스레드가 중지해야 한다. 아래의 그림은 Parallel GC 를 적용하였을 때 Stop The World가 발생하는 모습이다.

그림 6. Parallel GC의 Stop the world

Stop-The-World는 가비지 컬렉터가 애플리케이션에서 동작중인 모든 스레드를 멈춘 후 가비지 컬렉션을 수행하는 방식이다. 이 때문에 정지 시간이 실제로 애플리케이션에 얼마나 영향을 주었는지를 분석하기 위해서는 GC 로그를 확인해야 한다.

자바에서 가비지 컬렉터로 사용할 수 있는 방식은 다음 표와 같다. 해당 옵션에 대한 자세한 정보는 Oracle의 Java 가상 머신에 대한 페이지를 확인하라.

구분옵션
Serial GC-XX:+UseSerialGC
Parallel GC-XX:+UseParallelGC -XX:ParallelGCThreads=value
Parallel Compacting GC-XX:+UseParallelOldGC
CMS GC-XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=value -XX:+UseCMSInitiatingOccupancyOnly
G1-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC

표 . 자바 GC 옵션

자바 가상 머신 성능 튜닝

시스템 내부에서는 수많은 구성 요소들이 함께 동작하고 서로 복잡하게 의존하고 있다. 따라서 어떤 부분이 병목인지 찾는 것은 매우 어려운 일이다.

자바 가상 머신의 성능 튜닝은 웹 시스템 전체의 성능 튜닝 중 일부이며, 제일 마지막 단계의 병목을 찾아내는 부분이라 할 수 있다. 자바 가상 머신의 튜닝은OS, 웹 서버, 데이터베이스 서버, 웹 애플리케이션 서버 등의 튜닝에 비하여 노력 대비 얻는 효과가 높지 않지만, 안정된 서비스를 제공해야 하는 측면에서는 매우 중요하며 난이도와 노력이 많이 필요한 부분이다.

특히 자바 가상 머신의 성능 튜닝은 필요한 모든 데이터를 모니터링하고 수집하고 분석해서 추측하는 과정을 반복해서 수행해야 하는 과정이다. 성능 문제를 신속하게 해결하지 않으면 안 되는 긴급한 상황이 많기 때문에 모니터링과 분석 단계에 시간을 주지 않고, 추측이나 가설만 가지고 대응해 버리는 경우가 많다. 그러나 이러한 임기응변적인 대응은 상황을 더욱 악화시키게 된다. 시급하고 중요할수록 일정한 절차에 따라 성능 튜닝을 실시하는 것이 바람직하다.

앞부분에서 자바 성능의 가장 중요한 가비지 컬렉션에 대한 동작 방법과 주요 옵션들을 다루었다. 가비지 컬렉션 이외의 자바성능 튜닝에 관련된 상세한 내용은 자바 책들에서 다루고 있어 생략하도록 한다.

GC 로그, 자바 프로파일링, 자바 스레드 모니터링, 힙 메모리 등의 도구들은 각각의 용도에 맞게 오픈소스부터 상용 제품까지 다양한 제품이 있으니, 시스템 운영 상황에 맞는 적절한 분석도구들을 사용하여 측정 결과를 확인하고 성능 개선과 안정성 확보를 위한 작업을 지속해서 수행한다.