Chapter 5. Optimization Practice
사례 분석
1. 대용량 메모리 기기 대상 배포 전략
하루 15만건의 페이지 뷰 웹사이트
개요
JDK5
기존:
32비트 운영체제
1.5GB 힙 메모리
변경:
64비트 운영체제
16GB 힙메모리
-Xms를 통한 힙크기 12GB 고정
문제
하루 페이지 뷰가 15만 정도되는 웹사이트에서 서버 실행 효율이 기대 이하이고, 웹 사이트가 장시간 응답하지 않는 상황이 자주 발생.
원인
가비지 컬렉션에서 원인을 찾음.
페러랠 컬렉터
Young: 패러렐 스캐빈지
Old: 패러렐 올드
최장 14초의 STW 12GC에 달하는 힙 메모리를 전체 GC하기 위해 최대 14초까지 STW 발생.
구세대에 쌓이는 거대 객체 사용자가 웹 페이지를 요청하면 해당 파일을 디스크에서 메모리로 읽어 들이고, 이때 웹 페이지를 직렬화하는 과정에서 메모리에는 수많은 거대 객체가 쌓임.
상황 분석
현재까지단일 자바 애플리케이션을 배포하는 주된 방식
가상 머신 인스턴스 하나가 거대한 자바 힙 메모리 관리
가상 머신 여러 개를 동시에 띄워 논리적인 클러스터 구성
현재 사례에서는,
사용자 상호 작용이 많고
일시 정지 시간에 민감하고
대용량 메모리를 갖춤.
해결책 1. 단일 인스턴스
가상 머신 인스턴스 하나가 거대한 자바 힙 메모리 관리.
셰넌도어 / ZGC 처럼 지연 시간 통제를 목표로 하는 가비지 컬렉터를 이용하면 문제 해결.
패러렐 컬렉터로 거대 힙을 성공적으로 관리하는 사례
전체 GC 빈도를 가능한 한 낮게
사용자가 이용하지 않는 떄에 발생
힙 관리를 하루동안 잘하고, 새벽에 GC를 돌리는 방식. 이때 GC 빈도를 제어하려면 구세대가 안정되어야함. 오래 생존하는 객체가 적어야하고, 이를 통해 구세대 공간을 여유 있게 관리할 수 있음.
단, 고려해야 할 것들이 존재
힙 메모리의 거대 블록들을 수거하느라 일어나는 STW는 G1 컬렉터의 등장과 점진적 회복(incremental recovery)이 활발히 활용되면서 많이 줄어들었다.
64비트 가상 머신은 동일 버전 32비트 가상 머신보다 조금씩 느리다. (64비트 가상 머신에서는 대용량 메모리를 사용할 수 있다. 하지만 압축 포인터나 프로세서 캐시 라인 용량 같은 요인 때문)
애플리케이션이 충분히 안정적이어야 한다.
같은 프로그램이더라도 32비트 가상 머신보다 64비트 가상 머신에서 메모리를 많이 사용. (포인터 확장, 데이터 타입 정렬, 패딩 등의 요인 때문. 압축 포인터를 사용하여 메모리 추가 소비를 줄일 수 있음.)
해결책 2. 다중 인스턴스
가상 머신 여러 개를 동시에 띄워 논리적인 클러스터 구성
가상 머신 여러 개로 논리 클러스터를 구축해 하드웨어 자원을 십분 활용하는 것.
예를 들어,
같은 물리 머신에서 애플리케이션 서버 프로세스를 여러 개 띄우고 각각에 서로 다른 포트를 할당.
앞단에 로드밸런서를 두어 리버스 프록시 방식으로 요청을 분배.
이 방식의 목적은 하드웨어 자원을 최대한 끌어 쓰기 위함. 따라서 애플리케이션을 역할이나 분야별로 분할하거나 Statefull 또는 핫 전송 같은 고가용성 요구 사항은 고려하지 않아도 상관 없음
단, 고려해야 할 것들이 존재
노드들이 전역 자원을 놓고 경합한다. (동시 쓰기 연산 문제)
연결 풀과 같은 자원 풀을 효율적으로 활용하기 어렵다. (노드 별로 따로 만들어 관리되기 떄문)
각 프로세스는 약 2GB~4GB까지의 메모리만 할당됨. (클러스터 노드로 32비스가상 머신을 이용하면, 노드별 메모리는 여전히 32비트로 제한.)
노드당 로컬 캐시가 따로 존재. (메모리 낭비) -> 중앙화된 캐시를 활용
이 사례에서는 두번째 해결방법을 선택.
2. 클러스터 간 동기화로 인한 메모리 오버플로
브라우저-서버 기반 경영 정보 시스템
개요
듀얼 프로세서
8GB 메모리
미니 컴퓨터 2대에 3개씩 노드를 구성하여 구동.
문제
공유 데이터를 데이터베이스로 관리했지만, 읽기, 쓰기 경합 발생
이를 해결하기 위해 글로벌 캐시를 뒀지만, 메모리 오버플로 발생
원인
-XX:+HeapDumpOnOutOfMemoryError 매개변수를 추가하고 스냅숏을 분석.
수 많은 org.jgroups.protocols.pbcast.NAKACK 객체 발견
데이터 전송 실패시 재전송을 위해 그룹 멤버십 서비스에 등록된 모든 노드가 데이터를 제대로 수신했지 확인할때 까지 메모리에 보관해야함.
유저의 요청이 페이지당 수십개의 요청이 일어나는 상황에서, 클러스터 노드들 사이의 네트워크 통신이 빈번하게 발생.
이때 네트워크가 데이터 전송량을 다 처리하지 못하게 되면, 재전송된 데이터가 메모리에 계속 쌓이다가 오버플로를 발생시킴.
해결책 1
메시지를 메모리에 저장하는 상황에서 문제가 발생한다면, 메시지의 크기를 줄일 수 있음
네트워크 전송량을 처리할 수 있도록 압축을 할 수 있음. 다만, CPU 부하가 발생
해결책 2
JGroup의 전송 대기열의 사이즈를 제한할 수 있음.
다만, 이때 큐를 넘어서는 메시지는 버려지거나, 블로킹되거나, 예외가 발생하는 단점이 있음.
3. 힙 메모리 부족으로 인한 오버플로 오류
브라우저-서버 기반 온라인 시험 시스템
서버 푸시 기술을 활용해 클라이언트가 서버로부터 시험 데이터를 실시간으로 받음.
개요
i5 CPU
4GB 메모리
32비트 윈도우
문제
테스트 중 서버에서 메모리 오버플로 가끔 발생.
오류가 발생하면 온라인 시험 자체가 엉망이 됨.
힙 용량을 키우고, -XX:+HeapDumpOnOutOfMemoryError를 설정해도, 시스템은 덤프 파일을 생성하지 못함.
jstat을 보니, 힙, 메서드 영역은 모두 안정적이고, GC는 자주 발생하지 않았다.
원인
운영체제에서는 개별 프로세스가 관리할 수 있는 메모리 최대 크기에 제한이 있음
2GB중 1.6GB를 힙에 할당.
다이렉트 메모리 역시 GC의 대상인데, 공간이 부족하더라도 해당 메모리는 GC에 능동적으로 알리지 못함.
힙의 구세대가 꽉차길 기다리면서 GC가 수행되길 기다려야함.
다이렉트 메모리 부족 OutOfMemoryError
스레드 스텍 메모리 부족 StackOverFlowError (가상머신 허용치를 넘는 깊이) OutOfMemoryError (용량을 동적으로 확장하려할때)
소켓 버퍼 영역 메모리 부족 (소켓 연결에 receive와 send라는 두가지 버퍼 영역이 할당. 각 (37KB, 25KB)
네이티브 메모리 부족 (네이티브 라이브러리를 이용하기 위해 JNI를 호출하고, 이때 네이티브 라이브러리는 네이티브 메서드 스택과 네이티브 메모리 사용)
가상 머신과 가비지 컬렉터 구동 메모리 부족
해결책
4. 시스템을 느려지게 하는 외부 명령어
대학교 운영을 디지털화해주는 시스템.
개요
프로세서 4개가 장착된 솔라리스 10 시스템
글래스피시 미들웨어 사용
문제
동시성 스르테스 테스트시 응답 시간 저하
원인
사용자 애플리케이션이 아닌, 시스템이 자원을 대부분 소비
상황 분석
프로세서 자원 대부분을 소비하는 시스템 콜이 무엇인지 식별(솔라리스10의 dtrace 스크립트)
fork 시스템 콜이 문제. (새로운 프로세스를 생성할때 호출)
사용자 요청을 처리하려면 특정 시스템 정보가 필요하여, 요청 각각 외부 셀 스크립트를 실행
Runtime.getRuntime().exec() 메서드로 실행되고, 이는 자원을 많이 소모함. (현재 가상 머신과 똑같은 환경 변수 설정을 공유하는 프로세스를 복사 -> 새로운 프로세스에서 외부 명령 실행 -> 프로세스 종료 과정)
해결책
셀 스크립트를 실행하는 코드를 지우고, 필요한 정보를 자바 API로 가져오도록 변경
5. 서버 가상 머신 프로세스 비정상 종료
브라우저-서버 기반 경영 정보 시스템
개요
듀얼 프로세서
8GB 메모리
미니 컴퓨터 2대
총 6개의 노드의 선호도 클러스터로 구성된 시스템.
문제
클러스터 노드의 가상 머신 프로세스가 갑자기 닫히는 일이 빈번해짐
가상 머신 프로세스는 log파일만 남긴 채 사라지고, 두 컴퓨터의 모든 노드에서 프로세스 충돌 발생
원인
경영 정보 시스템에서 할 일 항목의 상태가 바뀌면, 사무 자동화 포털 시스템이 웹서비스를 통해 정보를 받아와서 동기화.
동기화 요청을 분석(SoapUI)하자, 3분후 timeout에 의한 연결 중단 응답을 받음
웹 서비스 호출을 비동기로 수행하고 있었고, 사무 자동화 시스템이 제때 응답해 주지 않아서 대기 중인 스레드와 소켓 연결이 점점 많아짐.
해결책
비동기 호출 부분을 생산자/소비자 방식의 메시지 큐로 변경하여 문제 해결
6. 부적절한 데이터 구조로 인한 메모리 과소비
64비트 자바 가상 머신을 이용하는 백그라운드 원격 프로시저 호출(RPC) 서버 사례
개요
메모리는 -Xms4g -Xms8g, Xmn1g로 설정
파뉴 + CMS 컬렉터 조합 이용
마이너 GC 시간은 30ms 이하.
문제
데이터 분석을 위해 10분 단위로 80MB 크기의 파일을 메모리로 읽는 과정에서, 100만 개 이상의 HashMap<Long,Long> 객체를 생성
마이너 GC가 해당 객체들을 검사하는 과정에서 STW 발생
원인
데이터 파일을 분석하는 동안 총 800MB 용량의 에덴이 빠르게 채워져서 GC가 발생.
하지만 Minor GC 후에도 신세대 객체 대부분이 생존.
파뉴 컬렉터의 복사 알고리즘으로 인해, 복사하는 과정, 참조를 관리하는 과정이 무거움
해결책 1. GC 최적화
GC 최적화만으로 해결이 가능하지만 부작용이 큼.
생존자 공간을 제거하여 첫 번째 Minor GC 후 신세대에서 살아남은 객체들을 곧바로 구세대로 옮기는 방법. 죽은 객체 회수는 다음번 Major GC에 맡기는 것.
해결책 2. 데이터 구조 수정
Key와 Value를 담기 위한 객체는 long 값 두개만 필요하지만, Long 객체(24바이트) + Map.entry 저장(16바이트) + next 필드(8바이트) + 해시 필드(4바이트)로 구성되어 비효율적.
7. 윈도우 가상 메모리로 인한 긴 일시 정지
GUI 데스크톱 프로그램에서 발생
개요
심장 박동 데이터는 서드 파티 서비스로부터 15초마다 얻어 옴.
서드 파티 서비스가 30초 내로 회신하지 않으면 연결이 끊긴걸로 간주
문제
거짓 양성 (false positive) 데이터가 자주 섞여 들어오는 문제 발생
원인
거짓 양성이 생기는 이유는 프로그램이 약 1분 간격으로 로그 출력 없이 일시 정지 상태가 되기 때문.
-XX:+PrintGCApplicationStoppedTime, -XX:+PrintGCDate-Stamps -Xloggc:gclog.log 변수를 추가한 후, GC가 문제임을 확인
-XX: +PrintReferenceGC 매개 변수를 추가하여 로그 정보를 자세히 보니, 컬렉션 준비 단계에서 실제 시작까지 시간 소요가 큼.
프로그램 창을 최소화하면 메모리 사용량이 급격하게 줄었지만, 가상 메모리에는 변화가 없음. 창을 최소화하면 작업 메모리가 디스크로 스왑된다고 짐작됨.
위 상황에서 GC를 수행하려면 스와프된 데이터를 메모리로 다시 불러와야함.
해결책
GUI 프로그램에서 이 현사은, -Dsun.awt.keepWorkingSetOnMinimize=true 로 설정하면 쉽게 해결
8. 안전 지점으로 인한 긴 일시 정지
컴퓨팅 작업을 처리하는 HBase클러스터
개요
JDK8
G1 컬렉터
매일 대량의 맵리듀스나 스파크 오프라인 분석을 수행
-XX:MaxGCPauseMillis를 500ms로 설정. (오프라인 분석에서 지연시간은 중요하지 않다고 생각)
문제
GC의 STW가 3초 이상까지 길어지는 일이 자주 발생
원인
GC가 객체를 회수하는 시간은 x00ms밖에 안걸림.
다만, 사용자 스레드 모두가 안전 지점에 도착할 때까지 기다리게 설정되어 있음
RpcServer.listener,port=24600 스레드가 원인
순환문이 안전 지대로 설정되지 않음. 보통 핫스팟 가상 머신은 순환문을 평가하여 최적화를 함. int 이하의 범위는 안전 지점으로 설정하지 않음. (카운티드 루프. 순환문의 실행 시간이 결정요인이 아님.)
해결
loop타입의 변수 타입을 long으로 변경. (-XX:+UserCountedLoopSafePoints를 사용할 수 있지만 버그가 존재하여 사용하지 않음)
성능 최적화 실전
1. JDK 업그레이드
동작 원리: JVM은 버전이 올라갈수록 내부적으로 JIT 컴파일러 성능, GC 알고리즘, 클래스 로딩, 런타임 최적화 등의 성능이 향상됩니다.
장점:
최신 GC(G1, ZGC 등) 지원
전반적인 실행 성능 향상 (코드 변경 없이)
JVM 내부 최적화 반영됨 (GC/클래스 로딩/JIT 등)
단점:
일부 레거시 라이브러리나 플러그인이 최신 JDK와 호환되지 않을 수 있음
환경 설정 변경 필요 (특히 Eclipse에서
-vm
설정 필요)
2. -Xverify:none
동작 원리: JVM은 클래스 로딩 시 바이트코드를 검증하는데, 해당 옵션은 이 검증을 생략합니다.
장점:
클래스 로딩 속도 개선 → 전체 구동 시간 단축
안정된 환경(검증된 코드)에서는 불필요한 검증 비용 제거 가능
단점:
보안 취약점 가능성 (잘못된 바이트코드도 로딩될 수 있음)
외부 라이브러리 사용 시 문제가 발생할 수 있음
3. -Xms, -Xmx (Heap Size 설정)
동작 원리: JVM의 힙 영역 크기를 초기/최대로 고정시켜, GC 발생 빈도를 조절합니다.
장점:
GC 발생 횟수 감소 → 성능 안정화
힙 크기 예측 가능 → 서버 환경에서 유리
단점:
메모리 할당이 고정되기 때문에 시스템 리소스 부족 시 병목 발생 가능
너무 크게 설정하면 메모리 낭비
4. -XX:+DisableExplicitGC
동작 원리: 코드 내에서
System.gc()
호출이 있어도 무시하도록 하여 강제 GC를 방지합니다.장점:
불필요한 전체 GC 수행 방지 → 지연 시간 감소
GC 타이밍을 JVM에게 완전히 위임 가능
단점:
일부 라이브러리는
System.gc()
를 통한 명시적 메모리 해제를 기대할 수 있음 → 예상치 못한 메모리 사용 증가
5. -Xlog:gc*, -Xlog:gc:gc.log
동작 원리: GC 발생 시점, 시간, 이유 등을 로그로 남겨주는 옵션.
gc.log
파일로 저장 가능.장점:
GC 동작 분석 가능 → 성능 튜닝 필수 도구
병목 분석에 효과적
단점:
로그 크기 증가 → 장기 운영 시 디스크 사용량 증가
GC 로그 해석에 대한 이해 필요
6. -XX:+UseG1GC, -XX:+UseZGC
동작 원리:
G1GC: 힙을 여러 영역으로 나눠서 병렬로 GC를 수행하여 지연시간을 줄이는 방식
ZGC: GC 수행 중 애플리케이션 정지를 거의 0에 가깝게 만드는 저지연(低遲延) GC
장점:
G1: 짧은 정지 시간(평균 37ms 수준), 안정적
ZGC: GC activity가 0%에 가까움, 초대형 힙에 적합
단점:
G1: 메모리 부족 상황에선 Minor GC가 자주 발생
ZGC: 성숙도가 낮고, 분석 툴 호환성이 떨어질 수 있음
7. -Xint, -Xcomp (JIT 설정)
동작 원리:
-Xint
: JIT 컴파일을 하지 않고 모든 바이트코드를 인터프리터 방식으로 실행-Xcomp
: 시작부터 모든 코드를 컴파일하려 시도 → 최적화된 실행 경로 확보
장점:
-Xint
: 디버깅 용도나 JIT 영향을 제거한 성능 측정 가능-Xcomp
: 최적화된 코드로 빠르게 실행될 가능성
단점:
-Xint
: 매우 느림 (30초 이상 소요되는 경우도 있음)-Xcomp
: 구동 속도가 느려질 수 있고, 예외나 초기 오버헤드 발생
8. -client / -server 모드
동작 원리:
-client
: 빠른 시작과 짧은 생명주기에 적합한 JIT 설정 (GUI에 적합)-server
: 실행 성능 최우선 → 서버나 장기 실행 애플리케이션에 적합
장점:
-client
: GUI 환경에서 빠른 시작 가능 (이클립스처럼)-server
: 장기 실행에 적합, 최적화된 성능
단점:
JDK 9부터는
-client
모드 제거됨 (즉, 최신 환경에선 무효)
Last updated