주제
JVM은 어떻게 동작하며 동작하는지 어떻게 확인하고 튜닝할 수 있는가?
동기
자바는 JVM 상에서 동작하는걸로 알고있는데 정확하게 JVM이 언제 동작하는지 궁금해졌다.
그리고 JVM 튜닝을 어떻게 할 수 있고 어떤 옵션을 더해서 튜닝할 수 있는가?
알게된 내용
JVM(Java Virtual Machine)의 동작 과정
1. 컴파일 단계
- 소스 코드 작성: .java 파일로 Java 소스 코드 작성
- javac 컴파일: Java 컴파일러가 소스 코드를 바이트코드(.class 파일)로 변환
- 바이트코드: 플랫폼 독립적인 중간 코드 형태
2. JVM 시작 및 초기화
- JVM 인스턴스 생성: 운영체제가 JVM 프로세스 시작
- 시스템 속성 설정: 힙 크기, 가비지 컬렉터 종류 등 설정
- 부트스트랩 클래스로더 초기화: 핵심 Java 클래스들 로딩 준비
3. 클래스 로딩 과정
클래스로더가 3단계로 동작:
- Loading: .class 파일을 메모리로 읽어옴
- Linking: 바이트코드 검증, 정적 변수 메모리 할당, 심볼릭 참조를 실제 참조로 변환
- Initialization: 정적 변수 초기화, static 블록 실행
4. 메모리 영역 할당
JVM이 다음 메모리 영역들을 생성:
- 힙(Heap): 객체 인스턴스 저장
- 메서드 영역: 클래스 메타데이터, 상수 풀 저장
- 스택(Stack): 메서드 호출 시 로컬 변수, 매개변수 저장
- PC 레지스터: 현재 실행 중인 명령어 위치
- 네이티브 메서드 스택: JNI 호출 시 사용
5. 실행 엔진 동작
- 인터프리터: 바이트코드를 한 줄씩 해석하여 실행
- JIT 컴파일러: 자주 사용되는 바이트코드를 네이티브 코드로 컴파일하여 성능 향상
- 가비지 컬렉터: 사용하지 않는 객체들을 자동으로 메모리에서 제거
6. 프로그램 실행
- main 메서드부터 실행 시작
- 메서드 호출 시 스택에 프레임 생성
- 객체 생성 시 힙 메모리 할당
- 바이트코드 명령어들을 순차적으로 실행
7. 종료
- 모든 non-daemon 스레드 종료 시 JVM 종료
- 또는 System.exit() 호출 시 강제 종료
JVM 개요
Java 설치 = JDK 설치 = JRE + 개발도구 = (JVM + 핵심 라이브러리) + 개발도구
즉, Java를 설치하면 JVM도 자동으로 컴퓨터에 깔리고 OS하에 관리된다.
JVM도 OS에 의해 실행되고 종료되는 SW 중 하나이다.
궁금했던건 Java를 설치하면 JVM이 깔리는건 OK
그렇다면 자바 코드가 실행될 때 JVM이 동작할텐데 인텔리제이 같은 IDE를 실행(초록색버튼)하면 JVM이 OS상에서 돌아가는가?
-> 맞는데 뭔가 부족한 설명
인텔리제이 같은 IDE 자체도 Java 코드로 만들어진 SW
따라서 IDE를 킬때도 JVM 위에서 동작한다.
인텔리제이 초록색 버튼을 눌러서 자바 main 클래스를 실행하는 것도 JVM 위에서 돌아감.
하지만 이 둘은 서로 다른 JVM 프로세스를 의미함. (서로 독립적인 메모리 공간)
이는 jps라는 명령어로 확인 가능
jps는 java process status의 줄임말 -> 현재 실행중인 java 프로세스를 보여주는 명령어 도구

아무것도 켜지 않은 상태에서는 jps 명령어 자체만 java 프로세스로 동작중이다.

IDE를 키고나서 새로운 프로세스(PID)가 생겼다.
더 정확히 알기 위해서 jps -v를 해보자.

프로세스 기본 정보
15644 # 프로세스 ID
exit # 종료 명령어
메모리 설정
-Xms128m # 초기 힙 메모리 128MB
-Xmx2048m # 최대 힙 메모리 2GB (두 번 설정됨)
-XX:ReservedCodeCacheSize=512m # 코드 캐시 512MB
-XX:SoftRefLRUPolicyMSPerMB=50 # Soft Reference 정책
에러 및 덤프 설정
-XX:ErrorFile=C:\Users\Admin\\java_error_in_idea64_%p.log
-XX:HeapDumpPath=C:\Users\Admin\\java_error_in_idea64.hprof
-XX:+HeapDumpOnOutOfMemoryError # 메모리 부족 시 덤프 생성
JVM 성능 및 최적화
-XX:-OmitStackTraceInFastThrow # 빠른 예외에서 스택트레이스 생략 안함
-XX:+IgnoreUnrecognizedVMOptions # 인식 안되는 옵션 무시
-XX:CICompilerCount=2 # 컴파일러 스레드 2개
-XX:CompileCommand=exclude,com/intellij/openapi/vfs/impl/FilePartNodeRoot,trieDescend
개발/디버깅 설정
-ea # assertion 활성화 (enableassertions)
-Dkotlinx.coroutines.debug=off # Kotlin 코루틴 디버그 끄기
시스템 속성들
-Dsun.io.useCanonCaches=false
-Dsun.java2d.metal=true # Metal 그래픽 API 사용 (Mac)
-Djbr.catch.SIGABRT=true
-Djdk.http.auth.tunneling.disabledSchemes=""
-Djdk.attach.allowAttachSelf=true
-Djdk.module.illegalAccess.silent=true
IntelliJ IDEA 전용 설정들
-Dide.show.tips.on.startup.default.value=false # 시작 시 팁 안보이기
-Djava.system.class.loader=com.intellij.util.lang.PathClassLoader
-Didea.vendor.name=JetBrains
-Didea.paths.selector=IntelliJIdea2024.1
-Djb.vmOptionsFile=C:\Users\Admin\AppData\Roaming\\JetBrains\\IntelliJIdea2024.1\idea64.exe.vmoptions
-Djna.boot.library.path=C:\Program Files\JetBrains\IntelliJ IDEA 2024.1/lib/jna/amd64
이쁘게 정리하면 위와 같은 옵션을 볼 수 있다.
결국 이 jps는 인텔리제이 IDE를 의미하는것.
초록색 버튼으로 IDE로 자바 코드를 실행시킨 후에 다시 jps를 해보자.


1. IntelliJ IDEA 자체 (15644)
15644 exit # IntelliJ IDEA 본체
-Xmx2048m # 최대 2GB 메모리
JetBrains, IntelliJIdea2024.1 관련 설정들
2. 컴파일 및 빌드 프로세스 (18144 Launcher)
18144 Launcher # IntelliJ의 빌드/컴파일 담당
-Xmx700m # 최대 700MB 메모리
-Dpreload.project.path=C:/Users/Admin/Desktop/코테 # 프로젝트 경로
-Dcompile.parallel=false # 병렬 컴파일 끄기
3. 실제 내 Java 프로그램 (19044 Main)
19044 Main # 내가 작성한 Java 코드가 실행 중
-javaagent:...idea_rt.jar # IntelliJ 디버깅/모니터링 에이전트
-Dfile.encoding=UTF-8 # 파일 인코딩 UTF-8
4. jps 명령어 자체 (16080)
16080 Jps # 방금 실행한 jps 명령어
인텔리제이로 자바 코드를 실행하면 추가로 프로세스들이 생기는걸 볼 수 있다.
그렇다면 JVM을 튜닝한다의 의미는?
JVM은 Java 코드를 실행하기 위해서 필수적으로 실행되는 SW
JVM은 서로 다른 독립적인 프로세스로 존재 가능함.(튜닝하지 않으면 똑같은 옵션의 서로 다른 JVM으로 동작)
즉, Java 코드를 실행시키기 전에 튜닝한 JVM으로 실행시켜야 한다는 의미
따라서 이는 GUI 환경에서 기존 파일을 더블클릭해서 키는 방법으로는 불가능하다.
CLI 환경에서 byte code를 java 명령어로 실행시킬 때 JVM 옵션을 추가로 주어서 실행 가능하다.
또는 배치 파일을 만들어서 더블 클릭으로 튜닝된 JVM으로 실행할 수 있다.
JVM 튜닝 옵션에는 뭐가 있을까?
1. 메모리 관련 튜닝
힙(Heap) 메모리
-Xms<size> # 초기 힙 크기 (-Xms512m)
-Xmx<size> # 최대 힙 크기 (-Xmx2g)
-XX:NewRatio=<n> # Old:Young 영역 비율 (기본값 2)
-XX:NewSize=<size> # Young 영역 초기 크기
-XX:MaxNewSize=<size> # Young 영역 최대 크기
스택 메모리
-Xss<size> # 스레드 스택 크기 (-Xss1m)
메타스페이스/PermGen
-XX:MetaspaceSize=<size> # 메타스페이스 초기 크기 (Java 8+)
-XX:MaxMetaspaceSize=<size> # 메타스페이스 최대 크기
-XX:PermSize=<size> # PermGen 초기 크기 (Java 7 이하)
-XX:MaxPermSize=<size> # PermGen 최대 크기 (Java 7 이하)
2. 가비지 컬렉터(GC) 튜닝
GC 알고리즘 선택
-XX:+UseSerialGC # Serial GC (단일 스레드)
-XX:+UseParallelGC # Parallel GC (기본값)
-XX:+UseConcMarkSweepGC # CMS GC (deprecated)
-XX:+UseG1GC # G1 GC (Java 9+ 기본값)
-XX:+UseZGC # ZGC (Java 11+)
-XX:+UseShenandoahGC # Shenandoah GC
GC 세부 설정
-XX:MaxGCPauseMillis=<ms> # G1GC 최대 정지 시간
-XX:GCTimeRatio=<n> # GC 시간 비율 목표
-XX:ParallelGCThreads=<n> # 병렬 GC 스레드 수
-XX:ConcGCThreads=<n> # 동시 GC 스레드 수
-XX:G1HeapRegionSize=<size> # G1 힙 리전 크기
3. 성능 최적화
JIT 컴파일러
-XX:+TieredCompilation # 계층형 컴파일 활성화
-XX:TieredStopAtLevel=<n> # 컴파일 레벨 제한
-XX:CompileThreshold=<n> # 컴파일 임계값
-XX:CICompilerCount=<n> # 컴파일러 스레드 수
-XX:ReservedCodeCacheSize=<size> # 코드 캐시 크기
최적화 옵션
-server # 서버 모드 (기본값)
-client # 클라이언트 모드
-XX:+UseCompressedOops # 압축된 포인터 사용
-XX:+UseStringDeduplication # 문자열 중복 제거 (G1GC)
4. 디버깅 및 모니터링
GC 로그
-verbose:gc # 기본 GC 정보
-XX:+PrintGC # GC 정보 출력
-XX:+PrintGCDetails # 자세한 GC 정보
-XX:+PrintGCTimeStamps # 타임스탬프 추가
-XX:+UseGCLogFileRotation # GC 로그 로테이션
-XX:GCLogFileSize=<size> # GC 로그 파일 크기
-Xloggc:<file> # GC 로그 파일 지정
클래스 로딩
-verbose:class # 클래스 로딩 정보
-XX:+TraceClassLoading # 클래스 로딩 추적
-XX:+TraceClassUnloading # 클래스 언로딩 추적
메모리 덤프
-XX:+HeapDumpOnOutOfMemoryError # OOM 시 힙 덤프
-XX:HeapDumpPath=<path> # 힙 덤프 경로
-XX:+PrintStringDeduplicationStatistics # 문자열 중복제거 통계
5. 보안 및 시스템 설정
-Djava.security.policy=<file> # 보안 정책 파일
-Djava.awt.headless=true # Headless 모드
-Dfile.encoding=UTF-8 # 파일 인코딩
-Duser.timezone=Asia/Seoul # 타임존 설정
-Djava.net.useSystemProxies=true # 시스템 프록시 사용
1. 웹 서버용 jvm 튜닝 예시 (대용량 트래픽)
java -Xms4g -Xmx8g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-XX:+UseStringDeduplication -XX:+HeapDumpOnOutOfMemoryError \
MyWebServer
목적: 응답 시간 최적화 + 안정성
- -Xms4g -Xmx8g: 초기 4GB, 최대 8GB
- 웹 서버는 많은 사용자 요청을 동시에 처리
- 충분한 메모리로 OutOfMemoryError 방지
- -XX:+UseG1GC: G1 가비지 컬렉터 사용
- 낮은 지연시간이 중요한 웹 서버에 적합
- 큰 힙에서도 예측 가능한 정지 시간
- -XX:MaxGCPauseMillis=200: GC 정지 시간을 200ms 이하로 제한
- 사용자가 응답 지연을 거의 느끼지 못함
- 웹 페이지 로딩이 끊기지 않음
- -XX:+UseStringDeduplication: 중복 문자열 제거
- 웹에서 URL, 로그, JSON 등 중복 문자열이 많음
- 메모리 사용량 20-30% 절약 가능
- -XX:+HeapDumpOnOutOfMemoryError: 메모리 부족 시 덤프 생성
- 장애 원인 분석을 위한 필수 설정
Spring Boot 웹 애플리케이션, Tomcat 서버, API 서버
2. 배치 처리용 (메모리 집약적)
java -Xms8g -Xmx16g -XX:+UseParallelGC -XX:ParallelGCThreads=8 \
-XX:+UseCompressedOops MyBatchJob
처리량(throughput) 최대화
- -Xms8g -Xmx16g: 초기 8GB, 최대 16GB
- 대용량 데이터 처리 (CSV 파일, DB 데이터)
- 메모리 부족으로 인한 처리 중단 방지
- -XX:+UseParallelGC: 병렬 GC 사용
- 처리량 우선시하는 배치 작업에 최적
- 응답시간보다 전체 작업 완료 시간 중요
- -XX:ParallelGCThreads=8: GC를 8개 스레드로 병렬 실행
- 현재 서버 CPU 코어 수에 맞춰 설정 (예시에서는 8코어 서버)
- GC 시간 단축으로 전체 처리 시간 감소
- -XX:+UseCompressedOops: 64비트에서 포인터 압축
- 메모리 사용량 20-40% 절약
- 더 많은 데이터를 메모리에 올려 캐시 효율 향상
ETL 작업, 로그 분석, 대용량 파일 처리, 데이터 마이그레이션
결론
JVM은 Java 코드를 실행하는 환경 + 환경(상황)에 따라 최적화할 수 있는 구체적인 SW
처음에는 JVM 자체가 추상적인 개념이라고 생각했다.
하지만 실제로는
1. 명령어로 확인 가능한 프로세스들 (jps)
2. 상황에 맞게 튜닝 가능한 설정 옵션 (JVM 튜닝 옵션)
이런 것들을 확인할 수 있는 하나의 SW였다.
이전까지 성능 최적화를 고려할 때, 알고리즘적인 문제나 N+1문제, I/O BOUND와 같은 애플리케이션 레벨에서만 생각했다.
하지만 이제는 JVM 레벨에서의 최적화라는 새로운 관점을 얻게 되었다.
메모리 부족으로 애플리케이션이 느려질 때 단순히 "서버 성능이 안 좋다"고 생각하는 것이 아니라, 구체적으로 힙 메모리를 늘리거나 GC 알고리즘을 바꿔서 해결할 수 있다는 점을 얻었다.
'일기' 카테고리의 다른 글
| [개발일기] 09.19 - 데이터베이스란 무엇인가? (0) | 2025.09.19 |
|---|---|
| [개발일기] 09.17 - JVM의 GC는 어떻게 동작할까? (0) | 2025.09.17 |
| 6.9 - 오늘의 기록 (채팅 이해, 다양한 방법으로 구현) (0) | 2025.06.09 |
| 5.26 - 오늘의 기록 (채팅에 어떤 db를 사용하는게 맞을까?) (0) | 2025.05.26 |
| 5.23 오늘의 기록 (2) | 2025.05.23 |