개발자

“자바 앱을 더 빠르게” 성능 튜닝의 모든 것

Rafael del Nero | InfoWorld 2023.10.05
자바가상머신(JVM) 최적화를 통해 JVM 내에서 실행되는 자바 애플리케이션의 성능과 효율성을 높일 수 있다. 실행 속도를 개선하고 메모리 사용량을 줄이고 리소스 사용을 최적화하는 다양한 기법을 사용한다.
 
ⓒ Getty Image Bank​

JVM 최적화 작업에는 힙 크기, 가비지 수집기 매개변수와 같은 JVM의 메모리 할당 설정 변경이 포함된다. 최종 목표는 효율적인 메모리 사용을 보장하고 불필요한 객체 생성과 메모리 누수를 최소화하는 것이다. JVM의 JIT(Just-In-Time) 컴파일러를 최적화하는 것도 중요하다. 코드 패턴을 분석하고 핫스팟을 파악하고 인라이닝 및 루프 풀기와 같은 최적화를 통해 JIT 컴파일러는 자주 실행되는 바이트코드를 네이티브 기계 코드로 동적으로 해석할 수 있게 된다. 그 결과 실행 속도가 더 빨라진다. 

JVM 최적화의 또 다른 중요한 측면은 스레드 관리다. 효율적인 스레드 활용은 동시성 자바 애플리케이션에 매우 중요하다. 스레드 사용 최적화 방법에는 경합 최소화, 컨텍스트 전환 감소, 스레드 풀링 및 동기화 메커니즘의 효과적인 채용 등이 있다. 

마지막으로, 힙 크기 및 스레드 스택 크기와 같은 JVM 매개변수를 잘 조정하면 JVM의 동작을 최적화해 더 좋은 성능을 얻을 수 있다. 프로파일링과 분석 툴은 성능 병목 지점, 핫스팟, 메모리 문제를 파악하는 데 유용하고, 개발자가 정보에 근거한 최적화 작업을 할 때 도움이 된다. JVM 최적화는 이런 여러 기법을 결합하고 지속적으로 애플리케이션을 벤치마킹, 테스트해 자바 애플리케이션의 성능과 응답성을 강화한다. 하나씩 자세히 살펴보자.
 

JIT 컴파일러 최적화

JVM의 JIT 컴파일러 최적화는 자바 성능 최적화의 중요한 부분이다. JIT 컴파일러는 자주 실행되는 바이트코드를 네이티브 기계 코드로 동적으로 변환해 자바 애플리케이션의 성능을 개선한다. JIT 컴파일러는 런타임에서 자바 메서드의 바이트코드를 분석하고, 코드에서 자주 실행되는 부분을 의미하는 핫스팟을 파악한다. 이렇게 JIT 컴파일러가 핫스팟을 식별하면 다양한 최적화 기법을 적용해 해당 코드 부분에 대해 고도로 최적화된 네이티브 기계 코드를 생성한다. 널리 쓰이는 JIT 최적화 기법은 다음과 같다.
 
  • 인라이닝(Inlining) : JIT 컴파일러는 메서드 호출의 인라이닝을 결정한다. 즉, 메서드 호출을 실제 메서드 코드로 대체하는 것이다. 인라이닝은 별도의 메서드 호출 필요성을 제거해 메서드 호출 오버헤드를 줄이고 실행 속도를 개선한다. 
  • 루프 풀기(Loop unrolling) : JIT 컴파일러는 루프 반복을 복제하고 루프 제어 명령의 수를 줄여 루프를 풀 수 있다. 이 기법은 특히 런타임에서 루프 반복을 확인할 수 있는 경우 루프 오버헤드를 줄이고 성능을 개선한다. 
  • 데드 코드 제거(Eliminate dead code) : JIT 컴파일러는 데드 코드, 즉 실행되지 않는 코드 부분을 식별해 제거할 수 있다. 데드 코드를 제거하면 불필요한 계산이 줄어들고 전체적인 실행 속도가 개선된다. 
  • 상수 접기(Constant folding) : JIT 컴파일러는 상수 식을 평가해 컴파일 시 이를 계산된 값으로 대체한다. 상수 접기는 런타임 계산의 필요성을 줄여주며 특히 자주 사용되는 상수와 관련해 성능을 개선한다.
  • 메서드 특수화(Method specialization) : JIT 컴파일러는 사용 패턴을 기반으로 메서드의 특수 버전을 생성한다. 특수 버전은 특정 인수 유형 또는 조건에 따라 최적화돼 성능을 개선한다.

이 5가지는 JIT 최적화의 몇 가지 예에 불과하다. JIT 컴파일러는 애플리케이션의 실행 프로필을 지속적으로 분석하고 동적으로 최적화를 적용해 성능을 개선한다. 개발자는 JIT 컴파일러를 최적화함으로써 JVM에서 실행되는 자바 애플리케이션에서 상당한 성능 향상 효과를 얻을 수 있다. 
 

자바 가비지 수집기 최적화

자바 가비지 수집기(Garbage Collector, GC) 최적화는 JVM 최적화에서 필수적인 부분으로, 메모리 관리를 개선하고 가비지 수집이 자바 애플리케이션 성능에 미치는 영향을 최소화하는 데 초점을 둔다. 가비지 수집기는 사용되지 않는 객체가 점유한 메모리를 회수하는데, 개발자가 가비지 수집을 최적화하는 방법은 다음과 같다.
 
  • 적절한 가비지 수집기 선택 : JVM은 다양한 가비지 수집 알고리즘을 구현하는 다양한 가비지 수집기를 제공한다. 직렬, 병렬, 동시 마크 스윕(CMS) 가비지 수집기, 그리고 더 새로운 변형으로는 G1(가비지-퍼스트), ZGC(Z 가비지 수집기)가 있다. 각기 장단점이 있다. 메모리 사용 패턴, 응답성 요구사항과 같은 애플리케이션의 특징을 파악하면 가장 효과적인 가비지 수집기를 선택하는 데 도움이 된다. 
  • GC 매개변수 조정 : JVM은 가비지 수집기의 동작을 최적화하기 위해 조정할 수 있는 구성 매개변수를 지원한다. 매개변수에는 힙 크기, 가비지 수집을 트리거하는 임곗값, 세대 메모리 관리 비율 등이다. JVM 매개변수 조정은 메모리 사용률과 가비지 수집 오버헤드 간의 균형을 맞추는 데 도움이 된다. 
  • 세대 메모리 관리 : JVM의 가비지 수집기는 대부분 힙을 새로운 세대와 오래된 세대로 나누는 세대(generational) 가비지 수집기다. 세대 메모리 관리를 최적화하는 방법은 각 세대의 크기 조정, 세대 간 비율 설정, 각 세대의 가비지 수집 사이클 빈도 및 전략 최적화 등이 있다. 효율적인 객체 할당과 생명 주기가 짧은 객체 수집에 도움이 된다.
  • 객체 생성 및 보존 최소화 : 과도한 객체 생성과 불필요한 객체 보존은 메모리 사용량을 증가시켜 가비지 수집 빈도가 높아지는 원인이 된다. 이때 필요한 것이 객체 생성 최적화다. 객체 재사용, 객체 풀링 기법 도입, 불필요한 할당 최소화 등의 작업이다. 객체 보존을 줄이기 위해서는 의도하지 않게 유지되는 참조되지 않는 객체와 같은 메모리 누수를 찾아 제거해야 한다. 
  • 동시 및 병렬 수집 : CMS, G1과 같은 일부 가비지 수집기는 동시 및 병렬 가비지 수집을 지원한다. 동시 가비지 수집을 활성화하면 애플리케이션이 가비지 수집과 동시에 실행되므로 응답성이 개선된다. 병렬 가비지 수집은 여러 스레드를 활용해 가비지 수집을 수행해 대용량 힙 처리 속도를 높인다. 
  • GC 로깅 및 분석 : 가비지 수집 로그와 통계를 모니터링해  분석하면 가비지 수집기의 동작과 성능에 대한 심층적인 정보를 얻을 수 있다. 잠재적인 병목 지점과 장시간의 멈춤 또는 과도한 메모리 사용을 파악하는 데도 도움이 된다. 이 정보를 사용해 가비지 수집 매개변수와 최적화 전략을 세부적으로 조정할 수 있다. 

가비지 수집을 최적화하면 메모리 관리를 개선하고 가비지 수집 오버헤드를 줄이고 애플리케이션 성능을 향상할 수 있다. 단, 이 작업은 애플리케이션의 특징과 요구사항에 따라 많은 부분이 좌우된다는 점에 유의해야 한다. 메모리 사용률과 응답성, 처리량 사이에서 균형을 맞추는 것이 가장 중요하다.
 

스레드 사용 최적화 

JVM의 효율적인 스레드 사용은 동시 자바 애플리케이션에서 최적의 성능과 확장성을 실현하기 위한 핵심 요건이다. JVM에서 효율적으로 스레드를 활용하는 검증된 팁은 다음과 같다.
 
  • 스레드 경합 최소화 : 스레드 경합은 여러 스레드가 공유 리소스를 두고 경쟁할 때 발생하는데 성능 하락의 주요 원인이다. 이런 경합을 피하려면 잠금 필요성을 최소화하고 핵심 섹션의 지속 시간을 줄이는 스레드 안전 데이터 구조 및 동기화 메커니즘을 설계하는 것이 중요하다. 잠금이 없거나 차단이 없는 알고리즘을 사용하면 경합을 완화할 수 있다. 
  • 스레드 풀링 활용 : 일반적으로 작업마다 명시적으로 스레드를 생성하는 대신 스레드 풀링을 사용하는 것이 더 효율적이다. 스레드 풀링은 고정된 수의 스레드를 미리 생성한 후 이를 재사용해 작업을 처리하는 방식이다. 이렇게 하면 스레드 생성 및 제거에 따르는 오버헤드를 방지하고 리소스 소비에 대한 통제력을 강화할 수 있다. 자바의 이그제큐터서비스(ExecutorService) 프레임워크가 기본적으로 스레드 풀링 기능을 제공한다. 
  • 과도한 컨텍스트 전환 지양 : 컨텍스트 전환은 스레드에서 다른 스레드로 CPU를 전환하는 과정을 의미한다. 그러니 이 컨텍스트 전환이 과도하면 오버헤드를 유발하고 성능을 저하한다. 가용 CPU 코어에 맞는 적절한 스레드 풀 크기를 사용하면 과도한 컨텍스트 전환을 방지하는 데 도움이 된다. 이렇게 하면 과도한 스레드 생성을 막고 컨텍스트 전환 빈도가 줄어든다. 또한 스레드 선호도(thread-affinity) 기법도 특정 CPU 코어에 스레드를 바인딩하는 방식으로 컨텍스트 전환을 최소화하는 데 도움이 된다.
  • 비동기 및 비차단 I/O 사용 : 비동기 및 비차단 I/O 작업을 활용하면 스레드 사용률과 확장성을 개선할 수 있다. 비동기 I/O는 I/O 작업이 완료되기를 기다리는 동안 스레드를 차단하는 대신 스레드가 다른 작업을 계속 처리할 수 있도록 허용한다. 이로써 하나의 스레드가 여러 I/O 작업을 동시에 처리할 수 있고 결과적으로 리소스 사용률이 개선되고 애플리케이션 처리량이 증가한다. 
  • 로드 밸런싱 : 작업을 여러 개의 작은 작업으로 나눌 수 있다면 워크로드를 여러 스레드 또는 머신에 걸쳐 분산하는 방법으로 효율성을 높일 수 있다. 로드 밸런싱은 각 스레드에 고르게 워크로드를 배분해 리소스 사용률을 최대화하고 유휴 시간을 최소화한다. 
  • 스레드 동기화 및 조율 : 효율적인 스레드 조율 및 동기화는 정확하고 성능이 우수한 동시 프로그래밍에 필수적이다. 저수준의 동기화된 블록 대신 LockCondition 객체와 같은 고수준 동기화 프리미티브를 활용하면 유연성과 스레드 상호작용에 대한 제어를 강화할 수 있다. 또한 스레드 안전 컬렉션 및 동시 데이터 구조는 동기화 요구사항을 간소화하고 성능을 개선한다. 
  • 스레드 안전 및 불변성 객체 : 스레드 안전 클래스를 설계하고 불변성 객체를 활용하면 스레드 동기화 필요성을 줄일 수 있다. 불변성 객체는 잠금 또는 동기화 없이 여러 스레드 간에 안전하게 공유할 수 있으므로 데이터 경쟁 및 경합의 위험을 줄인다.

이런 전략을 적용하면 JVM에서 효율적으로 스레드를 사용해 동시 자바 애플리케이션의 성능을 개선하고 리소스 사용률을 높이고 확장성을 강화할 수 있다. 
 

JVM 매개변수 조정 

JVM 매개변수 미세 조정은 자바 가상 머신에서 실행되는 자바 애플리케이션의 성능과 동작을 최적화하기 위해 필수적이다. 최적화할 수 있는 JVM 매개변수는 다음과 같다. 
 
  • 힙 크기(-Xmx-Xms) : 힙은 자바 객체가 할당 및 관리하는 메모리 영역이다.  -Xmx 매개변수는 최대 힙 크기, -Xms는 초기 힙 크기다. 이 두 매개변수는 애플리케이션의 메모리 요구사항을 기반으로 조정한다. 최대 힙 크기를 너무 낮게 설정하면 빈번한 가비지 수집 및 메모리 부족 오류가 발생할 수 있고, 너무 높게 설정하면 과도한 메모리를 소비한다. 마찬가지로, 초기 힙 크기를 너무 낮게 설정하면 잦은 크기 조정 작업이 일어나 성능이 저하될 수 있다.
  • 가비지 수집기: 앞서 언급했듯이, 애플리케이션을 위한 적절한 가비지 수집기를 선택하는 것만으로 성능을 크게 향상할 수 있다. 예를 들어 동시 마크 스윕(Concurrent Mark Sweep) 수집기는 중지 시간 요구사항이 낮은 애플리케이션에 적합한 반면, 가비지 퍼스트(G1) 수집기는 처리량과 중지 시간 간의 균형을 염두에 두고 설계됐다. 애플리케이션의 동작과 요구사항을 이해하면 가장 효율적인 가비지 수집기를 선택하는 데 도움이 된다.
  • 병렬 처리 및 동시성 : JVM은 애플리케이션의 병렬 처리 및 동시성 수준을 제어하기 위한 매개변수를 제공한다. 예를 들어 XX:ParallelGCThreads 매개변수는 병렬 가비지 수집에 사용되는 스레드 수를 설정한다. 사용 가능한 하드웨어 리소스를 기반으로 이런 매개변수를 조정하면 가비지 수집과 기타 병렬 작업의 효율성을 개선할 수 있다. 
  • JIT 컴파일 : JVM의 JIT(Just-In-Time) 컴파일러는 자주 실행되는 바이트코드를 네이티브 기계 코드로 변환해 성능을 높인다. -XX:CompileThreshold-XX:MaxInlineSize와 같은 JIT 컴파일 매개변수를 미세 조정해 컴파일 동작을 조절할 수 있다. 애플리케이션의 사용 패턴에 따라 적절한 임곗값과 크기를 설정하면 JIT 컴파일 프로세스를 최적화하고 전체적인 실행 속도를 개선할 수 있다. 
  • 스레드 스택 크기 : JVM의 각 스레드에는 메서드 호출과 로컬 변수를 저장하는 스택이 있다. -Xss 매개변수는 스레드 스택 크기를 정의한다. 스레드 스택 크기 조정은 생성 가능한 스레드의 수와 애플리케이션의 메모리 소비에 영향을 준다. 너무 낮게 설정하면 StackOverflowError가 발생할 수 있고 너무 높게 설정하면 생성 가능한 스레드의 수가 제한된다. 
  • I/O 버퍼 크기 : JVM은 I/O 작업에 기본 버퍼 크기를 사용한다. I/O 작업이 많은 애플리케이션의 경우 Dsun.nio.ch.maxUpdateArrayLength 매개변수를 사용해 버퍼 크기를 조정하면 I/O 성능이 개선된다. 
  • 프로파일링 및 모니터링 : 자바 플라이트 리코더(JFR), 자바 미션 컨트롤(JMC) 같은 툴은 JVM 동작 및 성능에 대한 심층적인 정보를 제공한다. 메트릭을 분석하고 데이터를 프로파일링하면 JVM 매개변수 조정으로 효과를 얻을 수 있는 부분을 찾는 데 도움이 된다.

JVM 매개변수 최적화 작업은 신중하게 검토하면서 여러 가지를 테스트해야 한다. 성능 개선 효과를 확인하려면 매개변수를 수정했을 때 어떤 영향이 있는지 벤치마킹하고 측정하는 것이 중요하다. 또한 JVM 구현과 권장 방법이 바뀔 수 있으므로, 매개변수 조정에 대한 최신 JVM 문서와 모범 사례를 지속적으로 확인해야 한다.
 

프로파일링 및 분석 툴 

프로파일링 및 분석 툴은 소프트웨어 애플리케이션의 성능 특징 및 동작을 이해하기 위해 필수적이다. 이런 툴은 CPU 사용량, 메모리 소비, 스레드 활동 및 메서드 수준 실행 시간 등 애플리케이션에 대한 심층적인 정보를 제공한다. 자바 애플리케이션 프로파일링 및 분석에 일반적으로 사용하는 툴은 다음과 같다. 
 
  • 자바 플라이트 리코더(JFR) 및 JDK 미션 컨트롤(JMC) : 자바 플라이트 리코더는 JVM에 내장된 이벤트 기반의 가벼운 프로파일링 프레임워크다. 메서드 프로파일링, 가비지 수집 활동, 스레드 상태 및 잠금 경합 등 상세한 런타임 정보를 수집한다. JDK 미션 컨트롤은 JFR 레코딩을 분석하는 그래픽 툴이다. 기록된 이벤트를 시각적으로 표현하므로 이를 통해 성능 병목 지점, 메모리 누수 등 여러 문제를 찾을 수 있다.
  • 비주얼VM(VisualVM) : 자바 개발 키트(JDK)에 포함된 강력한 프로파일링 및 분석 툴이다. CPU 프로파일링, 메모리 프로파일링, 스레드 분석, JVM 메트릭 모니터링 등 다양한 기능을 제공한다. 비주얼VM은 자바 애플리케이션의 성능을 분석, 최적화하는 사용자 친화적인 인터페이스를 제공하며, 부가기능을 쓸 수 있는 플러그인과 확장도 지원한다.
  • 유어키트 자바 프로파일러(YourKit Java Profiler) : 유어키트는 심층적인 프로파일링 기능을 제공하는 상용 자바 프로파일러다. CPU와 메모리 프로파일링, 스레드 분석 기능을 제공하고 성능 문제, 메모리 누수, 경합 문제를 탐지한다. 풍부한 기능의 UI를 제공하며 다양한 JVM, 애플리케이션 서버, 프레임워크를 지원한다. 엔터프라이즈 환경에서 성능을 최적화하는 데 널리 쓰인다.
  • J프로파일러(JProfiler) : 종합적인 프로파일링 및 분석 기능을 제공하는 또 다른 상용 자바 프로파일러다. CPU 프로파일링, 메모리 프로파일링, 스레드 프로파일링 및 메서드 수준 실행 시간에 대한 세부적인 정보를 제공한다. JVM 내부를 모니터링하고 성능 병목 지점을 분석하고 메모리 사용을 최적화할 때 유용하다. 다양한 IDE 및 애플리케이션 서버와의 통합도 제공한다.
  • 이클립스 MAT(메모리 애널라이저 툴) : 이클립스 MAT는 자바 애플리케이션의 메모리를 분석하는 강력한 오픈소스 툴이다. 메모리 누수를 확인하고 힙 덤프를 분석하고 객체 보존 패턴을 파악하는 데 유용하다. MAT는 메모리 소비를 최적화하고 메모리 관련 문제를 해결하기 위한 누수 탐지, 중복 데이터 탐지, 히스토그램 분석 등 다양한 기능을 제공한다. 
  • perf : 리눅스 시스템을 위한 명령줄 툴인 perf는 저수준 성능 모니터링 및 프로파일링 기능을 제공한다. CPU 성능 카운터, 하드웨어 이벤트 기반 샘플링, 시스템 전체 프로파일링 기능이 대표적이다. CPU 사용, 캐시 사용률, 명령어 수준 프로파일링 등을 분석할 수 있으며, 고급 성능 분석 및 최적화에도 유용하다. 

이런 툴은 고수준 성능 모니터링부터 저수준 코드 프로파일링에 이르기까지 다양한 프로파일링 및 분석 기능을 제공하므로 개발자와 성능 엔지니어가 성능 병목 지점, 메모리 누수, 스레드 동기화 문제 및 기타 성능 관련 문제를 파악하는 데 도움이 된다. 개발자는 이러한 툴을 사용해 자바 애플리케이션 성능 및 리소스 사용률을 최적화할 수 있다.
 

결론 

JVM 최적화는 자바 애플리케이션의 성능과 효율성을 개선하는 데 핵심적인 단계다. 개발자는 힙 크기, 가비지 수집기 알고리즘, 스레드 스택 크기, JIT 컴파일 설정과 같은 매개변수를 조정해 리소스 사용률을 최대화하고 확장성을 개선할 수 있다. 또한 자바 플라이트 리코더, JDK 미션 컨트롤과 같은 모니터링 및 프로파일링 툴은 성능 병목 지점을 파악하고 정보에 근거한 최적화를 실행하는 데 필수적이다. 

단, JVM 최적화에 한 가지 정답이 없다. 최적의 매개변수 값은 특정 애플리케이션과 하드웨어 구성, 성능 목표에 따라 다르다. 가장 효과적인 JVM 최적화 전략을 찾기 위해서는 철저한 테스트와 분석이 필요하다. 신중한 접근과 실험을 반복하며 여기서 소개한 기법을 사용하면 JVM의 성능을 최적화하고 자바 애플리케이션의 모든 잠재력을 끌어낼 수 있을 것이다.
editor@itworld.co.kr
 Tags 자바 Java 튜닝

회사명 : 한국IDG | 제호: ITWorld | 주소 : 서울시 중구 세종대로 23, 4층 우)04512
| 등록번호 : 서울 아00743 등록발행일자 : 2009년 01월 19일

발행인 : 박형미 | 편집인 : 박재곤 | 청소년보호책임자 : 한정규
| 사업자 등록번호 : 214-87-22467 Tel : 02-558-6950

Copyright © 2024 International Data Group. All rights reserved.