본문 바로가기
카테고리 없음

JVM(Java Virtual Machine) 아키텍처 및 동작 방식

by silvertogold100 2025. 8. 3.
반응형

JVM 아키텍처 및 동작 방식

https://www.geeksforgeeks.org/java/how-jvm-works-jvm-architecture/

JVM은 크게 세 가지 하위 시스템으로 구성:

  1. 클래스 로더 서브시스템 (Classloader Subsystem)
  2. 런타임 데이터 영역 (Runtime Data Areas)
  3. 실행 엔진 (Execution Engine)

1. 클래스 로더 서브시스템 (Classloader Subsystem)

자바 프로그램이 실행될 때 .class 파일을 읽어와 JVM 메모리에 로드하는 역할을 합니다. 클래스 로더는 세 단계로 작동함.

  • 로딩 (Loading):
    • .class 파일을 읽어와 바이너리 데이터를 생성하고, 이 데이터를 메소드 영역에 저장.
    • 클래스 이름, 부모 클래스 이름, 인터페이스 이름, 필드, 메소드 등 클래스에 대한 모든 정보가 로드됨.
    • 로딩 시, JVM은 로드된 클래스의 .class 파일에 대한 바이너리 표현을 나타내는 java.lang.Class 타입의 객체를 힙(Heap) 영역에 생성.
  • 링크 (Linking):
    • 로드된 클래스 파일을 실행하기 위해 준비하는 단계입니다.
    • 검증 (Verification): 로드된 .class 파일이 유효하고 안전한지 확인합니다. (예: 바이트코드의 형식이 올바른지, 보안상 문제가 없는지 등) 이 단계에서 문제가 발견되면 VerifyError가 발생할 수 있습니다.
    • 준비 (Preparation): 클래스 변수(static 변수)에 필요한 메모리를 할당하고 기본값으로 초기화합니다. (예: int는 0, boolean은 false, 참조 타입은 null 등). 정적 변수의 실제 초기화(사용자가 지정한 값)는 초기화 단계에서 일어납니다.
    • 해결 (Resolution): 심볼릭 참조(Symbolic References)를 직접 참조(Direct References)로 변환합니다. 심볼릭 참조는 클래스나 메소드의 이름과 같은 추상적인 참조를 말하며, 이를 실제 메모리 주소와 같은 직접적인 참조로 바꾸는 과정입니다.
  • 초기화 (Initialization):
    • 이 단계는 클래스 로딩 과정의 마지막 단계입니다.
    • 클래스 변수(static 변수)에 명시적인 값을 할당하고, 정적 초기화 블록 (static { ... })을 실행합니다.
    • JVM은 스레드로부터 안전한 방식으로 클래스를 초기화합니다 (여러 스레드가 동시에 초기화를 시도하지 않도록 보장).

클래스 로더의 종류:

  • 부트스트랩 클래스 로더 (Bootstrap Classloader): JVM의 핵심 클래스(예: rt.jar에 있는 java.lang.* 등)를 로드합니다. $JAVA_HOME/jre/lib 디렉토리에 있는 클래스들을 로드합니다.
  • 확장 클래스 로더 (Extension Classloader): 확장 디렉토리(예: $JAVA_HOME/jre/lib/ext)에 있는 클래스들을 로드합니다.
  • 애플리케이션/시스템 클래스 로더 (Application/System Classloader): 클래스패스(Classpath)에 지정된 애플리케이션 클래스들을 로드합니다. 이것은 개발자가 작성한 클래스들이 로드되는 주된 로더입니다.

2. 런타임 데이터 영역 (Runtime Data Areas)

JVM이 프로그램을 실행하면서 사용하는 메모리 영역입니다. 각 스레드마다 고유한 영역과 공유 영역 존재

  • 메소드 영역 (Method Area):
    • 모든 JVM 스레드가 공유하는 영역
    • 클래스 구조(런타임 상수 풀, 필드 및 메소드 데이터, 메소드 및 생성자의 코드 등)와 정적 변수가 저장됨.
    • 하나의 JVM 당 하나의 메소드 영역만 존재합니다.
  • 힙 영역 (Heap Area):
    • 모든 JVM 스레드가 공유하는 영역
    • new 키워드를 사용하여 생성된 모든 객체(Object)와 배열(Array)이 저장됨.
    • 가비지 컬렉터(Garbage Collector)의 주된 대상이 되는 영역
  • 스택 영역 (Stack Area):
    • 각 JVM 스레드마다 고유하게 생성되는 영역
    • 메소드가 호출될 때마다 스택 프레임(Stack Frame)이 생성되어 이 영역에 푸시(push)됩니다.
    • 각 스택 프레임은 다음을 포함합니다:
      • 지역 변수 배열 (Local Variable Array): 메소드 내의 지역 변수와 매개변수를 저장
      • 피연산자 스택 (Operand Stack): 연산을 위한 피연산자를 임시로 저장하는 스택
      • 프레임 데이터 (Frame Data): 메소드 호출에 필요한 기타 정보(예: 상수 풀 참조, 예외 처리 정보 등)를 저장함.
    • 메소드 실행이 완료되면 해당 스택 프레임은 팝(pop)되고 제거됨.
  • PC 레지스터 (PC Registers):
    • JVM 스레드마다 고유하게 생성
    • 현재 실행 중인 JVM 명령어의 주소를 저장합니다.
    • 메소드가 JVM 메소드인 경우 명령어의 주소를 저장하고, native 메소드인 경우 그 값을 정의하지 않음.
  • 네이티브 메소드 스택 (Native Method Stacks):
    • 각 JVM 스레드마다 고유하게 생성됩니다.
    • JVM이 Java 코드가 아닌 C/C++과 같은 네이티브 메소드를 호출할 때 사용되는 스택입니다. (JNI - Java Native Interface를 통해 호출될 때 사용)
    3. 실행 엔진 (Execution Engine)
    • 인터프리터 (Interpreter):
      • 바이트코드를 한 줄씩 읽어와 기계어로 번역하고 바로 실행
      • 장점: 빠른 시작 시간.
      • 단점: 동일한 코드가 반복적으로 호출될 때 매번 번역해야 하므로 전체 실행 속도는 느릴 수 있음.
    • JIT (Just-In-Time) 컴파일러:
      • 인터프리터의 단점을 보완하기 위해 도입되었습니다.
      • 자주 사용되는(핫 스팟) 바이트코드 블록을 통째로 읽어와 한 번에 네이티브 기계어 코드로 컴파일하고 캐싱합니다.
      • 이렇게 컴파일된 코드는 다음에 동일한 블록이 실행될 때 다시 번역할 필요 없이 바로 실행될 수 있어 성능을 크게 향상시킵니다.
      • JIT 컴파일러는 HotSpot이라는 기술을 사용하여 자주 실행되는 코드를 식별합니다.
    • 가비지 컬렉터 (Garbage Collector):
      • 힙 영역에서 더 이상 참조되지 않는(사용되지 않는) 객체를 자동으로 찾아 메모리에서 제거하여 메모리 누수를 방지하고 자원을 효율적으로 관리함.
      • 개발자가 명시적으로 메모리를 해제할 필요가 없습니다.
  • 런타임 데이터 영역에 로드된 바이트코드(.class 파일)를 실행하는 역할

참조 타입(reference type)

  • 객체의 번지를 참조하는 타입
  • 참조 타입으로 선언된 변수(a.k.a 참조변수) 는 객체가 생성된 메모리 번지를 저장
    • 내부 포인터로 JVM이 메모리 주소를 변경할 권한을 가짐. 프로그래머가 핸들링할 필요 없음. 위임한 것임.
    • . : 주소값을 참조한다. Ex. i.cv : 참조변수 i의 주소값을 참조해서 cv 라는 변수에 접근
  • 책 : JVM 밑바닥까지 파헤치기
  • JVM은 운영체제에서 할당받은 메모리 영역을 메소드, 힙, 스택 영역으로 구분해서 사용
  • 메소드 영역 : 바이트코드 파일을 읽은 내용이 저장되는 영역, static 으로 만들어지는 영역.
  • 힙 영역 : 객체가 생성되는 영역. 객체의 번지는 메소드 영역과 스택 영역의 상수와 변수에서 참조
  • 스택 영역 : 메소드를 호출할 때마다 생성되는 프레임(main(), println() 등의 메소드 프레임 안의 지역변수, 매개변수 등)이 저장되는 영역
    • 위 코드의 수행 과정 정리
    1. InitTest 가 메모리에 로드되면서 클래스 정적 변수 cv가 메소드 영역에 생성되고, 기본 값인 0으로 자동적으로 초기화된다. (InitTest 클래스가 메모리 주소 0xaaaa에 생성되었다고 하자)
    2. 명시적 초기화에 의해 cv가 1로 초기화된다.
    3. static 초기화 블럭에 의해서 cv 에 2가 저장된다. 이처럼 클래스 변수의 명시적 초기화와 클래스 초기화 블럭은 클래스가 메모리에 로드될 때 단 한번만 수행된다.
    4. 클래스가 메모리에 로드된 다음, java.exe는 InitTest 클래스의 main 메소드를 호출한다. 호출 스택에 main 메소드를 위한 공간이 마련된다. (메인 메소드를 위한 스택 프레임(Stack Frame))
    5. 참조변수 i와 InitTest 인스턴스가 생성된다. 이때, 인스턴스 변수 iv가 생성된다. iv는 기본값인 0으로 자동적으로 초기화 된다. 모든 인스턴스는 자신을 생성한 클래스의 주소를 갖고 있다.
    6. 명시적 초기화에 의해 iv 가 1로 초기화 된다.
    7. 인스턴스 초기화 블럭에 의해 iv 에 2가 저장된다.
    8. 생성자가 호출되어 iv 에 3이 저장된다. 인스턴스 초기화블럭이 생성자보다 먼저 수행된다는 것을 기억하자.
    9. 대입연산자에 의해 생성된 InitTest 인스턴스의 주소가 i에 저장된다.
    10. cv 의 값을 화면에 출력한다. (2)
    11. iv 의 값을 화면에 출력한다 (3)
    12. 메인 메소드의 모든 문장에 수행되었으므로 전체 프로그램이 종료된다.
  • class InitTest { static int cv = 1; int iv = 1; static { cv = 2; } InitTest() { iv = 3; } { iv = 2} public static void main(String[] args) { InitTest i = new InitTest(); System.out.println(cv); System.out.println(i.iv);
  • JVM이 객체를 생성할 때 다음과 같은 순서로 초기화를 진행.
    1. 기본값 초기화: 모든 인스턴스 변수는 기본값(int의 경우 0)으로 초기화됩니다.
    2. 명시적 초기화: int iv = 1;과 같이 선언 시 할당된 값이 초기화됩니다. (iv는 1이 됨)
    3. 인스턴스 초기화 블록 실행: { iv = 2; } 블록이 실행됩니다. (iv는 2가 됨)
    4. 생성자 실행: InitTest() 생성자가 실행됩니다. 생성자 내부의 iv = 3;이 실행됩니다. (iv는 최종적으로 3이 됨)
    따라서 인스턴스 초기화 블록의 위치가 생성자보다 앞에 있든 뒤에 있든, 실행 순서는 항상 인스턴스 초기화 블록이 생성자보다 먼저.
  • itar 을 넣으면 for문 자동 완성 : Iterate elements of Array
  • iter 을 넣으면 향상 된 for문 으로 자동 완성 : Iterate Iterable or Array
    • 향상된 for문 (for-each): 배열의 모든 원소를 읽기 전용으로 순회할 때 편리합니다. 각 원소의 값을 읽어와 처리하는 용도
    • 향상된 for문은 내부적으로 **반복자(Iterator)**를 사용하거나, 배열의 경우 인덱스를 사용하여 각 원소의 '복사본'을 가져와서 처리
  • 스택 영역의 참조 변수(Ex. int[] arr 에서 arr)에는 힙 영역에 있는 배열 객체(Ex. new int[5])의 '시작 메모리 주소'(Ex. 주소 0xABC123) 가 저장됩니다.
  • System.out.println(arr)와 같이 배열 객체를 직접 출력할 때 나타나는 값은 해당 배열 객체의 해시 코드(hashCode)를 16진수로 변환한 값입니다.
    • 정확히 말하면, Object 클래스의 toString() 메소드가 호출되고, 이 메소드는 기본적으로 getClass().getName() + "@" + Integer.toHexString(hashCode()) 형식의 문자열을 반환합니다.
    • 예시: [I@1b6d3586
      • [I: 배열의 타입을 나타냅니다. [는 배열을, I는 int 타입 배열임을 의미합니다. (참고: [Ljava.lang.String;는 String 배열을 의미합니다)
      • @: 구분자입니다.
      • 1b6d3586: 해당 배열 객체의 해시 코드를 16진수로 표현한 값입니다.
반응형