5. 최적화 사례 분석 및 실전

2026. 1. 17. 15:08Life Style/jvm 밑바닥까지 파헤치기

5.1 들어가며

- 애플리케이션에서 실제로 발생하는 문제들에 대처하려면 2~4장의 지식과 도구만으로 부족함. 이번 장에서 실제 사례 기반으로 살펴보자.

5.2 사례 분석

5.2.1 대용량 메모리 기기 대상 배포 전략

하루 페이지 뷰가 15만 정도인 웹 사이트에서 하드웨어 시스템 교체 발생
웹 사이트는 64비트 JDK 5로 구성, -Xmx, -Xms 매개변수 지정하여 자바 힙 크기를 12GB로 고정
그렇게 얼마간 운영 결과 웹 사이트가 장시간 응답하지 않는 일 자주 발생

문제 원인은 가비지 컬렉션
페러렐 컬렉터로 GC 실행 -> 힙 메모리를 크게 잡아서 회수하고 재활용 하는데 오래 걸리는 것이 주된 문제
(교체 전에는 32 비트 운영체제, 힙 메모리는 겨우 1.5GB에 불과)

해결책
- "셰넌도어(Shenandoah)와 ZGC"
👉 “힙 크기와 거의 무관하게 Stop-The-World(STW)를 매우 짧게 유지하는” 차세대 GC
- "가상 머신 여러개 동시에 띄워 논리적인 클러스터 구성"

5.2.2 클러스터 간 동기화로 인한 메모리 오버플로

서버 구성

듀얼 프로세서와 8GB 메모리를 갖춘 HP 미니컴퓨터 2대 사용
(각각 미들웨어인 웹로직 9.2를 3개씩 구동하여 총 6노드의 선호도 클러스터 구성)

- 선호도 클러스터란?
클라이언트의 세션을 특정 노드에 고정(affinity) 시켜
상태 정보를 공유하지 않고도 안정적으로 서비스를 제공하는 클러스터 구성

선호도 클러스터이므로 노드 사이에 세션 동기화는 일어나지 않지만 일부 데이터를 공유 -> DB 로 관리하다
JBossCache로 글로벌 캐시 구축

처음에는 원활하다가 시간이 지나자 메모리 오버플로가 가끔씩 발생

원인) JBossCache로 노드 간 데이터 공유시,
모든 노드가 데이터 수신했는지 확인할 때까지는 메모리에 데이터 보관
특정 상황에서 네트워크가 데이터 전송량을 다 처리하지 못하게 되면 
재전송된 데이터가 메모리에 계속 쌓이다가 오버플로 발생

5.2.3 힙 메모리 부족으로 인한 오버플로 오류

브라우저-서버 기반 온라인 시험 시스템
(서버 푸시 기술을 활용해 클라이언트가 서버로부터 시험 데이터를 실시간으로 받아볼 수 있음)

테스트 중 서버에서 메모리 오버플로가 발생

힙 메모리 최대로 늘려서 해결하고자 시도, 하지만 여전히 메모리 오버플로 발생

jstat 구동해 로그 확인

원인) 다이렉트 메모리 부족

5.2.4 시스템을 느려지게 하는 외부 명령어

대학교 운영을 디지털화 해 주는 시스템
이 시스템에 동시성 스트레스 테스트를 수행하자 응답 속도가 지나치게 느려짐

원인) 사용자 요청 처리를 위한 외부 셸 스크립트 실행

해결) 셸 스크립트 실행 코드 지우고 해당 정보를 JAVA API로 가져오도록 수정

5.2.5 서버 가상 머신 프로세스 비정상 종료

5.2.2 시스템(그림)

한동안 문제없이 운영되던 시스템에서
클러스터 노드의 가상 머신 프로세스가 갑자기 닫히는 일이 빈번히 발생

원인) 동기화 요청이 비동기로 이루어졌는데
속도가 느려서 계속 대기중인 스레드와 소켓 연결이 점점 많아지다가 
가상 머신의 한계를 넘어서서 가상머신 프로세스가 비정상 종료된 것

해결) 동기화 요청 인터페이스 수정하고, 비동기 호출을 생성자/소비자 방식의 메시지 큐로 변경

5.2.6 부적절한 데이터 구조로 인한 메모리 과소비

자바 가상 머신을 이용하는 백그라운드 원격 프로시저 호출 서버 사례

데이터 분석을 위해 10분 단위로 80MB 크기의 파일을 메모리로 읽어들임
이 때 100만 개 이상의 HashMap<Long, Long> 객체를 만들어 냄

마이너 GC가 100만개가 넘는 객체를 검사하느라 일시 정지가 500밀리초로 늘어남
👉 마이너 GC (Minor GC)는 자바 힙의 신세대(Young Generation) 영역에서만 발생하는 가비지 컬렉션

Heap
 ├─ Young Generation (신세대)
 │   ├─ Eden : 새로 생성된 객체가 들어오는 곳
 │   ├─ Survivor S0 ├─ 살아남은 객체들이 잠시 머무는 곳
 │   └─ Survivor S1 │
 └─ Old Generation (구세대) : 오래 살아남은 객체들이 이동되는 곳

해결책) 프로그램 자체를 고치는 것

5.2.7 윈도우 가상 메모리로 인한 긴 일시정지

심장 박동을 보여주는 GUI 데스크톱 프로그램

거짓 양성 데이터가 자주 섞여 들어오는 문제 발생
로그 확인 후 1분 간격으로 로그 출력 없이 일시정지 상테가 되기 때문임을 확인

매개변수 추가 후 긴 일시 정지 때의 로그 정보 자세히 살핌

프로그램 창 최소화하면 작업 메모리가 디스크로 스와프 됨
이 상태에서 가비지 컬렉션 진행되면 스와프된 데이터를 메모리로 다시 불러옴
(비정상적으로 긴 일시 정지 현상의 원인)

해결책) 매개변수 추가하여 프로그램이 최소화되어도 작업 메모리 유지

5.2.8 안전 지점(Safepoint)으로 인한 긴 일시 정지

HBase 클러스터에서 GC 자체는 빨랐지만,
Safepoint에 들어가는 데 시간이 오래 걸려
전체 Stop-The-World(STW)가 3초 이상 발생한 장애 사례

GC 로그상 GC 시간은 0.1~0.3초, 애플리케이션 전체가 멈춘 시간(STW)은 2~3초
❓ “GC는 빨리 끝났는데, 왜 서비스는 몇 초 동안 멈췄지?”

Safepoint 로그를 찍어보니: RpcServer.listener 이 스레드가 Safepoint에 너무 늦게 들어옴
- Safepoint란? JVM이 GC를 하기 전에 모든 스레드를 “안전한 지점”에서 멈추게 하는 것

int i = start; 
while (i <= end) { //반복문의 i가 int 타입, JVM은 성능 최적화를 위해: int 기반 루프에는 Safepoint를 자주 넣지 않음
    ...
    if (c.timedOut(...)) {
        closeConnection(c);
        end--;
    } else {
        i++;
    }
}

비유를 들자면,
GC가 시작됨 → “다 멈춰!”
대부분 스레드: 바로 멈춤

그런데…
RpcServer.listener는: 수십만 개 connection이 발생하는 큰 루프 한가운데 Safepoint가 없음
루프 끝날 때까지 계속 실행
👉 그동안: 다른 모든 스레드가 기다림, 결과: STW가 2~3초로 증가

해결책)
루프 index를 long으로 변경

long i = start;
while (i <= end) {
    ...
}

왜 효과가 있나? JVM이 매 반복마다 Safepoint 삽입
GC 발생 시: 한 번 반복 끝나고 바로 Safepoint 진입 가능

REF) https://juejin.cn/post/6844903878765314061

5.3 실전: 이클립스 구동 시간 줄이기

5.3.1 최적화 전 상태

  • 컴파일: 총 1만8552번 컴파일에 약 1분 6초 소요
  • 클래스 로딩: 총 2만 6098개의 클래스 로딩에 18.84초 소요
  • 가비지 컬렉션: 28번 수행에 396밀리초 소요
  • 힙 메모리 구성
    • 에덴: 최대 2GB 중 108MB 할당
    • 생존자 공간: 최대 2GB 중 1MB 할당
    • 구세대: 최대 2GB 중 232MB 할당

5.3.2 JDK 버전 업그레이드에 따른 성능 변화

  • JDK 버전 업그레이드되면 전반적인 성능 개선 효과 나타남

5.3.3 클래스 로딩 시간 최적화

  • as-is : 클래스 로딩에 총 2만 6098개의 클래스 로딩에 18.84초 소요
  • to-be : -Xverify:none 매개변수 설정하면 바이트코드 검증을 거너뛰는데 이를 추가하니 클래스 로딩속도가 14초로, 2초 빨라졌음

5.3.4 컴파일 시간 최적화

  • 컴파일 시간 측면에서는 시간 단축 요인을 찾지 못함

5.3.5 메모리 설정 최적화

  • -Xlog:gc*(자세한 GC로그 출력 기능),gc:gc.log(출력된 GC 로그를 gc.log 파일에 저장) 매개변수 추가해서 GC 로그를 확인 가능

5.3.6 적절한 컬렉터 선택으로 지연 시간 단축

  • JDK 17이 제공하는 컬렉터 중에 G1과 ZGC를 비교 -> 유의미한 차이을 발견하지 못함