JVM

jin·2일 전

JVM은 왜 태어났나

과거에는 윈도우용 프로그램과 리눅스용 프로그램, 맥용 프로그램을 전부 따로 만들어야 했다. 그래서 자바는 엄청난 선언을 한다

Write Once, Run Anywhere

JVM의 내부 구조

  1. 클래스 로더(Class Loader)
    프로그램이 실행되면 가장 먼저 하드디스크에 있는 바이트코드를 싹 모아서 메모리에 올린다.

  2. 실행 엔진(Execution Engine)
    메모리에 올라온 바이트 코드를 기계어로 번역하고 실행한다. 이때 자주 쓰이는 코드는 매번 번역하지 않고 아예 기계어로 바꿔버리는 JIT(Just-In-Time)컴파일러를 사용하여 파이썬 보다 훨씬 빠른 계산 속도를 낸다.

  3. 메모리

  • 스택 영역: 각 쓰레드가 각자 가지는 공간
  • 힙 영역: 덩치가 큰 공유 데이터를 저장하는 공간

쓰레드 풀과 톰캣의 동작 방식

스프링 부트로 서버를 띄우면 기본적으로 내장된 톰캣이라는 매니저가 프로그램 문을 연다.
미리 쓰레드를 생성해둔다.(200개의 Thread Pool)

1. 손님 입장 (요청 도착): 손님이 10명 들어옵니다.
2. 대기실에서 직원 호출: 톰캣 매니저가 쓰레드 풀(대기실)에서 쉬고 있던 직원 10명을 부릅니다.
3. 전담 마크 시작: 10명의 직원이 각각 1명의 손님을 맡아 주문을 처리합니다. DB 응답을 기다릴 때는 다른 일은 하지 않고 얌전히 기다립니다(Blocking).
4. 대기실 복귀 (반납): 식사(응답 완료)가 끝나면, 이 직원은 퇴사(쓰레드 소멸)하는 것이 아니라 다시 대기실(Pool)로 돌아가 다음 손님을 기다립니다.

손님이 201명 왔는데요?

FastAPI라면 1명의 직원이 비동기로 201번째 주문도 순식간에 받아버렸겠지만, 톰캣은 원칙상 직원이 남아있지 못하면 주문을 받지 못한다.
이에 대비하여 톰캣은 웨이팅 줄(Queue, Accept Count)를 준비해둔다.

- 큐(Queue) 대기: 201번째 손님부터는 식당 문 앞의 대기열(Queue)에서 줄을 서서 기다립니다. (기본적으로 100명까지 줄을 설 수 있습니다.)
- 직원 순환: 안에 있던 200명 중 누군가 서빙을 마치고 대기실로 돌아오면, 매니저가 잽싸게 그 직원을 대기열 1번 손님에게 배정합니다.
- 진짜 대참사 (Connection Refused): 만약 줄 서는 공간마저 꽉 차버린 상태에서 새로운 손님이 오면? 톰캣은 **"죄송합니다. 오늘 영업 끝났습니다"**라며 에러를 뱉어냅니다(이것이 그 유명한 서버 다운, 503 Service Unavailable 등의 상태입니다).

그래서 만약 트래픽이 몰릴 것이 예상되면, 이 쓰레드 풀의 크기(Max Threads)를 200에서 400으로 늘리거나, 대기열의 크기를 튜닝하는 작업을 하게된다.

가비지 컬렉터(Garbage Collector,GC)

앞서 톰캣 서버는 쓰레드가 쉴 새 없이 요청을 전담해서 처리한다고 했다. 그럼 공용으로 쓰이는 Heap 메모리에는 이 전 요청을 처리할 때 사용했던 데이터 객체들이 산더미 처럼 쌓인다.

생성된 데이터의 98%는 잠깐 쓰이고 바로 버려진다. 끝까지 살아남은 놈은 극소수다

예를 들어 손님이 주문할 때 잠깐 쓴 '주문서 객체'는 요리가 나오면 바로 쓰레기가 된다. 반면 '메뉴판 객체'나 '식당 설정 정보'는 식당이 문을 닫을 때 까지 살아있어야 한다.
그래서 GC는 힙 메모리라는 거대한 식당을 아예 두 구역으로 쪼개버린다.

  1. 신규 구역(Young Generation): 패스트푸드 존
  • Minor GC : 이 구역이 꽉 차차면 청소부 아저씨가 빗자루를 들고 쓱 훑고 지나간다. 워낙 순식간에 일어나서 서버 성능에는 거의 영향을 주지 않는다.
  1. 고인물 구역(Old Generation): VIP 룸
    신규 구역에서 청소부 아저씨의 빗자루 질을 여러 번 버텨낸 질긴 객체가 있다. 이런 애들은 고인물 구역으로 Promotion 하여 자리를 잡는다.
  • Major GC/Full GC : 청소부 아저씨가 대청소를 선언한다. 이곳은 공간이 워낙 넓고 복잡해서 청소하는데 시간이 아주 오래 걸린다.

🚨"전원 멈춰! 청소 끝날 때까지 아무도 움직이지 마!"
Stop-The-World(STW)

대청소가 시작되는 순간 톰캣의 200개의 애플리케이션 쓰레드는 하던 일을 완전히 멈추고 얼음상태가 된다. STW가 0.1초 만에 끝나면 다행이지만, 만약 메모리가 너무 꼬여서 3~5초 동안 멈춘다면? 그 5초 동안은 어떤 손님도 주문을 낼 수 없고, 식당 밖에서 대기하던 손님들은 앱 화면이 멈춰버리는 끔찍한 경험을 하게 된다. (API 지연 시간 폭발)

따라서 어떻게 하면 객체를 덜 만들어서 이 대청소를 최대한 안 일어나게 할까라는 고민에서 나온 것이 JVM 튜닝이다.

JVM 튜닝

1) Heap Size 조절: 가장 기본적이고 직관적인 튜닝. 즉 청소할 공간의 절대적인 크기를 정해준다.

  • -Xms(초기 힙 크기)/-Xmx(최대 힙 크기)
    ☑️ 크게 잡으면: 식당이 아주 넓어지니 쓰레기가 꽉 차는 데 오래 걸립니다. 즉, 대청소(Major GC) 횟수가 줄어듭니다. 하지만 한 번 대청소를 시작하면 치울 게 너무 많아서 STW 시간(멈춤 현상)이 엄청나게 길어집니다.
    ☑️ 작게 잡으면: 반대로 대청소를 훅훅 끝낼 수 있지만, 5분마다 대청소를 하겠다고 식당을 멈춰 세우니 서버가 제 기능을 못 합니다.

2) 구역의 비율 조절(Young vs Old 영역 크기)

  • -XX:NewRatio (Old 영역과 Young 영역의 비율)
    Young구역을 넓히면 금방 먹고 나갈 손님들(단기 객체)를 수용하기 좋아진다. 하지만 VIP룸이 좁아지며 조금만 오래 앉아있어도 VIP룸이 꽉 차서 무거운 대청소가 너무 자주 발생한다.
    따라서 서비스가 금방 쓰고 버리는 데이터가 많은지 오래 들고 있어야 할 캐시 데이터가 많은지를 분석하여 파티션을 밀고 당기는 것이 중요하다.

3) 청소 업체 알고리즘(GC 알고리즘)
옛날에는 식당 크기나 비율을 수동으로 깎는 튜닝을 많이 했지만, 요즘 현대 자바(Java 8 이후 ~ Java 21)에서는 "그냥 더 똑똑하고 비싼 청소 업체를 고용하자!"로 트렌드가 바뀌었습니다.
서버를 띄울 때 어떤 청소 업체를 부를지 옵션으로 정해줄 수 있다.

  • Parallel GC : 청소부 아저씨가 한 명이 아니라, 여러 명의 직원이 동시에 빗자루를 들고 우르르 치운다. 대청소를 할 때는 여전히 식당 셔터를 다 내리고 다 같이 치워야 한다. 서버의 전체적인 처리량은 좋지만 한번 멈출 때 꽤 부하가 발생한다.
  • G1 GC(Garbage-First GC) : 식당을 Young/Old 두 구역으로만 나누는 게 아니라, 바둑판 처럼 수십 개의 작은 구역을 잘개 쪼갠다. 전체를 다 치우는 것이 아니라 쓱 둘러보고 쓰레기가 가장 많이 쌓인 구역(Garbage-First)만 잽싸게 치우고 빠진다. 덕분에 STW시간이 매우 짧고 일관되게 유지된다.
  • ZGC / Shenandoah GC(최신 하이엔드 기술) : STW 시간을 1ms이하로 줄이는 것을 목표로 하는 기술이다. 손님이 밥을 먹고 직원들이 서빙을 하는 와중에도 식당을 멈추지 않고 청소부들이 밑에서 몰래 몰래 쓰레기를 치운다.
profile
성장중

0개의 댓글