6. JVM(Java Virtual machine) 기본 이론
1. Java 와 C++ 메모리 관리 차이
Native 언어 (C,C++ ... )
Native 언어는 컴파일 후, CPU 가 이해할 수 있는 기계어로 직접 번역되고 이후 실행된다. (때문에, 운영체제 환경에 대해서 직접적인 의존성을 가진다)
윈도우에서 개발된 C, C++ 코드를 리눅스, 유닉스 환경에서 실행시킬 수 없다. 각 운영체제 별로 기계어가 다르기 때문이다. (같은 코드라도 각 운영체제에 맞는 기계어로 번역해주어야 실행이 가능하다)
결론적으로, Native 언어는 운영체제에 직접적으로 요청을 할 수 있기 떄문에, 하드웨어와 직접적으로 연관되어 있다.
JVM 언어
하드웨어를 소프트웨어로 구현한 것을 Virtual Machine 이라고 부르며, 이 Virtual Machine 에서 인식할 수 있는 코드가 Java Byte Code 일 때, JVM(Java Virtual Machine) 이라고 부른다.
결과적으로 JVM 은 소프트웨어로 CPU + 가상메모리(RAM + SSD) 모두가 구현된 것을 의미한다. 여기에 운영체제가 제공하는 기능 일부도 제공한다.
때문에, JVM 을 이해한다는 것은 OS, 컴퓨터구조를 모두 이해한다는 의미와 같다.. (열라 어렵겠지?)
결론적으로, JVM 언어는 소프트웨어와 직접적으로 연관되어 있다.
Java 와 C++ 메모리 관리 차이
JVM 은 User Mode Application 으로 Application Process 범위를 넘어서지 않는다. 쉽게 말해서 JVM 프로세스가 다운된다고 해서 OS 가 다운될 일은 없다!
(물론 JNI 를 통해서 운영체제에 영향을 줄 수 있다. 해당 케이스는 패스~~)
때문에, 보안적 장점이 명확하다! (Native 언어는 운영체제와 직접적인 의존성을 가지기 때문에, 사소한 문제라도 OS 가 다운될 수 있다)
C++
객체에 대한 모든 관리 책임은 개발자에게 있음 (메모리 할당 및 해제)
객체의 생명주기에 개발자가 개입하는 구조
Java
개체 메모리 해제는 전적으로 JVM 의 몫
하지만, 문제 발생시 구조를 알아야 대응 가능
2. JVM 구성요소 정리
Class loader
소프트웨어의 시점은 크게 3가지로 나뉘어 진다.
컴파일
링킹 : 실행에 필요한 라이브러리, 시동코드 등을 컴파일 된 코드에 연결해주는 과정
실행
하지만, JVM 은 컴파일, 실행(링킹 + 실행) 과정으로만 나뉘어진다.
클래스 로더는 여기서 컴파일 된 바이트코드를 로딩하고, 링킹하고, 초기화하는 역할을 담당한다. (매우 중요한 역할이다!)
로딩(Loading) : 바이트코드를 JVM 내 Runtime data area(Method area) 에 배치하는 작업.
링킹(Linking) : 로드 된 클래스 파일들을 사용 가능한 상태로 변경한다.
초기화(initialization) : 정적(
static
) 초기화 블록이나 정적(static
) 변수들을 초기화하는 과정을 의미하고, 클래스가 사용될 때 최초로 한번만 동작한다.
클래스 로딩은 JVM 이 올라올 때 한번에 로딩되는 것이 아니라, 해당 코드가 호출 될 때마다 동적으로 로딩된다. 때문에 앞선 클래스 로딩의 절차적으로 문제가 생긴다면 다음 클래스 로딩에 문제가 생길 수 밖에 없다.
Runtime data area
Method area : 클래스 관련 정보(클래스 메서드, 클래스 정보 등), 문자열 상수를 관리하는 영역
Heap area : GC 가 관리하는 영역
Stack area : 스레드마다 가지고 있는 영역
PC register : 스레드마다 가지고 있는 영역
Native method stack
Execution engine
Interpreter : 기본적으로 바이트코드를 기계어로 번역해서 실행한다.
JIT compiler : 반복되는 바이트코드들은 미리 기계어로 번역해놓고 실행시킨다.
Garbage collector : 힙 메모리를 관리해준다.
3. .class 파일과 바이트 코드 소개
...
4. 클래스 로더 및 로딩 과정
.class
파일은(Java byte code) JVM 내에서 실제로 실행 가능한 코드이다.
클래스 로더 종류
이름을 알고 있는 특정 클래스에 대한 정의(Java Byte code) 를 가져오는 역할을 수행
가져온다는 것을 보았을 때, 네트워크를 통해서 .class 파일을 가져올 수도 있다. (그만큼 확장성이 있다는 뜻?!)
클래스 로더의 종류는 다음과 같다.
Bootstrap ClassLoader
Platform ClassLoader
Application ClassLoader
3가지 클래스 로더에서 클래스를 로드하지 못했을 때,
ClassNotFoundException
이 발생한다.
부트스트랩 클래스 로더
JVM 이 실행될 때 가장 먼저 실행되는 클래스로더이다.
JVM 에서 핵심 라이브러리(rt.jar, tools.jar 내에는
java.lang.*
,java.util.*
등 핵심 클래스가 있다) 로 취급되는 코드들을 로드한다.이 로더는 Java 로 작성된 것이 아니라, Native 코드로 작성되어 있다.
부트스트랩?
부트스트랩이라는 용어는 아무것도 없는 상태에서 스스로 부팅하는 과정을 의미한다.
즉, 시스템의 기초적인 기능을 초기화하고 운영체제를 실행할 수 있도록하는 과정을 의미한다.
플랫폼 클래스 로더
Java9 부터 도입되었으며 부트스트랩 클래스로더와 애플리케이션 클래스로더의 경계선을 명확하게 하기 위해서 존재하는 클래스로더이다.
JVM 핵심 라이브러리 외에 확장 라이브러리들을 로드한다. (Java SE, Java EE API 플랫폼 내 라이브러리..)
애플리케이션 클래스 로더
JVM에서 사용자가 작성한 클래스 + 외부 라이브러리를 로드한다.
가장 최상위에 위치한다.
로딩(Loading)
💡 Java 클래스 로딩 후 사용 과정
JVM에서 .class
파일이 메모리에 올라간 후 사용되는 과정은 크게 아래와 같은 단계를 따른다.
Loading → .class 파일을 찾아 메모리(Runtime data area 내 Method area) 에 올림 이 시점에
java.lang.class
객체가 생성된다!Bootstrap
Platform
Application
Linking
Verification → 바이트코드 유효성 검사
Preparation →
static
필드 기본값(0
,null
) 할당Resolution → 심볼(이름)을 실제 참조로 해석 (필요 시점까지 지연 가능)
Initialization →
static
블록 실행, 상수 할당 등Using
Unloading → GC에 의해 클래스 언로드 (드물게 발생)
✅ 클래스 로더는 실행 중에도 유연한 구조를 가능하게 해준다.Java는 클래스 로더를 통해 클래스의 로딩, 링킹, 초기화를 런타임 시점에 동적으로 수행할 수 있다.
이 덕분에 실행 성능에 초기 비용이 들 수 있지만 (클래스 로더에 너무 많은 기능이 있다),
대신 다음과 같은 유연성과 확장성을 얻는다.
장점1. 🔁 인터페이스만 맞으면, 런타임에 구현 클래스를 교체할 수 있다.
리플렉션을 통해 런타임에 클래스 이름만 바꿔도 구현체를 변경할 수 있음
DI 컨테이너(Spring)나 플러그인 시스템의 핵심 기반
장점2. 🌐 코드를 네트워크나 외부 경로에서 로딩할 수 있다.
실행 중에
.jar
파일을 네트워크에서 다운받아 로딩할 수 있음IDE 플러그인, 게임 모드, 빌드 없이 기능 확장 등 다양한 상황에서 활용됨
장점3. ⏱ 해석(Resolution) 은 필요할 때까지 지연된다.
해석은 클래스 링크(Linking) 과정 중 일부이지만,
호출될 때까지 지연될 수 있음 (
invokevirtual
,invokedynamic
등)이는 다형성과 동적 바인딩을 지원하기 위해 필요한 구조
링킹(Linking)
✅ 1. Verification (검증)
📦 개념
JVM은
.class
파일이 문법적으로 올바른지, 구조적으로 안전한지를 확인합니다.이는 악성
.class
파일이나 잘못된 바이트코드 실행을 막기 위한 "보안 장치" 입니다.
🛠 왜 보안 장치인가?
.class
파일은 사람이 작성한 것이 아니라 컴파일 된 이진 코드입니다. 누군가 바이트코드를 조작하면, 정상적인 Java 코드에서는 할 수 없는 위험한 행동이 가능해질 수 있습니다.예를 들어, 스택을 넘치게 하거나 타입을 속이거나, 존재하지 않는 메서드를 호출하는 식의 공격이 가능하다.
JVM은 이런 비정상적인 바이트코드를 검증(Verification) 단계에서 미리 탐지하고 차단합니다.
JVM 언어는 Native 언어처럼 운영체제에 직접 영향을 줄 수는 없지만, 애플리케이션 내부 데이터를 망가뜨리거나 실행 흐름을 왜곡하는 행위는 막아야 합니다. (
JNI 를 사용해서 직접적으로 운영체제에 요청을 날릴수도 있지만 해당 케이스는 제외)
🛠 주요 검사항목
.class
파일 포맷 및 구조 검증상속 구조, 메서드 시그니처, 접근 제한자 등의 유효성 확인
바이트코드 명령어의 타입 안정성, 스택 정합성, 제어 흐름 안전성 검사
참조하는 클래스가 존재하는지 여부 확인
✅ 2단계. Preparation (준비)
📦 개념 설명
클래스가 인스턴스화되기 전, static 필드들에 대해 메모리 공간을 할당하고 기본값으로 초기화하는 단계이다.
정수형:
0
, 참조형:null
, boolean:false
등.단, 이 메모리는 힙이 아니라 메서드 영역(Method Area) 또는 HotSpot 기준 Metaspace에 할당된다.
🛠 주요 특징
클래스는 아직 초기화되지 않았고,
<clinit>()
메서드도 실행되지 않았다.이 시점에 존재하는 건
java.lang.Class
객체뿐이며, 이는 메타정보 용도로만 존재한다. → 리플렉션, 어노테이션 처리 등에 사용됨.static final
상수는 컴파일 시점에 값이 확정된 리터럴 상수인 경우에만 이 시점에 초기화된다. -> 초기화 할수도 안할수도 있다. (예:static final int A = 42;
→ OK,static final int B = SomeCalc();
→ 초기화 단계까지 대기)
✅ 3단계. Resolution (해석)
📦 개념 설명
.class
파일 내부에는 클래스, 필드, 메서드 등의 참조 정보가 문자열 혹은 인덱스(=심볼) 형태로 저장되어 있다.하지만 JVM이 이 정보를 직접 사용할 수는 없기 때문에, 실제 메모리상의 참조로 변환하는 과정이 필요하다. 이 과정을 해석(Resolution) 이라고 한다.
즉, 해석은 심볼(이름 정보) → 실제 메모리 주소(클래스/메서드/필드 참조)로 매핑하는 작업이다.
이때 변환된 참조는 Metaspace에 올라가 있는 클래스 구조체 등을 통해, 실제 JVM이 사용할 수 있게 된다.
🛠 주요 특징
이 작업은 실행 시점까지 지연될 수 있으며,
특히
invokevirtual
,invokedynamic
같은 바이트코드 명령어와 함께 호출 시점에 수행됩니다.이는 Java의 다형성(오버라이딩, 인터페이스 구현 등)을 구현하기 위해 반드시 필요한 구조입니다.
초기화(Initialization)
✅ 초기화는 언제 발생하는가?
링킹(Linking)이 끝났다고 해서 자동으로 초기화되지 않습니다.
초기화는 반드시 “클래스의 명시적 사용” 이 발생해야만 수행됩니다.
static
필드 읽기
MyClass.value
static
메서드 호출
MyClass.run()
클래스 인스턴스 생성
new MyClass()
Class.forName()
호출
Class.forName("MyClass")
→ 무조건 초기화 발생
서브클래스 초기화 시 부모 클래스도 초기화
new ChildClass()
시 부모도 초기화됨
✅ 초기화 단계에서 수행되는 작업
①
static 필드에 명시된 값 할당
static int x = 42;
②
static 블록 실행
static { System.out.println("Init!"); }
내부적으로는
위 과정을 <clinit>
이라는 특수 메서드에서 실행
JVM이 자동 호출
5. 런타임 데이터 영역
공통 단위
Method area
JVM이 클래스를 로딩할 때, 클래스의 구조 정보(타입, 필드, 메서드, 정적 변수 등)를 저장하는 영역
Java 7까지는
PermGen(Permanent Generation)
영역을 통해 구현Java 8부터는 PermGen이 제거되고,
Metaspace
라는 새로운 구조로 대체됨
PermGen vs Metaspace
PermGen
Java 7까지 사용
JVM 내 별도의 영역
크기가 고정되어 있었고, 설정을 초과할 경우
OutOfMemoryError: PermGen space
발생클래스 수가 많아지면 관리에 한계
Metaspace
Java 8부터 도입
JVM 외부, 네이티브 메모리(Native Memory) 에 존재 (네이티브 메모리 : 운영체제가 관리하는 메모리)
크기가 동적으로 조절 가능
시스템 자원이 허용하는 한 무제한에 가깝게 클래스 정보를 저장할 수 있음
⚙️ Runtime Constant Pool (RCP) 정리
Runtime Constant Pool 이란?
.class
파일은 Constant Pool Table을 포함하고 있으며, 클래스의 필드, 메서드, 상수 등의 심볼릭 정보(예: "클래스 이름", "메서드 이름", 문자열 리터럴 등)를 저장함JVM이 클래스를 로딩하면 이 테이블을 바탕으로 Runtime Constant Pool (RCP) 을 생성함
RCP는 클래스마다 하나씩 존재하며, 바이트코드 실행을 위한 핵심 참조 테이블 역할을 수행함
Runtime Constant Pool의 역할
숫자, 문자열, 심볼릭 참조(클래스 이름, 메서드 서명 등)를 포함
심볼릭 참조는 실행 시점에 실제 메모리 주소로 해석됨 (이 과정을 Resolution이라고 함)
정적 필드 접근, 메서드 호출, 상수 사용 등에 쓰이는 참조 기반 데이터
☑️ Java 버전별 RCP 저장 위치Java 7 이하
RCP는 PermGen(Method Area)에 저장됨
PermGen은 JVM 내부 메모리이며, 크기가 고정되어 있음
GC 대상이기도 하고, 너무 많은 클래스를 로딩하면
OutOfMemoryError: PermGen space
가 발생할 수 있음Java 8 이상
RCP는 Metaspace에 저장됨
Metaspace는 JVM 외부 네이티브 메모리이며, 운영체제가 직접 관리함
크기를 동적으로 조정할 수 있으며, PermGen처럼 고정된 한계가 없음
시스템 메모리가 허용하는 범위 내에서 확장 가능함
☑️ Intern Pool과의 차이
Runtime Constant Pool
모든
.class
파일에서 유래된 상수 정보를 저장바이트코드 해석, 메서드 호출, 상수 값 참조 등에 사용됨
클래스 로딩 시 고정 생성됨 → 실행 중 수정되지 않음
위치는 Java 8 이상에서 Metaspace
Intern Pool
String
객체만을 저장하는 별도의 문자열 상수 풀동일한 문자열을 공유하고 중복 생성을 피하기 위해 사용됨
실행 중에
String.intern()
을 통해 새로운 문자열을 추가할 수 있음위치는 Java 7까지는 PermGen, Java 8 이상에서는 Java Heap 영역
GC의 대상이 됨
Heap area
JVM 프로세스가 사용할 수 있는 데이터 영역의 대부분을 차지한다.
물론 사용하기에는 큰 힙 메모리 영역이 좋을 수 있다.
하지만 그만큼 GC 를 해야하는 범위가 넓어진다는 의미이기 때문에, 성능이 느려질 수 있다.
우리가 인스턴스라고 불리는 것들은 힙 메모리에 저장된다.
JVM 옵션을 통해서 사용하는 힙 메모리 범위를 제한할 수 있다.
쓰레드 단위
Stack area
스택 영역은 일반적으로 사용할 수 있는 공간이 정해져있다.
재귀함수를 잘못 호출하거나, 반복문을 잘못 사용하는 경우
StackOverFlow
가 발생한다.
PC(Program counter) register
각 스레드의 명령어의 위치를 나타내는 포인터를 저장하는 영역
Native method stack
Native Method Stack은 JVM이 Java 코드 외에 C/C++ 등으로 작성된 네이티브 메서드를 호출할 때 사용하는 스택 영역입니다.
JNI, OS 레벨 API 연동 시 활용되며, 일반적인 Java 스택과는 별도로 관리됩니다.
6. JVM 스택 구조
Stack area
JVM Stack은 각 쓰레드별로 할당되며, 메서드 호출마다 Stack Frame이 생성된다.
각 Stack Frame은 메서드 실행에 필요한 정보들을 포함한다:
지역 변수 테이블: 인자, 로컬 변수 저장.
int
, 참조형은 슬롯 1개,long
,double
은 2개 슬롯 사용피연산자 스택: 바이트코드 연산 중간값 저장. JVM은 스택 기반 구조이므로 레지스터 대신 이걸 사용
동적 링크 영역: 심볼릭 참조를 실제 메서드/필드로 연결해 캐싱함
반환 주소: 호출한 메서드로 돌아갈 PC 값
this 참조는 항상 0번 슬롯에 저장 (인스턴스 메서드에 한함)
Stack은 슬롯 수 기준으로 크기를 제한할 수 있으며, 초과 시
StackOverflowError
발생
Stack frame 구조
7. JVM 힙 영역 GC
Heap 영역
GC(Garbage collector) 가 관리하는 메모리 영역으로 Java 에서 사용되는 객체의 인스턴스 및 배열이 저장되는 공간
설정에 따라 크기를 변경하거나 고정할 수 있음
부족 시,
OutOfMemoryError
오류 발생힙 메모리를 늘리면 그만큼 GC 범위가 넓어지는 것을 의미하기 때문에,
OutOfMemoryError
가 발생하더라도 힙 메모리를 늘리는 것은 신중하게 고민해야 한다.
세대별 컬렉션 이론(Generational collection theory) 을 기반을 설계 및 운영
Eden, Survivor
New / Old generation
영구 세대(Metaspace)
JVM Garbage collector
Heap 영역에서 참조되지 않는 개체를 수집 및 제거해 메모리 회수
참조되지 않는 개체라는 것은, 스택 영역 내 참조 변수가 사용되지 않는 상황을 의미한다.
정적 변수에 객체를 생성하게 되면, 프로그램 종료까지 메모리를 회수할 수 없다. (GC 불가)
Minor / Major(Full) GC
GC 수행 시 프로그램 일시 정지 (stop-the-world)
GC 속도
Minor GC 가 보통 1초 이내 완료
Full GC 는 보통 수 초 이상 진행되기도 하며 이 지연 때문에, DB 연결이 끊기는 등 운영문제가 발생할 수 있다.
때문에, Full GC 가 일어나지 않도록 하는게 포인트겠지?!
GC 기술의 시작
Java 에서(거의) 모든 인스턴스는 힙 영역에 저장
GC 기술은 1960년대 만들어진 기술 -> 리스프 언어 창시자 존 맥카시
GC 가 처리해야 할 문제의 핵심 요소
회수 대상 메모리를 판단하는 것
메모리 회수 시점
(구체적인) 메모리 회수 방법
JVM Heap 영역
Last updated