Java

JVM의 내부 구조와 메모리 구조에 대해

Adrian_Kim 2025. 7. 6. 16:59

Java 코드가 실행 되는 과정

Java 코드는 일반적으로 IDE를 통해 .java 확장자의 파일로 작성된다.

그러나 JVM은 이 파일을 직접 실행하지 않고, 먼저 컴파일 과정을 거쳐 .class 확장자의 바이트코드(Bytecode) 파일로 변환한다.

이 바이트코드는 JVM이 이해할 수 있는 중간 코드로, 플랫폼에 독립적으로 실행될 수 있는 장점이 있다.

예를 들어 Person.java 파일이 있다면, 컴파일을 거쳐 Person.class라는 바이트코드 파일이 생성되고, 이는 JVM 위에서 실행된다.

JVM의 내부 구조

JVM Structure(1)

JVM은 크게 세가지 영역으로 구분할 수 있다.

  • 클래스 로더
  • 실행 엔진
  • 런타임 데이터 영역

이 외에도 바이트 코드 검증기, Java 네이티브 인터페이스(JNI), 가비지 컬렉터 등이 내부 구성 요소로 포합된다.

클래스 로더 (Class Loader)

JVM 내부에서 동적 클래스 적재를 담당하는 구성요소이다.

JVM은 애플리케이션이 실행 될 때, 필요한 클래스 파일들을 런타임 시점에 동적으로 메모리에 로드하는데 이 작업을 클래스 로더가 수행한다.

클래스 로더의 동작은 크게 세 단계로 구분된다. 

  1. 로딩 (Loading) 
  2. 링킹 (Linking)
  3. 초기화 (Initialization)

로딩 (loading)

클래스들을 로드하는 역할을 수행한다. 

클래스 로더는 총 세가지로 구성되어있고 계층적 구조를 지닌다.

  1. Boot Strap ClassLoader 
    • bootstrap classpath에 존재하는 클래스 파일들을 로드 (가장 높은 우선순위를 지님)
  2. Extension ClassLoader
    • ext(jre \ lib) 경로에 존재하는 클래스 파일들을 로드
  3. Application ClassLoader
    • application level의 경로에 존재하는 클래스 파일들을 로드

링킹 (Linking)

클래스를 유요하게 사용 가능한 상태로 만드는 역할을 수행한다.

  • 검증 (verify)
    • 바이트 코드 검증기 (Bytecode verifier)가 클래스의 바이트코드가 JVM명세에 부합하고 보안에 문제가 없는 지 검증 
    • 만약 문제가 있다면 VerificationError가 발생
  • 준비 (perpare)
    • 정적 변수들을 위한 메모리 할당과 기본값으로의 초기화 진행
  • 해결 (resolve)
    • Constant Poll에 있는 symbolic reference들을 실제 메모리 주소 등의 직접 참조로 치환

초기화 (Initialization)

프로그램 코드상에 정의된 static 초기화 블록 또는 static 변수의 초기값 할당이 실행된다.

실행 엔진 (Execution Engine)

실행 엔진은 JVM 내에서 바이트코드를 실제로 실행하는 역할을 맡은 핵심 구성요소이다.

클래스 로더를 통해 메모리에 적재되고, 초기화된 바이트코드가 실행엔진에 전달되면, 실행엔진은 이를 읽어들여 명령어 단위로 해석 및 실행을 하게 된다. 실행 엔진은 크게 인터프리터JIT 컴파일러로 구성되어 서로 협업하여 효율적인 실행일 구현한다.

 

인터프리터 (Interpreter)

바이트코드를 한줄씩 해석하여 즉시 실행하는 방식을 취한다.

시작이 빠르고 메모리 사용이 적지만, 한 메서드 혹은 루프가 반복 실행될 경우 매번 다시 해석을 해야 하므로  실행 속도가 느려지는 단점이 존재한다.

 

JIT 컴파일러 (Just In Time Compiler)

실행 중에 동적으로 바이트코드를 기계어로 컴파일 해 성능을 높인다.

실행 엔진은 바이트 코드를 처음에는 인터프리터로 실행을 하다, 특정 코드가 자주 실행되는 것이 감지되면 해당 부분을 JIT 컴파일하여 네이티브 코드로 변환한다.

컴파일 된 네이티브 코드는 CPU에서 직접 실행되므로, 인터프리터 방식에 비해 훨씬 빠른 속도를 낸다.

Java 네이티브 인터페이스 (JNI)

네이티브 코드와의 상호작용을 담당하는 인터페이스이다.

Java로 작성된 애플리케이션이 C/C++로 작성된 로우레벨 시스템 함수나 기존 라이브러리를 사용할 필요가 있을 때, JNI를 통해 JVM 외부의 네이티브 라이브러리를 호출할 수 있다.

JVM은 실행엔진을 통해 JNI 호출을 만나면, 해당 네이티브 함수를 네이티브 메서드 스택을 이용해 호출하고, 결과를 다시 JVM으로 반환한다. System.arraycopy()가 그 예시이다.

가비지 컬렉터 (Garbage Collector)

힙 메모리를 자동으로 관리해주는 구성요소이다.

더 이상 참조되지 않는 객체를 가비지 컬렉터가 감지하여 힙에서 제거한다.

JVM은 주기적으로 또는 힙이 부족할 때, GC를 실행하여 메모리 누수를 방지하고, 확보된 메모리 공간을 재활용 한다.

System.gc()로 GC 힌트를 줄 순 있지만, GC 실행을 강제할 수는 없으며, JVM이 자체 판단에 따라 수행한다.

JVM의 메모리 구조

JVM은 실행 시 자체적으로 관리하는 몇가지 메모리 영역을 가지고 있다.

각 영역은 특정 목적과 역할을 가지며, 어떤 것은 모든 스레드가 공유하고, 어떤 것은 스레드마다 독립적으로 존재한다.

JVM이 생성하는 주요 메모리 영역에는 힙, 메서드 영역, 스택, PC 레지스터, 네이티브 메서드 스택등이 있다.

힙 (Heap)

모든 객체와 배열이 저장되는 공간이다.

힙은 JVM 내에서 하나만 존재하며, 애플리케이션의 모든 스레드가 공유한다.

JVM이 시작될 때 힙 영역이 생성되고, 종료될 때 파괴된다.

힙은 동적 메모리 할당을 위한 공간으로 런타임에 크기가 변할 수 있다. 힙 내에서는 GC가 주기적으로 동작해, 더 이상 참조되지 않는 객체들을 자동으로 해제한다.

메서드

JVM이 클래스에 대한 메타데이터를 보관하는 메모리 공간이다.

로드된 각 클래스의 Runtime Constant Pool, 필드 및 메서드 정보, 바이트 코드, 정적 변수, 클래스 이니셜라이저 등이 저장된다.

메서드 영역은 JVM당 하나만 존재하며, 애플리케이션의 모든 스레드가 공유한다.

Java 7까지는 메서드 영역을 PermGen(Permanent Generation) 이라는 힙 내 영구 영역으로 운영을 했고

Java 8이후부터는 메타스페이스(Metaspace) 라는 새로운 방식으로 구현했다.

PermGen은 힙에 속하면서  크기가 고정된 영역이었는데, 클래스나 리플렉션 객체를 많이 로드할 경우 OOM이 자주 발생하는 문제가 발생했다. 이에 반해 Metaspace는 JVM 외부의 네이티브 메모리에 할당되며, 기본적으로 자동 확장되어 PermGen보다 메모리 부족 문제가 완화되었다.

스택 (Stack)

Java 스레드마다 개별적으로 존재하는 메모리 영역으로, 메서드 호출과 로컬 변수 저장을 위한 공간이다.

스택은 스택 프레임이라는 단위로 구성되며, 메서드가 호출될 때마다 새로운 프레임이 해당 스레드에 push되고, 메서드가 종료되면 해당 프레임이 pop되어 제거된다. 

각 스택 프레임에는 다음과 같은 내용들이 포함된다.

  • 로컬 변수 배열
    • 메서드에 전달된 매개변수와 메서드 내에서 정의한 지역 변수들이 저장되는 공간
  • 연산 스택
    • 메서드가 연산을 수행할 때 피연산자를 임시로 쌓아두는 스택 공간
  • 프레임 데이터
    • 해당 메서드와 관련된 추가 정보가 포함됨

PC Register

각 스레드마다 하나씩 존재하는 작은 메모리 공간으로, 현재 실행중인 JVM 명령어의 주소 또는 위치를 가리키는 역할을 한다.

구체적으로, 자바 스레드가 현재 실행중인 프레임을 기준으로, 어느 바이트코드를 실행하고 있었는지를 추적하는 용도로 사용한다.

네이티브 메서드 스택 (Native Method Stack)

JNI를 통해 호출되는 네이티브 코드를 위해 마련된 스택 공간이다.

스레드별로 존재하며, 각 스레드가 Java코드가 아닌 네이티브 함수를 호출 할 때 해당 함수의 지역 변수, 매개변수, 반환 주소 등을 보관한다

 

참고 사항

bootstrap classpath

bootstrap classloader가 클래스를 로딩할 때 어디서 클래스를 찾을 지 결정하는 경로
해당 경로에는 JDK의 핵심 클래스들이 들어있는 rt.jar (java8 이전) 혹은 **모듈 파일들 (jmod, java9이후)이 포함됨
확인 방법 - cli에 다음 명령어 기입후 sun.boot.class.path의 값을 확인 해 보면 알 수 있음
java -XshowSettings:properties -version

 

---

Reference 

1. https://gist.github.com/bossiernesto/ccb3a847e83ae0ddf7db0b0eae30870f 

2. https://www.geeksforgeeks.org/java/java-memory-management/

3. https://www.alibabacloud.com/blog/an-illustration-of-jvm-and-the-java-program-operation-principle_600307#:~:text=figure%29,to%20place%20the%20processing%20results

 

'Java' 카테고리의 다른 글

Heap Pollution  (3) 2025.07.15
언체크 예외는 왜 존재할까?  (0) 2025.07.01
Primitive Type vs Reference Type  (3) 2025.06.18
[Java] Optional  (0) 2024.01.26
[Java] Stream  (2) 2024.01.24