1장. Java 성능 최적화 및 테스트 방법론 핵심
성능 엔지니어링은 단순히 코드를 빠르게 만다는 작업을 넘어, 하드웨어, 운영체제(OS), 그리고 가비지 컬렉션(GC)과 컴파일러가 복잡하게 얽힌 JVM의 매커니즘을 총체적으로 이해해야 하는 분야입니다.
1. Java 성능의 본질: 예술과 과학의 조화
Java 성능 최적화는 완전한 성능 스토리(The Complete Performance Story) 를 추적하는 과정이며 단순히 Java 코드에 국한되지 않고 하드웨어 리소스와 OS의 상호작용을 모두 포함합니다.
- 과학적 측면 (The Science): 수치, 측정값, 통계적 분석 데이터를 기반으로 하는 엄밀한 접근 방식이다. 성능 엔지니어는 관찰된 데이터를 바탕으로 병목 지점을 객관적으로 증명해야 한다.
- 예술적 측면 (The Art): 깊은 지식과 경험을 바탕으로 시스템의 복잡한 상호작용 속에서 최적의 지점을 찾아내는 통찰력
- 최적화의 두 영역
- JVM 설정 (Thuning Flags): JIT 컴파일러와 GC 알고리즘 등을 조정하는 영역.
- 플랫폼 모범 사례(Java API 및 설계): 효율적인 알고리즘 선택과 Java API의 적절한 활용을 통해 코드 자체의 효율성을 극대화하는 영역입니다.
2. 효율적인 성능 구현을 위한 4대 설계 원칙
훌륭한 성능은 사후 튜닝이 아닌 설계 단계의 철학에서 결정된다.
- 알고리즘 우선순위: 가장 근본적인 원칙이다. 아무리 튜닝해다 O(n^2) 알고리즘이 O(logN) 알고리즘을 이길 수는 없다.
- 적은 코드의 가치: 1,000번의 미세한 상처(Death by 1,000 cuts) 원칙에 따르면, 무분별한 코드 추가는 컴파일 시간을 늘리고 하드웨어 캐시 효율을 떨어뜨린다. 특히, 이런 미세한 오버헤드가 쌓여 코드 캐시 오버플로와 같은 임계 리소스 임계치(Critical Resource Threshold)를 건드리는 순간 성능은 급격히 무너진다.
- 조기 최적화(Premature Optimization)의 재해석: 도널두 커누스의 격언에 따르면 가독성을 해치는 무의미한 미세 최적화란 뜻이지만 좋은 습관(Best Practice)로서의 최적화는 반드시 필요함
- 일반적인 사례(Common Case) 최적화: 모든 예외 케이스가 아닌, 전체 실행의 90% 이상을 차지하는 가장 빈번한 시나리오에 최적화 역량을 집중해라
3. 성능 테스트의 범주와 전략
- 테스트의 성격에 따라 벤치마크를 분류하고 각 함정을 이해해야 한다.
Microbenchmark: 아주 작은 단위의 성능을 측정- 주의사항: JIT 컴파일러는 결과값이 사용되지 않는 코드를
Dead code Elimination을 통해 아예 실행하지 않을 수 있습니다. 따라서jmh프레임워크와Blackhole을 사용하여 결과 소비를 명시해야 합니다.
- 주의사항: JIT 컴파일러는 결과값이 사용되지 않는 코드를
Macrobenchmark: DB, LDAP등 외부 리소스를 포함한 전체 시스템을 테스트한다. 모듈별 성능의 합이 전체 성능과 일치하지 않는 경우가 많으므로 전체 성능 스토리 확인을 위해 필수적이다.Mesobenchmark: 특정 모듈이나 REST 엔드포인트 단위의 테스트이다. 마이크로보다 실질적이며 자동화된 모듈 테스트에 적합함
4. 핵심 성능 측정 지표: 처리량 배치 응답 시간
- 애플리케이션의 목표에 따라 우선순위 지표를 설정해야 함
| 지표 유형 | 정의 및 핵심 포인 트 | 비고 |
|---|---|---|
| 처리량(Throughput) | 일정 시간 내 완료된 작업량(TPS, RPS). JIT 최적화를 위해 Warm-up기간 이후 측정이 필수 | 서버형 앱에서 핵심 |
| 응답 시간(Response Time) | 요청 후 결과 수신까지의 시간. 평균값의 함정을 피하기 위해 90th%, 99th% 백분위수 분석 필수. | 실시간성 앱에서 핵심 |
| 결과 시간(Elapsed Time) | 배치 작업 등 전체 프로세스가 완료되는 데 걸린 총 시간. | 배치 작업에서 핵심 |
Think Time vs. Cycle Time
- Think Time: 요청 사이에 고정된 시간동안 대기한다. 응답 속도가 빨라지면 전체 처리량이 늘어남
- Cycle Time: 응답 시간과 관계없이 전체 주기를 일정하게 유지한다. 이를 통해 서버에 가해지는 처리량을 고정된 상태로 유지하며 응답 시간을 정밀하게 측정할 수 있다.
5. 통계적 유의성과 결과 분석
성능 테스트의 가변성을 제어하기 위해 통계적 도구를 활용
- 회귀 테스트: 기존 코드(Baseline)와 수정 코드(Specimen)의 비교 과정이다.
- Student’s t-test 및 p-value:
Null Hypothesis (귀무 가설): 두 테스트의 성능이 동일하다는 가정p-value: 이 귀무 가설이 참일 확률이다. p-value가 낮을수록 두 그룹 간의 차이가 유의미하다고 판단한다.
- 통계적 유의성 vs 실질적 중요도: 1%의 차이가 통계적으로 매우 유의미할 수 있지만, 엔지니어는 통계적으로는 덜 유의미해 보여도 10%의 큰 차이를 보이는 지점을 먼저 조사하는 예술적 판단략을 발휘해야 한다.
6. Java 성능 진단 도구 상장
JDK 기본 도구
| 도구 | 주요 용도 및 고유 기능 |
|---|---|
| jcmd | JVM 정보, 플래그 확인, 시스템 프로퍼티 출력 등 다목적 진단 |
| jstack | 스레드 덤프 생성. 교착 상태(Deadlock) 및 실행 흐름 분석 |
| jmap | 힙 덤프 생성 및 메모리 히스토리 분석 |
| jstat | GC 활동(Eden, Survivor, Old 영역 변화) 및 클래스 로딩 모니터링 |
| jinfo | JVM 플래그 확인 및 일부 플래그(manageable)의 동적 수정 |
고급 분석: 프로파일링 (Profiling)
- Sampling vs . Instrumented: 샘플링은 오버헤드가 적으나 정확도가 낮고, 인스트루먼테이션은 정확하나 오버헤드가 매우 크다.
- Safepoint Bias와 Async Profiler: 기존 샘플리 프로파일러는 스레드가 세이프포인트(Safepoint)에 도달해야만 스택을 추출할 수 있어 특정 코드 구간에서 데이터가 왜곡되는 편(Bias)이 발생한다. Async-profiler는 세이프포인트를 기다리지 않고 스택을 추출하여 이러한 편향을 제거하므로 훨씬 정확한 분석이 가능하다.
2장. An Approach to Performance Testing
성능 공학의 관점에서 Java 성능 테스트는 단순한 수치 측정을 넘어선 과학인 동시에 예술의 영역이다. 특정과 분석에 엄격한 방법론을 적용하는 것이 과학이라면 , 데이터 이면의 시스템 동작을 통찰하고 의사결정을 내리는 것은 예술에 가깝기 때문이다. 만약 과학적 분석이 결여된 채 성능 테스트를 수행한다면, JIT 컴파일러의 웜업(Warm-up)의 효과를 간과하거나 통계적 유의성을 놓치게 되어 실제 운영 환경에서 시스템 붕괴라는 치명적인 결과를 초래할 수 있습니다.
따라서 성능 테스트의 목표는 단순히 코드가 빠른지 묻는 것이 아니라, 비즈니스 요구사항을 안정적으로 충족하며 확장 가능한지를 증명하는 데 두어야 합니다. 이러한 전략적 토대 위에서 먼저 테스트의 대상 범위에 따른 벤치마크의 유형부터 명확히 구분해야 합니다.
1. 벤치마크의 3대 유형: 마이크로, 매크로, 메조 분석
| 벤치마크 유형 | 정의 및 성능 공학적 핵심 제언 |
|---|---|
| Microbenchmark | 정의: 특정 알고리즘이나 스레드 생성 오버헤드 등 아주 작은 단위를 측정한다. Expert Caveat: 작성하기 가장 까다로운 유형이다. JIT 컴파일러는 결과가 사용되지 않는 코드를 삭제(Dead Code Elimination)할 수 있으므로, 이를 방지하기 위해 volatile 키워드 사용이나 JMH(Java Microbenchmark Harness)의 Blackhole 클래스 활용이 필수적이다. GC와 JIT 최적화의 영향을 중립화하지 못하면 데이터는 무의미해진다. |
| Macrobenchmark | 정의: DB, 네트워크, 외부 시스템을 모두 포함한 전체 애플리케이션의 성능을 측정한다. Expert Caveat: 실제 리소스 사용과 모듈 간 상호작용을 확인하는 유일한 방법이다. 특정 모듈의 속도 향상이 전체 시스템의 병목(예: DB)에 가로막혀 효과가 0이 되는 “전체론적 관점”의 분석을 가능케 한다. |
| Mesobenchmark | 정의: 특정 모듈이나 REST API 호출 등 중간 단계의 기능을 측정한다. Expert Caveat: 마이크로보다 현실적이고 매크로보다 통제가 쉽지만, 보안(Auth)이나 세션 관리 같은 필수 오버헤드를 누락하기 쉽다. 실제 애플리케이션 환경보다 결과가 과도하게 좋게 나올 수 있음을 항상 경계해야 한다. |
| 테스트 단위를 설정한 후에는 무엇을 어떻게 측정할 것인지 결정하는 성능 지표의 설계 단계로 나아가야 합니다. |
2. 핵심 성능 측정 지표: 처리량, 배치, 응답 시간
성능 측정은 우리가 무엇을 성공으로 정의하느냐에 따라 달라진다. 주요 지표인 처리량과 응답 시간은 서로 밀접하게 연관되어 있다.
- Throughput (TPS, RPS): 일정 시간 내에 완료된 작업량(Transactions/Requests Per Second)이다. 클라이언트가 대기 시간 없이 요청을 보낼 때 시스템이 견딜 수 있는 최대 부하 용량을 의미한다.
- Response Time (응답 시간): 요청 후 응답까지의 시간
- Average vs Percentile: 평균은 이상치에 의해 왜곡되는 거짓말하는 지표가 될 수 있다. 따라서 90th%, 99th% 백분위 지표를 반드시 병행 확인해야 한다.
- 이상치(Outlier)와 GC의 상관관계: 대부분 1초 이내인 응답 중 하나가 100초를 기록한다면 이는 Java의 GC로 인한
Stop-the-World일시 정지가 원인일 확률이 매우 높다. 이러한 이상치는 사용자 경험을 파괴하는 주범이다.
- Warm-up & Think Time:
- Warm-up: Java는 실행될수록 최적화되는 특성이 있다. JIT 컴파일러가 최적의 기계어 코드를 생성할 때까지 충분한 예열 시간을 거친 후 측정하는 것이 필수다.
- Think Time: 실제 사용자의 행동을 모사하기 위해 요청 사이에 두는 휴식 시간. 이를 도입해야 비로소 응답 시간 중심의 현실적인 부하 테스트가 가능해진다.
측정된 지표를 확보했다면, 데이터에 내재된 무작위 변동성을 통계적으로 해석하는 안목이 요구된다.
3. 데이터의 변동성과 통계적 유의성 분석
성능 테스트 결과는 OS 스케쥴링, 네트워크 혼잡도 등으로 인해 매번 다르게 나타난다. 이를 단순히 평균으로 퉁치는 것이 아니라 통계적으로 접근해야 한다.
- 귀무 가설(Null Hypothesis):
두 테스트(Baseline vs Specimen)간의 성능 차이가 없다는 가정을 세우는 것부터 시작한다. - Student’s t-test & Alpha 값:
- t-test를 통해 계산된
p-value가 사전에 설정한 알파값 보다 작을 때, 우리는 통계적으로 유의미한 성능 차이가 있다고 판단한다.
- t-test를 통해 계산된
- 통계적 유의성(Significance) vs 중요도(Importance):
- 이 지점이 성능 공학의 예술이 발휘되는 곳, 1%성능 향상이 99%확률로 입증된 사례와 10%의 성능 향상이 80% 확률로 관찰된 사례가 있다면, 수치적 확실성보다 개선 잠재력이 큰 10% 사례를 먼저 조사하는 것이 전략적으로 더 중요하다. 데이터 분석법을 숙지했다면, 마지막으로 효율적인 테스트 주기를 위한 실전 원칙을 정립해야 한다.
4. 실전 성능 테스트의 4대 원칙
성능 테스트는 개발의 마지막 단계에 수행되는 통과의례가 아니라, 전 과정에 녹아있어야 하는 핵심 전략이다. The Early, Test Often 철학을 실현하기 위한 원칙은 다음과 같다.
- 전 과정 자동화: 반복 실행은 물론, 결과 데이터 수집과 t-test분석까지 자동화하여 인적 주관이 개입할 여지를 차단해야 한다.
- 전수 측정(Measure Everything): 단순히 응답 시간만 보지 말고 CPU, 디스크 I/O, 네트워크 사용량을 모두 기록해야 한다. 특히 전문가 도구인 JFR(Java Flight Recorder)과 DB진단을 위해 AWR(Automatic Workload Repository) 보고서를 활용하여 가시성을 전방위로 확보해야 한다.
- 대상 시스템 (Target System) 실행: 단일 코어 노트북에서의 테스트는 72코어 서버에서 발생할 스레드 동기화 병목을 결코 발견할 수 없다. 반드시 운영 환경과 유사한 하드웨어에서 최종 검증을 수행해야 한다.
- 회귀 테스트: 코드 체크인 프로세스에 성능 테스트를 결합하여 성능 저하를 조기에 발견해야 한다. 아키텍처 변경이 필요한 치명적 결함은 개발 초기에 잡아야 수정 비용을 최소화할 수 있다.
3. 자바 성능 도구함 (A Java Performance Toolbox)
성능 최적화는 단순히 코드를 수정하는 행위가 이니다. 엄밀한 과학적 방법론에 기반하여 가설을 세우고, 데이터를 통해 이를 증명하는 정교한 공학적 절차이다. 핵심 목표는 자바 성능 분석의 성패를 좌우하는 가시성(Visibility)의 개념을 정립하고, 이를 확보하기 위한 운영체제와 JVM 내부의 필수 도구들을 볼 것
1. 성능 분석의 핵심 철학
- 성능 분석의 대원칙은 가기성이 없으면 튜닝도 없다
- 애플리케이션의 내부 동작을 객체화하여 관찰할 수 없다면, 모든 최적화 시도는 데이터에 기반하지 않은 단순한 추측일 뿐이다.
- 도구의 선택도 중요함
- jvisualvm과 같은 중량급 GUI 도구를 운영환경에서 실행하는 것 그 자체로 시스템 자원을 소모하여 측정 대상의 상태를 왜곡하는
하이젠 버그(Heisenbug)효과를 유발할 수 있다.
- jvisualvm과 같은 중량급 GUI 도구를 운영환경에서 실행하는 것 그 자체로 시스템 자원을 소모하여 측정 대상의 상태를 왜곡하는
- 모든 심층 분석읠 출발점은 JVM이 기생하고 있는 하부 구조, 운영체제의 지표를 정확하게 해석해야 함
2. 운영체제(OS) 모니터링
- JVM 튜닝 전에 OS 레벨의 병목을 먼저 확인해야 한다.
- JVM은 운영체제제 입장에서 자원을 할당받아 실행하는 프로세스에 불과함
vmstat,iostat,nicstat,typeperf같은 명령어는 가시성 확보를 위한 가장 기초적인 방법
- CPU 사용률:
- CPU 사용률은 사용자 시간(User Time)과 시스템 시간(System/Privileged time)으로 구분
- 사용자 시간: 애플리케이션 코드를 실행하는 시간
- 시스템 시간: I/O 처리나 네트워크 전송 등 커널 자원을 호출하는데 사용하는 시간
- 목표: CPU 사용률을 낮추는 것이 아니라 짧은 시간 동안 높게 유지하여 작업을 빨리 끝내는 것이이다.
- 낮은 CPU 사용률: 애플리케이션이 블로킹(Blocking) 되어 있거나 (DB 대기, 락 대기 등), 할 일이 없음을 의미한다.
- User Time vs System Time: User Time이 높아야 한다. System Time(커널)이 높다면 비효율적인 I/O 등이 원인일 수 있다.
- 성능의 역설
- 낮은 CPU 사용률이 최적화의 증거라고 오해하지만 CPU 사용률을 100%에 가깝게 유지하며 단위 시간당 처리량을 극대화하는 것이 성능 튜닝의 지향점임
- CPU 사용률은 사용자 시간(User Time)과 시스템 시간(System/Privileged time)으로 구분
CPU 실행 큐와 부하 분석
- 단순 CPU 사용률보다 시스템의 포화 상태를 더 정확하게 보여주는 것은 실행 큐(Run Queue)임.
- 이는 CPU를 할당받기 위해 대기 중인 스레드의 수치임
| 시스템 | 지표 이름 | 해석 방식 (학술적 차이) |
|---|---|---|
| Unix/Linux | Run Queue (vmstat의 r) | 실행 중인 스레드와 실행 가능(Runnable)한 스레드의 전체 합계. |
| Windows | Processor Queue Length | 현재 실행 중인 스레드를 제외하고 대기 중인 스레드의 수. |
- 실행 큐의 길이가 가용 CPU 코어 수를 지속적으로 초과한다면 시스템이 결정론적인 처리를 보장할 수 없는 과부하 상태임을 타나냄
디스크 I/O 스와핑의 치명적 위험
- 디스크 I/O 병목은 비효율적인 버퍼링으로 인해 전송량 대비 시스템 시간이 과도하게 발생하는 경우와, 물리적 한계치에 도달한 과도한 처리량으로 나타남
- JVM에서 GC가 힙을 스캔할 때, 해당 메모리 페이지가 디스크로 스왑되어 있다면 OS는 이를 다시 메모리로 읽어 들이는 페이징 인을 강제함. 이 과정에서 발생하는 디스크 I/O 대기 시간으로 인해 수 밀리초면 끝날 GC 중지시간이 수 초에서 수 분까지 늘어나는 GC-SWAP 피드백 루프가 발생하여 시스템 성능이 파괴된다.
- 스와핑(Swapping): 물리 메모리가 부족해 디스크를 메모리처럼 쓰는 현상. 성능 저하의 주 원인임
네트워크 사용량 및 대역폭 계산
- 네트워크 대역폭 포화 상태를 확인해야 한다 (일반적으로 이더넷은 40% 이상 시 포화 가능성)
자바 전용 모니터링 및 관리 도구 (Java Monitoring Tools)
- 커맨드라인 진단 도구 (Command-line Diagnostic)
jcmd: 실행 중인 JVM에 명령을 전달하여 정보를 통합 출력하는 전천후 도구jinfo: JVM 시스템 속성 및 플래그를 확인하고 동적 변경을 지원jstack: 스레드 덤프를 생성하여 락 경합 및 교착 상태 분석.
- 실시간 모니터링 도구 (Real-time Monitoring)
jstat: GC 활동 및 클래스 로딩 통계를 시계열로 출력jconsole: JMX 기반으로 메모리, 스레드, CPU를 시각화하는 모니터링 GUI
- 사후/중량 분석 도구 (Post-mortem/Heavyweight):
jmap: 힙 덤프를 생성하거나 메모리 세부 통계를 추출jvisualvm: 프로파일링, 힙 덤프 분석 등을 지원하는 통합 분석 환경
JVM 튜닝 플래그
- JVM 설정을 파악할 때
-XX:+PrintFlagsFinal옵션은 필수- 에르고노믹스(Ergonomics)에 의해 결정된 최종 플래그 값을 출력
- 출력 내용 중
manageable로 표시된 플래그는jinfo를 통해 런타임에 동적으로 수정 가능하여, 서비스 중단 없는 실헙적 튜닝을 지원
- 단 JVM 플래그는 729개 정도 있으며 대부분 플래그는 특정 진단이나 실험 목적으로 설계되었기에 명확한 프로파일링 데이터 없이 수백 개의 플래그를 변경하는 것은 공학이 아닌 부두교적 주술과 같음
프로파일링 도구의 원리와 함청
프로파일러는 코드의 실행 경로와 자원 점유 지점을 특정하는 가장 강력한 무기지만 모든 측정은 애플리케이션에 오버헤드를 유발하며 데이터의 왜곡이 발생할 수 있음을 인지해야 한다.
샘플링(Sampling) Vs 계측(Instrumented) 방식
- 샘플링: 주기적으로 스레드 스택을 조사하는 확률론적 방식. 오버헤드가 적어 운영 환경에 적합하나 샘플링 간격 사이에 발생한 짧은 이벤트는 누락될 수 있음
- 계측: 모든 메서드 진입/출입 지점에 코드를 삽입하는 결정론적 방식. 정확도는 높으나 오버헤드가 심하여 실제 성능 특성을 완전히 왜곡할 위험이 크다.
세이프포인트 편향(safepoint Bias)
전통적인 샘플링 프로파일러의 가장 큰 결함은 스레드 스택을 추출할 때 JVM이 해당 스레드를 세이프포인트에 도달하게끔 강제해야 한다는 점이다. 소스 컨텍스트에 따르면 스레드는 다음 5가지 상황에서 자동으로 세이프포인트에 놓인다.
- 동기화 락(Synchronized lock)에 걸려 대기 중일 때
- I/O 작업 대기로 블록되었을 때
- 모니터(Monitor) 대기 중일 때
- 파킹(Parked) 상태일 때
- JNI 코드를 실행 중일 때 (단, GC 락 기능을 수행하지 않을 떄) 이로 인해 프로파일러는 세이프포인트 체크가 없는 긴 연산 루프를 수행 중인 메서드를 실제보다 과소평가하거나 아예 누락시키는 세이프포인트 편향을 보일 수 있다.
비동기 프로파일러(Async Profile)
- 현대적인 프로파일러는
AsyncGetCallTrace인터페이스를 사용하여 세이프포인트 편향을 극복 SIGPROF와 같은 신호를 사용하여 스레드가 세이프포인트에 도달하기를 기다리지 않고 임의의 지점에서 스택을 추출하여 통계적으로 유의미한 성능 데이터를 제공
4장 JIT 컴파일러의 동작 원리와 최적화
JIT 컴파일러 개요 및 HotSpot 컴파일 매커니즘
Java의 핵심 철학인 Write One, Run Anywhere는 바이트코드라는 중간 단계 언어를 통해 실현됩니다. 하지만 인터프리터가 바이트코드를 한 줄씩 기계어로 해석하여 실행하는 방식은 C++와 같은 네이티브 언어에 비해 성능상 안좋을 수 밖에 없습니다. 이를 극복하고 Java가 C++와 대등한 성능을 경재할 수 있게 만든 것이 JIT(Just-In-Time) 컴파일러입니다.
JIT 컴파일러의 전략적 핵심은 모든 코드를 컴파일하는 오버헤드를 범하는 대신, 애플리케이션 실행 중 실시간 프로파일링을 통해 가장 자주 실행되는 영역 HotSpot을 찾아 네이티브 기계어로 변환하는 데 있습니다. 이를 통해 인터프리팅 오버헤드를 제거하고 CPU 아키텍처에 특화된 최적화를 수행합니다.
- HotSpot Compilation: 코드 실행 빈도를 추적하여 성능에 가장 큰 영향을 주는 핫스팟을 찾아내는 매커니즘
- Tiered Compilation(단계/계층별 컴파일): 컴파일 속도와 최적화 수준 사이의 균형을 맞추기 위해 도입된 5단계 매커니즘
- Level 0: 인터프리터 단계
- Level 1: C1 컴파일러가 최적화를 수행하되, 프로파일링 정보는 수집하지 않음
- Level 2: C1 컴파일러가 제한된 프로파일링과 함께 컴파일 수행.
- Level 3: C1 컴파일러가 전체 프로파일링과 함께 컴파일 수행
- Level 4: C2 컴파일러(서버 컴파일러)가 수집된 정보를 바탕으로 최고 수준의 최적화 수행
코드 캐시 관리 및 컴파일 모니터링
JIT 컴파일러가 생성한 네이티브 코드는 힙과 독립된 메모리 영역인 코드 캐시에 저장됩니다. 이 공간은 크기가 고정되어 있어 전략적인 관리가 필수적이며, 관리 실패 시 전체 시스템의 처리량이 급락하는 성능 절벽(Performane Cliff) 현상을 초래할 수 있습니다.
- 한 줄 정리
- 컴파일된 코드는 코드 캐시에 저장되며, 이 공간이 부족하면 더 이상의 컴파일이 중단되어 성능이 급격히 저하된다.
Code Cache: JVM이 컴파일된 네이티브 기계어를 보관하는 전용 논-힙(Non-Heap) 메모리 영역.Speculative Optimization(추측 최적화): JIT 프로파일링 데이터를 바탕으로 “이 조건은 항상 참일 것이다” 라고 베팅하여 최적화합니다.Deoptimization(역최적화): 추측 최적화의 가정이 실패할 경우, 컴파일된 코드를 폐기하고 다시 인터프리터 상태로 되돌리는 과정. 이 과정에서 무효화된 코드는Zombie Method가 된다.Sweeper(스위퍼): 코드 캐시를 주기적으로 검사하여 더 이상 사용되지 않는 코드를 제거하거 공간을 확보함Code Cache Full 상황: 캐시가 가득 차면 JVM은 컴파일러를 끕니다. 이후로는 새로 발생하는 핫스팟 코드조차 느린 인터프리터로만 실행되어 성능이 심각하게 저하된다.
고급 컴파일러 최적화: 인라이닝과 탈출 분석
JIT 컴파일러의 진정한 위력은 메서드 인라이닝(Method Inlining)과 탈출 분석(Escape Analysis)라는 고급 최적화 기법입니다.
Method Inlining: 호출 빈도가 높은 작은 메서드의 분문을 호출부에 직접 삽입합니다. 단순히 호출비용을 줄이는 것을 넘어, 컴파일러가 더 넓은 범위의 코드를 한꺼번에 볼 수 있게 함으로써데드 코드 제거(Dead Code Elimination)나상수 폴딩(Constant Folding)같은 추가 최적화의 기폭제 역할을 한다.Escape Analysis: 객체가 메서드 범위 밖으로 탈출 하지 않음을 확인하면, 힙 대신스택 할당(Stack Allocation)을 수행하거나잠금 최적화(Lock Elision)을 적용합니다.Scalar Replacement(스칼라 교체): 탈출 분석의 핵심 기법으로, 객체를 생성하는 대신 객체의 필드들을 개별적인 기본 타입 변수로 쪼개어 레지스터나 스택에 할당하는 방식입니다.
참고
인라이닝은 컴파일러가 메서드 크기와 호출 빈도를 보고 기계적으로 수행합니다. 이는 가용 범위를 확장시켜 최적화의 효율을 극대화하지만, 결과적으로 코드 크기가 커져 코드 캐시 점유율을 높이는 트레이드 오프가 있습니다.
탈출 분석과 스칼라 교체는 GC의 부담을 직접적으로 줄여 애플리케이션의 응답 시간개선에 결정적인 기여를 합니다.
차세대 컴파일러와 사전 컴파일 기술
최근 서버리스나 마이크로서비스 환경에서는 빠른 시작 속도가 무엇보다 중요해졌습니다. 이에 따라 GraalVM과 AOT(Ahead-of-Time) 컴파일 기술이 JIT의 한계를 보완하는 대안으로 부상하고 있습니다.
- GraalVM: Java로 구현된 현대적 JIT 컴파일러입니다. C2보다 복잡한 최적화 알고리즘을 더 유연하게 적용할 수 있도록 설계되었습니다.
- Ahead-of-Time(AOT) Compilation: 프로그램을 실행하기 전 바이트코드를 타켓 플랫폼의 네이티브 기계어로 미리 변환하는 기술입니다.
- Native Image: AOT 기술을 사용하여 생성된 실행 파일로, JVM 없이도 즉각적으로 실행 가능하다.
참고
- 전략적 우위: AOT는 실행 시점에 컴파일할 필요가 없으므로
Warm-up시간이 0에 가깝습니다. 또한 컴파일러 스레드나 코드 캐시가 필요 없이 메모리 점유(Footprint)가 매우 적습니다. - 트레이드오프(Trade-off): AOT는 런타임 프로파일링 정보를 사용할 수 없으므로, 장기적으로 최고 성능(Peak Throughput)은 JIT보다 낮을 수 있습니다. 특정 환경용으로 미리 빌드되므로 Java의 강점인 Run Anywhere를 희생하고 Start Fast를 선택한 케이스입니다.
5장 Garbage Collection
1. GC 개요 및 기본 원리
현재 Java 애플리케이션에서 가비지 컬렉션은 단순히 메모리를 해제하는 자동화 도구를 넘어, 시스템의 저네 성능 스펙트럼을 결정짓는 핵심 변수이다. GC의 존재 이유는 개발자의 메모리 관리 부담을 줄여 생산성을 높이는 데 있지만 성능 관점에서 Stop-the World 현상이 초래하는 응답성(Responsiveness)과 처리량의 상관관계를 정밀하게 분석하는 것이 중요하다.
- 가비지 컬렉션 (GC): 프로그램에서 더 이상 참조되지 않는 객체를 찾아 제거하고 메모리를 회수하는 자동화 프로세스
- Stop the World: GC 실행을 위해 JVM이 애플리케이션의 모든 스레드를 일시 정지시키는 상태
- Mark and Sweep: 사용 중인 객체를 표시하고, 표시되지 않은 객체를 삭제하는 가장 기본적인 알고리즘
내부 동작 원리
성능 튜닝의 관점에서 처리량은 전체 실행 시간 중 GC에 소비되지 않은 시간의 비율을 의미하며, 지연 시간은 단일 Stop the World로 인해 서비스가 중단되는 절대적인 시간을 의미한다. 대부분의 GC 알고리즘은 서비스 가용성 확보를 위해 Stop the world 시간을 최소화하거나 효율적으로 분산하는 데 최적화되어 있습니다. 메모리 단편화가 발생하면 할당 효율이 떨어지므로, 현대적 컬렉터는 이를 해결하기 위한 아박 단계를 포함합니다 .
세대별 가비지 컬렉터 (Generational GC)
GC 설계의 전략적 근간은 약한 세대 가설(Week Generational hypothesis)에 있습니다. 대부분의 객체는 금방 소멸한다는 실증적 관찰 결과로 JVM은 이를 활용해 메모리를 세대별로 분리 관리하여 저네 힙을 매번 검사해야 하는 비용을 획기적으로 줄입니다.
- Young Generation: 새 객체가 할당 되는 곳(Eden, Survivor 0, Survivor 1). 반드시 한 번에 하나의 Survivor 영역만 사용됨
- Old Generation: Young 영역에서 생존하여 승격된 객체들이 상주하는 공간
- Minor GC: Young 영역 청소, 매우 빠름.
- Full GC(Major GC): Old 영역 또는 힙을 청소, Stop The World 시간이 상대적으로 매우 김
내부 동작 및 성능 분석
세대별 관리의 핵심 트레이드오프는 기억 세트(Remembered Sets)의 관리 비용입니다. JVM은 Old영역의 객체가 Young 영역의 객체를 참조하는 경우를 별도로 추적함으로써, Minor GC시 Old 영역 전체를 스캔하지 않고도 빠르게 가비지를 식별할 수 있게 한다. 이런 구조는 수명이 짧은 객체가 많은 현대적 웹 애플리케이션의 처치량 개선에 결정적임
주요 GC 알고리즘 특징 및 선택
시스템의 목표가 최대 처리량인지 최소 지연 시간인지에 따라 알고리즘 선택은 달라진다. JVM은 하드웨어 리소스를 활용하여 병렬 처리와 동시성을 조절함으로써 성능 병목을 해결한다.
- Throughput Collector (Parallel GC): 여러 GC 스레드를 사용해 병렬 처리
- CMS (Concurrent Mark Sweep): 앱과 동시에 GC를 수행해 지연 최소화
- G1(Garbage First): 힙을 지역Region) 단위로 분할 관리
내부 동작
- G1 GC는 전체 힙을 바둑판 형태의 Region으로 나누고, 가비지가 가장 많은 지역부터 우선적으로 청소
- G1 가장 강력한 특징은
일시 정지 목표 설정(Pause Time Goal)로-XX:MaxGCPauseMillis플래그를 통해 개발자가 원하는 최대 정지 시간을 설정할 수 있다.- JVM은 이를 달성하기 위해 청소할 지역의 범위를 동적으로 조절하는 소프트 실시간 특성을 보임
힙 및 세대 크기 조정
힙 크기는 GC 빈도와 지속 시간에 직접적인 영향을 미친다. 무조건 힙을 크게 설정하는 것이 아닌 이유는 힙이 커질수록 한 번의 Stop the world 지속 시간이 기하급수적으로 늘어날 수 있기 때문임
- -Xms/-Xmx: 힙의 시작 크기와 최대 크기
- -Xmn: Young Generation의 크기 설정
- Metaspace: 클래스 메타데이터를 저장하는 Native Memory 영역, 이전의 PermGen을 대체함
내부 동작 및 성능 분석
- 성능 안정성을 위해 -Xms와 -Xmx를 동일하게 설정하는 것이 권장
- Xms, Xmx 두 값이 다르면 JVM은 부하 상황에 따라 힙 크기를 늘리거나 줄이며 이 과정에서 힙 진동 (Heap Oscillation)이 발생하고 추가적인 Full GC가 유발되어 CPU 자원을 낭비하게 됌
- Metaspace는 기본적으로 무제한 확장되지만, OS레벨의 OOM-Killer에 의해 프로세스가 강제 종료되는 것을 방지하기 위해 적절한 상한선을 설정해야 함
6장 가비지 컬렉션 알고리즘
1. Throughput Collector (Parallel GC)
- 개요: CPU cycle의 최우선 순위를 GC 작업 효율에 할당하여 전체 실행 시간 대비 GC Overhead 비율 최소화하여 개별 Pause Time의 길이보다 단위 시간 당 총 작업량(RPS)이 중요한 시스템에서 자원 활용률을 극대화하는 전략임
- 목적: 애플리케이션의 전체 처리량(Throughput) 극대화
- 정의: 여러 GC 스레드를 사용하여 가용 가능한 모든 CPU 코어를 동원해 가비지를 수집하는 알고리즘
- 구성 / 분류: Young Generation(Minor GC)과 Old Generation(Major GC) 수집기 모두 병렬 실행 지원
- 핵심 매커니즘: 수집 시작 시 모든 애플리케이션 스레드를 중단(Stop-the-world) 시킨 후, 병렬 스레드로 수집 작업을 완료하여 중단 시간의 절대 합계를 단축
- 특징 / 수치 / 옵션:
- 기본 활성화 플래그:
-XX:+UseParallelGC - 힙 크기 조정:
- 워크로드에 따라 세대별 크기를 자동 조정하는
Adaptive방식 -Xms,-Xmx등으로 제어하는Static방식
- 워크로드에 따라 세대별 크기를 자동 조정하는
- 기본 활성화 플래그:
- 적용 환경 / 주의점
- 데이터 처리, 과학 연산 등 배치 중심 환경
- 수백 밀리초 이상의
Pause Time이 서비스 수준 협약(SLA)을 위반하지 않는 시스템
- 전환: 처리량 최적화는 배치 작업에 적합하나, 대화형 서비스에서는 지연 시간 예측 가능성을 위해
G1 GC로의 전환이 요구됨
2. G1(Garbage First) Collector
- 목적: 대용량 힙에서 예측 가능한 범위 내의 짧은 중단 시간(Pause Time) 유지
- 정의: 힙을 고정된 크기의 논리적 구역(Region)으로 분할하여 관리하는 가비지 컬렉터
- 구성 / 분류:
- 힙을 1 ~ 32MB 사이의 수천 개 Region으로 분할 (물리적 세대 구분 제거)
- Eden, Survivor, Old 영역 역할을 각 Region에 동적으로 부여
- 거대 객체(Humongous Objects)를 위한 전용 Humongous Region 할당 (물리저긍로는 연속되지 않으나 논리적으로 연속된 공간 활용 가능)
- 핵심 매커니즘:
- 가비지 밀도가 가장 높은 영역(Garbage First)을 우선 순위에 두고 수집하여 효율 증대
- Mixed Colletion: Young 영역 수집 시 Old 영역 중 일부를 병행 수집하여 Full GC 발생 확률 억제
- 특징 / 수치 / 옵션:
- 기본값: JDK 9 이상에서 부터 기본 GC로 설정
- 주요 튜닝 플래그: 목표 Pause Time을 설정하는
-XX:MaxGCPauseMillis=NG1의 핵심 제어 노브
- 적용 환경 / 주의점
- 4GB 이상의 대규모 힙 환경
- 자동화 수준이 높으나 성능 임계점 돌파를 위해 세밀한 모니터링 미 핸드 튜닝이 요구됨
3. CMS(Concurrent Mark Sweep) Collector
- 목적: 애플리케이션 스레드 중단 시간의 최소화 및 응답성 향상
- 정의: 힙 정리 작업의 핵심 단계를 백그라운드 스레드에서 수행하는 저지연 수집기
- 구성 / 분류 (수집 단계)
- Initial Mark (Stop-the-world): Root에서 직접 참조되는 객체 식별 (매우 짧음)
- Concurrent Mark: 애플리케이션 실행과 병행하여 참조 체인 추적 (중단 없음)
- Remark (Stop-the-world) 마킹 단계 중 변경된 참조 정보 최종 확정
- Concurrent Sweep: 가비지 객체를 메모리에서 제거 (중단 없음)
- 핵심 매커니즘: 백그라운드 스레드를 이용해 Old Generation을 지속적으로 스캔하고 가비지를 정리하여 중단 지점 분산
- 특징 / 수치 / 옵션
- Concurrent Mode Failure: Old 영역의 단편화가 심화되거나 수집 속다가 할당 속도를 못 따라갈 경우 발생하며, 발생 시 Parallel GC 방식의 STW Full GC로 강제 전환됨
- 적용 환경 / 주의점
- 지연 시간에 극도로 민감한 레거시 시스템
- 단편화 문제로 인한 장기 가용성 저하 주의 필요
4. Advanced GC Tuning 및 객체 할당
- 목적: 특정 워크로드에 특화된 메모리 레이아웃 설계 및 승격 전략 최적화
- 정의: 객체의 수명과 크기를 기반으로 내부 파라미터를 미세 조정하여 수집 효율을 개선하는 작업
- 구성 / 분류
- Tenuring Threshold: 객체가 Old 영역으로 승격되기까지 생존해야 하는 Minor GC 횟수
- Survivor Space: Young 영역 내에서 객체가 머무는 대기 공간의 비율
- Humongous Objects: 크기의 50%를 초과하는 거대 객체
- 핵심 매커니즘
- 객체 헤더 내 Age 비트 필드를 기록하여
-XX:MaxTenuringThreshold=N값을 기준으로 승격 여부 결정 - 거대 객체는 단편화 방지를 위해 Survivor를 거치지 않고 직접 할당되거나 별도의 Region에서 관리됌
- 객체 헤더 내 Age 비트 필드를 기록하여
- 특징 / 수치 / 옵션:
-XX:+AgressiveHeap: 메모리가 충분한 환경에서 JVM이 스스로 공격적인 최적화 설정 적용-XX:G1HeapRegionSize=N: G1 Region 크기를 조정하여 거대 객체 처리 효율화
- 적용 환경 / 주의점
- 객체 생존 패턴이 비정상적이거나 대형 데이터 처리가 빈번한 환경
5. Experimental GC Algorithms (ZGC, 섀넌도우, Epsilon)
- 개요
- 현대 Java는 지연 시간 제로 및 클라우드 네이티브 대응을 위해 힙 크기와 무관하게 10ms 이하의 지연 시간을 지향하는 실험적 알고리즘을 도입합니다.
- G1이 Mixed Collection 단계에서 STW 방식으로 압착을 수행하는 것과 달리, 이들은 애플리케이션 실행 중 메모리를 압착하는
Concurrent Compaction을 핵심 차별적으로 가집니다.
- 목적: 초저지연 시간 달성 및 특수 목적 운영 환경(서버리스, 테스트 등)지원
- 정의: 차세대 성능 목표 달성을 위해 설계된 최신 가비지 컬렉션 기술
- 구성 / 분류:
- ZGC: 테라바이트급 힙에서도 10ms 이하의 Pause Time 보장을 목표로 설계
- Shenandoah: Red Hat에서 개발한 수집기로, 애플리케이션과 동시에 메모리 이동 및 압착 수행
- Epsilon GC: 수집 로직이 없는 No-op 수집기로, 할당만 수행함
- 핵심 매커니즘
Concurrent Compaction: 애플리케이션 스레드가 실행 중인 상태에서 객체의 위치를 옮기고 참조를 갱신하여 STW 최소화Epsilon: 메모리 수집을 생략함으로써 성능 측정의 베이스라인이나 극단적으로 수명이 짧은 작업에 활용
- 특정 / 수치 / 옵션
- 실험적 단계로 제겅되므로
-XX:+UnlockExperimentalVMOptions등 명시적 활성화 필요
- 실험적 단계로 제겅되므로
- 적용 환경 / 주의점
- 초저지연 요구 시스템 (ZGC/Shenandoah)
- 순수 성능 측정 및 메모리 누수 테스트(Epsilon)
7장 힙 메모리 모범 사례
1. 힙 분석 (Heap Analysis)
- 목적: 힙 메모리 내 객체 분포 파악 및 메모리 누수 지점 식별을 통한 효율적 리소스 관리
- 정의: 힙 내 클래스별 인스
- 구성 / 분류:
- 힙 히스토그램 (빠른 진단용 요약 데이터)
- 힙 덤프 (상세 분석용 바이너리 파일)
- 핵심 매커니즘:
jcmd(최신 JVM 권장 도구)또는jamp을 활용한 런타임 데이터 추출. - 특징 / 수치 / 옵션:
-XX:+HeapDumpOnOutOfMemoryError플래그로 OOM 시점 덤프 자동화- 전체 힙 크기에 상응하는 물리적 디스크 공간 필요.
- 적용 환경 / 주의점: 덤프 생성 시 JVM 일시 정지로 인한 서비스 중단 유의
메모리 사용량 감소 - 객체 크기 최적화 및 초기화
- 목적: 객체 내부 구조 정렬 및 할당 시점 제어를 통한 힙 메모리 점유량 최적화
- 정의: 필드 배치 구조 재설계 및 불필요한 인스턴스 생성을 억제하는 물리적 최적화 기법
- 구성 / 분류
- 객체 크기 감소 (Field Reduction)
- 지연 초기화 (Lazy Initialization)
- 불변 및 표준 객체 (Immutable/Canonical Objects)
- 핵심 매커니즘:
- 8바이트 경계 정렬 및 패딩 삽입
- 필드 순서 재정렬을 통한 오프셋 최소화
- 지연 초기화 적용 시 메모리 베리어(Memory Barrier) 및 인스트럭션 파이프라인 영향 발생
- 특징 / 수치 / 옵션:
- 일반 객체 헤더: 12바이트(Compressed Oops 활성 시) 또는 16바이터
- 8바이트 단위 정렬 경계 준수 및 이에 따른 4~8바이트 패딩 오버헤드
- 주의점: 지연 초기화 시 멀티스레드 동기화 비용 및 복잡도 증가
객체 수명 주기 및 참조 관리 - 객체 재사용 및 참조 유형
- 목적: 가비지 컬렉션(GC) 부하 경감 미 64비트 환경에서의 포인터 주소 처리 효율 극대화.
- 정의: 참조 강도에 따른 GC 수거 우선순위 제어 및 메모리 주소 압축 기술
- 구성 / 분류
- 객체 풀링(Object Reuse)
- SoftReference / WeakReference
- Compressed Oops
- 핵심 매커니즘:
- SoftReference: LRU(Least Recenlty Used) 알고리즘 기반 수거(마지막 액세스 이후 경과 시간 기준)
- WeakReference: 다음 GC 사이클 발생 시 무조건 수거 대상 편입
- Compressed Oops: 2^32 주소 범위를3비트 시프트(Shift)하여 32GB까지 32비트 오프셋으로 표현
- 특징 / 수치 / 옵션
-XX:+UseCompressedOops(최신 JVM 기본 활성)- 32GB 이상 힙 환경에서 Compressed Oops 비활성화 및 포인터 크기 2배 증가
- 적용 환경 / 주의점: 객체 풀링은 단명 객체(Short-lived Objects)에 적용 시 GC 효율 저해 및 관리 복잡성 초래
8장 네이티브 메모리
풋프린트(Footprint) 개요
- 목적: JVM 전체 메모리 점유량 파악을 통한 시스템 예측 가능성 확보
- 정의: 프로세스가 OS로부터 할당받아 실제 점유 중인 물리 메모리 총량
- 구성 / 분류
- 자바 힙
- 코드 캐시
- 메타 스페이스
- 스레드 스택
- 네이티브 라이브러리 할당 (Native Library Allocations)
- 핵심 매커니즘
- OS 레벨에서 프로세스에 할당된 RSS(Resident Set Size) 지표 측정
- 특징 / 수치 / 옵션:
-Xmx: 힙 메모리 최대 크기 제한 설정-Xms: 힙 메모리 시작 크기 설정 및 초기 점유량 조정
- 적용 환경 / 주의점:
- 클라우드 및 컨테이너 환경의 메모리 제한 초과 방지 필수
- 힙 크기 외 논 힙 영역이 차지하는 비중 고려
네이티브 메모리 추적
- 목적: JVM 네이티브 메모리 할당 내역의 상세 분류 미 분석
- 정의: JVM 내부 네이티브 메모리 사용처를 스스로 추적하여 보고하는 기능
- 구성 / 분류:
- summary: 전체 할당 현황 요약 정보 출력
- detail: 개별 할당 지점 정보 포함 출력
- 핵심 매커니즘:
- JVM 할당 함수 호출 시 메모리 사용량 기록 미 태깅 수행
- 특징 / 수치 / 옵션:
-XX:NativeMemoryTracking=summary: 요약 모드 활성화-XX:NativeMemoryTracking=detail: 상세 모드 활성화jcmd <pid> VM.native_memory baseline: 비교를 위한 기준점 설정jcmd <pid> VM.native_memory summary.diff: 기준점 대비 메모리 변화량 확인
- 적용 환경 / 주의점
- detail 모드 사용 시 5 ~ 10%의 성능 오버헤드 발생 유의
- JNI 등 JVM 외부 네이티브 라이브러리 할당분은 추적 불가
공유 라이브러리 네이티브 메모리 (Shared Library Native Memory)
- 목적: 여러 프로세스 간 공유 라이브러리 사용량의 정확한 측정
- 정의: OS 레벨에서 공유되는
.so/.dll파일이 점유하는 메모리 영역 - 구성 / 분류
- Read-Only 영역: 실행 코드 세그먼트
- Read-Write 영역: 전역 변수 및 데이터 세그먼트
- 핵심 매커니즘:
- 메모리 맵 파일(Memory-mapped files)를 통한 프로세스 간 물리 메모리 공유
- 특징 / 수치 / 옵션
pmap: 프로세스 메모리 맵 확인 도구 활용/proc/<pid>/smaps: 공유 메모리에 대한 상세 정보 확인 경로
- 적용 환경 주의점
- 다수 JVM 실행시 RSS 중복 계산으로 인한 측정 오류 주의
- 실제 물리 메모리 점유량 계산 시 공유 영역 고려 필수
운영체제 튜닝: 대형 페이지 (Large Pages)
- 목적: TLB 미스 축소를 통한 메모리 접근 성능 최적화
- 정의: 표준 4KB 페이지보다 큰 단위로 관리되는 메모리 페이지
- 구성 / 분류:
- Linux: HugePages
- Windows: Large Pages
- Slaris: Large Pages
- 핵심 매커니즘
- 가상 주소-물리 주소 변환 매핑 엔트리 개수 축소로 TLB 효율 향상
- 특징 / 수치 / 옵션:
-XX:+UseLargePages: 대형 페이지 사용 활성화 플래그-XX:LargePageSizeInBytes=N: 페이지 크기의 명시적 설정 옵션/proc/meminfo: Linux 내HugePages_Total설정값 확인
- 적용 환경 / 주의점
- OS 차원의 사전 메모리 예약 설정 선행 필요
- 메모리 단편화 발생 시 대형 페이지 할당 실패 가능성 존재
- 대규모 힙 메모리 사용 시 성능 개선 효과 가시적
9장 스레딩 동기화 성능 분석
1. 스레딩과 하드웨어 (Threading and Hardware)
- 목적: 물리적 자원 한계와 자바 스레드 실행 효율의 관계 이해를 통한 최적 성능 도출
- 정의: 하드웨어 코어와 자바 스레드 간의 물리적 및 논리적 매핑 원칙
- 구성 / 분류: 물리 코어(Physical Core), 하이퍼스레드(Hyper-threads/Logical CPU)
- 핵심 매커니즘: 하이퍼스레딩은 코어가 메모리 로드를 위해 대기(Stall) 하는 동안 다른 스레드의 명령을 실행하여 유휴 사이클을 활용함
- 특징
- 하이퍼스레드들은 동일한 실행 코어의 자원을 공유하므로 메모리 로드 대기 시 스톨 현상이 발생함
- 4코어 8스레드 장비는 하이퍼스레딩 기술을 통해 단일 코어 대비 약 5~6배의 처리량을 제공
- 하이퍼스레딩 환경에서 5번째부터 8번째 논리 CPU는 각각 기존 성능의 약 20~40%만을 추가로 개선함
- 주의점: 자바는 논리 CPU를 독립된 프로세서로 인식하므로 실제 물리적 실행 능력을 고려한 부하 설계가 필요
2. 스레드 풀 및 ThreadPoolExecutor (ThreadPools and ThreadPoolExecutors)
- 목적: 스레드 생성 비용 절감 및 한정된 자원 내에서의 테스크 실행 최적화
- 정의:
ThreadPoolExecutor를 이용한 스레드 생명주기 및 작업 큐 관리 - 구성 / 분류
corePoolSize(기본 스레드 수),maximumPoolSize(최대 스레드 수), 대기큐(Work Queue)
- 핵심 매커니즘
- 제출된 태스크는
corePoolSize에 도달할 때까지 매번 새로운 스레드를 생성하여 할당 corePoolSize가 가득 찬 상태에서는 추가 스레드를 생성하지 않고 태스크를 대기 큐에 저장함- 대기 큐거 완전히 가 찬 이후에야 비로소
maximumPoolSize까지 추가 스레드를 생성
- 제출된 태스크는
- 특징 / 수치 / 옵션
- 큰 대기 큐는 CPU 사용량을 낮추는 효과가 있으나 요청 응답 지연을 증가시킴
- 작은 대기 큐와 많은 스레드 설정은 처리량을 극대화할 수 있으나 CPU 자원 고갈 및 컨텍스트 스위칭 비용을 초래함
- 주의점
- 대기 큐가 가득 차지 않으면
maximumPoolSize설정이 무시되는 동작 규칙을 반드시 인지해야 함
- 대기 큐가 가득 차지 않으면
3. 포크/조인 풀 (The ForkJoinPool)
- 목적: 분할 정복 알고리즘 기반의 재귀적 작업을 병렬로 효율 처리하여 CPU 활용도 극대화
- 정의: 작업을 작은 단위로 분할(Fork)하고 결과를 합산(Join)하는 프레임워크
- 구성 / 분류:
ForkJoinPool,ForkJoinTask, 양방향 큐 (Deque) - 핵심 매커니즘
- 작업 훔치기(Work Stealing) 알고리즘을 통해 스레드 간 작업 불균형을 해소함
- 유휴 스레드는 바쁜 스레드 큐의 Back에서 작업을 가져와 자신의 큐에서 작업하는 스레드와의 경합을 최소화 함
- 특징
- 자바 8의 병렬 스트림(Parallel Stream)은 내부적으로 공용
ForkJoinPool을 기본 엔진으로 사용
- 자바 8의 병렬 스트림(Parallel Stream)은 내부적으로 공용
- 주의점: 작업이 독립적으로 분할 가능하고 개별 연산 비용이 충분히 큰 경우에만 자동 병렬화의 이득이 발생
4. 스레드 동기화
- 목적: 다중 스레드 환경에서 데이터 일관성을 유지하고 경합으로 인한 성능 저하 방지
- 정의: 잠금 매커니즘과 메모리 가시성 확보 기술
- 구성 / 분류: 동기화 비용(Synchronization Costs), 위성 현상(False Sharing)
- 핵심 매커니즘
- 락 획득 및 해제 시 하드웨어 레벨의 메모리 배리어와 캐시 동기화 오버헤드가 수반
- 위성 현상은 서로 다른 변수가 동일한 64바이트 캐시 라인 내에 위치할 때 발생
- 특징
- 위성 현상은 한 코어의 쓰기 작업이 다른 코어의 캐시 라인을 무효화하여 불필요한 성능 저하를 유발
@Contended애노테이션을 사용하거나 변수 사이에 패딩을 추가하여 데이터를 서로 다른 캐시 라인으로 정렬함으로써 해결함
- 주의점: 경합이 심한 락은 애플리케이션의 병렬성을 심각하게 제한하므로 락 프리 알고리즘이나 분할 기술 검토가 필요함
5. JVM 스레드 튜닝
- 목적: JVM 레벨 설정을 통한 시스템 환경별 스레드 자원 할당 최적화
- 정의: 메모리 점유 및 락 동작 방식 제어를 위한 시스템 플래그
- 구성 / 분류:
-XX: ThreadStackSize,-XX:+UseBiasedLocking,Thread Prioriteis - 핵심 매커니즘:
XX:ThreadStackSize=N옵션으로 각 스레드가 사용하는 스택 메모리 크기를 결정하며 기본값은 1MB
- 특징 / 수치 / 옵션
- 스택 크기를 줄이면 더 많은 스레드를 생성할 수 있으나 깊은 재귀 호출 시
StackOverflowError위험이 증가 -XX:+UseBiasedLocking은 경합 없는 상황에서 성능을 높이나 최신 JDK에서는 복잡성과 비용 문제로 지원 중단 또는 배제되는 추세임
- 스택 크기를 줄이면 더 많은 스레드를 생성할 수 있으나 깊은 재귀 호출 시
- 주의점: 애플리케이션의 스택 깊이와 사용 가능한 네이티브 메모리 총량을 고려하여 스택 사이즈를 결정해야 함
6. 스레드 및 락 모니터링 (Monitoring Threads and Locks)
- 목적: 데드락 진단 및 실행 중인 스레드의 가시성 문제
- 정의: 도구를 이용한 스레드 상태 추적 미 락 경합 분석
- 구성 / 분류
jstack,jcmd,JFr
- 핵심 매커니즘
jstack,jcmd의Thread.print명령은 실행 시 전체JVM을 일시 정지 시킬 위험이 있음- JFR은 시스템에 매우 낮은 부하를 주면서 락 경합 정보와 스레드 대기 시간을 실시간 기록함
- 특징
JFR은 빈번하게 호출되는 진단 도구들에 비해 저비용이므로 운영 환경에서의 지속적 모니터링에 적합 함
- 주의점: 고부하 시스템의 병목 분석 시에는 시스템 중단 위험이 적은 JFR을 우선적으로 활용하는것이 좋음
10장 자바 서버
자바 NIO
- 목적
- 서버 확장성(Scalability) 확보
- I/O 작업 효율 최적화
- 정의
- 논블로킹 IO 작업을 지원하는 자바 AI
- 구성 / 분류
- 채널 (Channels)
- 버퍼 (Buffers)
- 셀렉터 (Selectors)
- 핵심 매커니즘
- 셀렉터를 이용한 멀티플렉싱 (Multiplexing) 구현
- 단일 스레드에서 다수 채널의 이벤트 상태 감시
- 모든 데이터 입출력에 버퍼 필수 사용
- 채널의 논블로킹 모드 활성화 가능
- 특징
- 스레드당 연결 수 제한 문제 해결
- 스레드 생성 미 컨텍스트 스위칭 오버헤드 감소
- 적용 환경: 대규모 동시 연결 처리가 필요한 서버 환경에 적합
서버 컨테이너 및 스레드 풀 튜닝 (Server Containers & Thread Pools)
- 목적:
- 서버 자원의 효율적 배분
- 처리량 (Throughput) 극대화
- 정의
- 요청 처리를 위해 사전 생성된 스레드 집합 관리 매커니즘
- 구성 / 분류
- Acceptor 스레드: 연결 수락 및 큐 배치 담당
- Worker 스레드: 비즈니스 로직 실행 담당
- 작업 큐 (Queue): 요청 버퍼링 담당
- 핵심 매커니즘
- Aceeptor가 수락한 연결을 작업 큐에 저장
- 유휴 Worker 스레드가 큐의 작업을 인출하여 실행
- 큐는 일시적 부하 발생 시 버퍼 역할 수행
- 작업 큐의 크기는 시스템 레이턴시와 직결
- 특징 / 수치 / 옵션
maxThreads: 최대 동시 처리 스레드 제한치minSpareThreads: 상시 유지 최소 스레드 수minSpareThreads: 설정을 통한 콜드 스타트 레이턴시 방지
- 적용 환경 및 주의점
- CPU 코어 수 기반 스레드 수 설정
- I/O 집약적 작업과 CPU 집약적 작업의 특성 차이 반영 필수
비동기 REST 서버
- 목적
- 작업 대기 중인 Worker 스레드의 점유 방지
- 스레드 풀 가용성 확보
- 정의
- 장시간 소요되는 I/O 작업 시 스레드를 즉시 풀로 반환하는 모델
- 구성 / 분류
@Suspended애노테이션AsyncResponse객체
- 핵심 매커니즘
- 요청 스레드가 비즈니스 로직을 별도 프로바이더에 위임
- 위임 직후 요청 처리 스레드는 스레드 풀로 즉시 반환
- 외부 작업 완료 후
AsyncResponse.resume()호출을 통한 응답 전송
- 특징 / 수치 / 옵션:
- JAX-RS 표준 비동기 모델 활용
- 적은 수의 스레드로 대량의 동시 요청 수용 가능
- 적용 환경
- 외부 서비스 호출 등 블로킹 시간이 긴 작업에 적합
비동기 아웃바운드 호출
- 목적:
- 외부 리소스 호출 시 호출 측 스레드 블로킹 제거
- 정의
- 논블로킹 방식으로 HTTP 요청을 수행하는 클라이언트 매커니즘
- 구성 / 분류
CompletableFutre기반 비동기 체이닝- Java 11
HttpClient
- 핵심 매커니즘
- 요청 송신 후 응답 대기 없이 제어권 즉시 반환
- 응답 도착 시 콜백 또는
Future완료 처리를 통한 후속 로직 실행
- 특징 / 수치 / 옵션
- Java 11 이상 환경 필요
sendAsync메서드 활용- 전용 비동기 스레드 풀 할당 가능
- 적용 환경
- MSA 간 통신 빈도가 높은 아키텍처
11장 생략
12장 Java Se API Tips
Strings: Compact Strings
- 목적: 힙 메모리 내 문자열 객체 데이터의 공간 효율성 극대화
- 정의: Latin-1 범위 문자열로만 구성된 문자열을 16비트 대신 8비트로 저장하는 기법
- 구성 / 분류:
byte[]: 실제 데이터 저장 필드 (기존char[]대체)coder 필드: Latin-1(0) 또는 UTF-16(1) 여부를 나타내는 식별자 (1바이트)
- 핵심 매커니즘
- 문자열 생성 시 모든 문자가 0XFF 이하인지 판별
- 조건 충족 시 내부 배열 크기를 절반으로 축소하여 할당
- 특징 / 수치 / 옵션
-XX:+CompactStrings: 해당 기능 활성화 옵션 (기본값: 활성)- 메모리 절감: 일반적인 힙 애플리케이션 기준 저네 힙 사용량의 10~15% 감소 유도
- 적용 환경 / 주의점
- UTF-16 전용 문자 포함 시 기존과 동일한 2바이트 공간 점유
- 객체 헤더(Header) 및 필드 오버헤드는 그대로 유지
Strings: Duplicate Strings & interning
- 목적: 중복된 문자열 상수를 단일 객체로 통합하여 메모리 낭비 방지
- 정의: 고유한 문자열 값을 보관하는 전역 풀(Global Pool)을 통한 객체 재사용 매커니즘
- 구성 / 분류:
String.intern(): 개발자가 명시적으로 호출하는 수동 인터닝G1 GC String Dedupliation: G1 수집기가 백그라운드에서 수행하는 자동 중복 제거
- 핵심 매커니즘
Native Memory영역의 해시 테이블을 통한 객체 참조 관리
- 특징 / 수치 / 옵션
-XX:StringTableSize=N해시 테이블 버킷 수 설정 옵션- 기본값: Java 8u40+ 기준 60,013
- 해시 충돌: 테이블 크기를 소수(Prime Number)로 설정 시 충돌 확률 감소
-XX:PrintStringTableStatistics: 종료 시 테이블 통계(성공/충돌 비율) ㅜㄹ력
- 적용 환경 / 주의점:
- 과도한 수동 인터닝은 Native 영역 메모리 부족 및 CPU 오버헤드 유발
String Concatenation
- 목적: 문자열 결합 시 발생하는 임시 객체 할당 및 복사 연산 최적화
- 정의: 문자열 더하기(+) 연산자에 대해 컴파일러 및 런타임이 수행하는 코드 변환 기술
- 구성 / 분류
- Java 8 이전:
StringBuilder객체 생성 및append()호출 방식으로 컴파일 - Java 9 이후:
InvokeDynamic Indy)를 통한StringConcatFactory호출
- Java 8 이전:
- 핵심 매커니즘:
MethodHandle기반의 동적 바인딩을 사용하여 최적의 결합 전략을 런타임에 결정
- 특징 / 수치 / 옵션
- 컴파일 타임에 바이트코드를 고정하지 않아 차후 JVM 업그레이드만으로 성능 항상 향유 가능
- 적용 환경 / 주의점
- 루프(Loop) 내에서의 + 연산은 반복마다 신규 StringBuilder 객체 생성 유발
- 대량 반복 결합 시 단일 StringBuilder 인스턴스 사용 시와 성능 차이 발생
Buffer I/O
- 목적: 운영체제 시스템 콜(System Call) 횟수 최소화를 통한 입출력 속도 향상
- 정의: 하드웨어와의 직접 통신 대신 메모리 내 임시 저장소를 거치는 데이터 전송 방식
- 구성 / 분류:
BufferedInpuStream/BufferedOutputStream: 바이트 단위 버퍼링BufferedReader/BufferedWriter: 문자 단위 버퍼링
- 핵심 매커니즘
User Space내부 배열(Buffer)에 데이터를 모아두었다가 임계치 도달 시 일괄 전송
- 특징 / 수치 / 옵션
- 기반 버퍼 크기: 8kb (8,192바이트)
- 소량의 빈번한 쓰기 작업 시 커널 모드 전환 비용 대폭 감소
- 적용 환경 / 주의점
- 이미 대용량 배열을 직접 처리하는 코드에서는 버퍼링 중첩으로 인한 이중 복사 오버헤드 발생
Class Data Sharing(CDS)
- 목적: JVM 가동 시간(Startup Time) 단축 및 여러 프로세스 간 메모리 풋프린트 공유
- 정의: 클래스 메타데이터를 미리 파싱하여 아카이브 파일로 저장하고 런타임에 매핑하는 기능
- 구성 / 분류 :
AppCDS: JDK 표준 클래스를 넘어 사용자 정의 클래스까지 지원 범위 확장
- 핵심 매커니즘
- 클래스 로딩 시 필요한 검증(Verification) 및 파싱(Parsing) 단계 생략
- 메모리 맵 파일(.jsa)을 사용하여 다수의 JVM이 동일한 일기 전용 데이터 공유
- 특징 / 수치 / 옵션
-Xshare:dump: 공유 아카이브 파일 생성 옵션-Xshare:on: CDS 사용 강제 옵션 (실패 시 JVM 기동 중단)
- 적용 환경: 마이크로서비스(MSA) 및 서버리스 환경과 같이 기동 속도가 중요한 아키텍처에 적용
Random Numbers
- 목적: 멀티스레드 환경에서 난수 발생기의 경합 제거
- 정의: 스레드 안정성 확보를 위한 다양한 의사 난수 생성 API
- 구성 / 분류
java.util.Random: 모든 스레드가 단일 시드(Seed) 고융java.util.concurrent.ThreadLocalRandom: 스레드별 개별 시드 할당java.security.SecureRandom: OS 엔트로피 풀을 사용하는 보안용 난수 생성기
- 핵심 매커니즘:
- Random 클래스는 시드 업데이트를 위해
CAS(Compare-And-Sweep)연산 수행 ThreadLocalRandom은 스레드 내부 필드에 시드를 격리하는 CAS 오버헤드 원천 봉쇄
- Random 클래스는 시드 업데이트를 위해
- 특징
SecureRandom: 동기화 비용 외에 시스템엔트로피 부족 시 스레드 블로킹 발생 가능
- 주의점
- 고도의 동시성이 요구되는 서버 환경에서 Random 사용 시 CPU 사이클 낭비 발생
Java Native Interface(JNI)
- 목적: Java 코드에서 C/C++ 라이브러리 호출 및 OS 저수준 기능 제어
- 정의: JVM과 네이티브 코드 간의 통신을 위한 표준 인터페이스
- 핵심 매커니즘
Java Stack에서Navtive Stack으로 프레임 전환 수행- 인자 전달을 위한 데이터 마샬링 및
CPU 레지스터 컨텍스트 스위칭발생
- 특징
- 인라인화 불가: JIT 컴파일러가 네이티브 메서드 경계를 가로질러 최적화(Inlining)수행 불가
- 호출 비용: 단순 Java 메서드 호출 대비 수십 배 이상의 CPU 사이클 소모
- 주의점
- 연산량이 극히 적은 작업을 JNI로 분리할 경우 전환 오버헤드가 연산 이득을 상쇄
Exceptions & Logging
- 목적: 런타임 예외 처리 및 로깅 시스템에 의한 성능 저하 억제
- 구성 / 분류
Stack Trace 생성 비용: 예외 객체 인스턴스화 시 발생하는 부하- 로그이 오버헤드: 문자열 결합 및 파일 I/O에 따른 비용
- 매커니즘
Throwable.fillInStackTrace(): Native 호출을 통해 호출 스택 전체를 순회하며 캡처
- 특징 / 수치 / 옵션
- 비용 비례성: 스택 깊이가 깊을수록 스택 워킹 및 메모리 복사 비용 선형 증가
-XX:-StakTraceInThrowable: 스택 트레이스 생성 비활성화 옵션
- 주의점:
- 예외를 로직 제어 목적으로 사용할 경우 성능 측정 지표 왜곡 및 저하 발생