주제

JVM

세부 주제

JVM 아키텍처, 클래스 로더, 런타임 데이터 영역


Java 프로그램 실행 흐름

아키텍처


JDK

💡

JDK는 Java Application을 개발하고 실행하는 데 필요한 도구와 라이브러리를 포함하는 패키지이다.

  • JRE
  • Compiler (javac)
  • Archiver (jar)
  • Documentation Generator (javadoc)

JRE

💡

JRE는 Java Application을 실행하기 위한 최소 요구 사항제공한다. 자바로 만들어진 프로그램을 실행하는 데 필요한 라이브러리, 각종 API와 JVM포함되어 있다.

  • JVM
  • Java core packages
  • classes
  • supporting files

JVM

💡

JVM은 Java 바이트 코드실행할 수 있는 런타임 환경을 제공하는 규격이다. JVM이 Java 바이트 코드를 실행하고, 하드웨어가 이해할 수 있는 기계어로 변환할 수 있는 플랫폼을 만든다.

  • Class Loader Subsystem
    • .class 파일을 읽어서 메모리(Runtime Data Area)에 적재
  • Runtime Data Areas
    • JVM이 프로그램을 실행하면서 사용하는 메모리 공간
  • Execution Engine (Interpreter, JIT Compiler, GC)
    • 메모리에 로드된 바이트코드를 실제로 실행하는 핵심 엔진

런타임 데이터 영역

💡

JVM은 자바 프로그램을 실행하는 동안 필요한 메모리를 몇 개의 데이터 영역으로 나누어 관리한다.

영역들각자의 목적생성/삭제 시점이 있다. 어떤 영역은 가상 머신 프로세스의 시작과 동시에 만들어지며, 어떤 영역은 사용자 스레드의 시작/종료에 맞춰 생성되고 삭제된다.

1. PC : 프로그램 카운터

💡

pc는 작은 메모리 영역으로, 현재 실행 중인 스레드의 ‘바이트코드 줄 번호 표시기’라고 생각하면 좋다.

  • 적은 CPU를 여러 스레드가 사용할 때, 스레드 전환을 위한 정보를 저장하기 위해 스레드마다 독립적으로 존재하는 공간임
  • 스레드가 자바 메소드를 실행 중일 때는 실행 중인 바이트코드 명령어의 주소가 PC에 기록됨
    • 하지만, 네이티브 메소드를 실행 중일 때의 PC 값은 Undefined, ‘정의되지 않음’임

PC는 유일하게 <<자바 가상 머신 명세>>에서 OutOfMemoryError 조건이 명시되지 않은 영역이다.

2. JVM Stack

💡

각 함수가 호출될 때마다 JVM은 스택 프레임을 만들어 지역 변수 테이블, 피연산자 스택, 동적 링크, 메소드 반환값 등의 정보를 저장한다.

스택 메모리 영역에서는 아래 두 가지 오류가 발생할 수 있다.

  1. 스레드가 요청한 스택의 깊이가 JVM이 허용하는 깊이를 넘었을 때 : StackOverFlowError
  2. 스택 용량을 동적으로 확장 가능한 머신에서 스택 확장 시점에 여유 메모리가 충분하지 않다면 : OutOfMemoryError

3. Native Method Stack

💡

JVM Stack과 매우 비슷한 역할을 한다. 차이점이 있는데, JVM은 자바 메소드(바이트 코드)를 실행할 때 사용하고, Native Method Stack은 네이티브 메소드를 실행할 때 사용한다는 점이다.

  • 구현이 자유롭기 때문에, 여러 형태로 존재함
  • HotSpot JVM에서는 JVM Stack과 합쳐서 관리함
  • JVM Stack처럼 두 가지 오류를 던질 수 있음

4. Java Heap

💡

자바 힙은 자바 애플리케이션이 사용할 수 있는 가장 큰 메모리다. 모든 스레드가 공유하며, 가상 머신이 구동될 때 만들어진다.

  • 유일한 목적은 객체 인스턴스를 저장하는 것이고, 거의 모든 객체 인스턴스가 이 영역에 할당됨
  • 가비지 컬렉터가 관리하는 메모리 영역
    • new, old, eden … 등 여러 영역으로 구분되곤 함
    • 이렇게 구분하는 목적은 오직 메모리 회수와 할당을 더 빠르게 하기 위함임

5. Method Area

💡

메소드 영역은 JVM이 읽어 들인 타입 정보, 상수, 정적 변수 그리고 JIT 컴파일러가 컴파일한 코드 캐시 등을 저장하는 데 이용된다. 메소드 영역도 모든 스레드가 공유하며, 가상 머신이 구동될 때 만들어진다.

  • 자바 힙과 마찬가지로 연속될 필요가 없으며, 크기를 고정할 수도 있고, 확장 가능하게 만들 수도 있음
  • 메소드 영역도 GC의 대상이 될 수 있지만, 잘 쓰이지 않음

6. 런타임 상수 풀

💡

런타임 상수 풀메소드 영역의 일부다. 상수 풀 테이블에는 클래스 버전, 필드, 메소드, 인터페이스클래스 파일에 포함된 설명 정보에 더해 컴파일 타임에 생성된 다양한 리터럴과 심벌 참조가 저장된다.

  • 클래스 파일의 상수 풀과 비교해 런타임 상수 풀은 동적이라는 특징이 있음
  • 자바 언어에서 상수가 꼭 컴파일 타임에 생성될 필요는 없음
  • 런타임 상수 풀도 확장될 수는 있으나, 메소드 영역에 속하므로 그 크기까지만 확장 가능함

7. 다이렉트 메모리

💡

다이렉트 메모리는 JVM 런타임에 속하지 않지만, 자주 쓰이는 메모리이다. OutOfMemoryError의 원인이 될 수 있다.

  • NIO는 힙이 아닌 메모리를 직접 할당할 수 있는 네이티브 함수 라이브러리를 이용하며, 이 메모리에 있는 DirectByteBuffer 객체를 통해 작업을 수행할 수 있음
  • 즉, 자바 힙과 네이티브 힙 사이에서 데이터를 복사해 주고받지 않아도 되므로 성능이 좋음

클래스 파일

💡

클래스 파일은 JVM에서 실행할 수 있는 Java Byte Code를 포함한 .class 확장자 파일이다.

JVM 위에서 돌아가는 언어라면, JVM은 자바를 포함하여 어떠한 프로그래밍 언어에도 종속되지 않는다. ‘클래스 파일’이라는 특정한 바이너리 파일 형식에만 의존한다.

클래스 파일에는 자바 가상 머신 명령어 집합과 심벌 테이블, 그리고 몇 가지 추가 정보가 담긴다.

  • <<자바 가상 머신 명세>>는 클래스 파일이 몇 가지 필수 구문을 갖추고 특정 구조를 따르도록 제약하고 있음
    • 바이트 코드 형식은 튜링 완전하므로 JVM에서 어떠한 언어도 표현할 수 있도록 보장함
  • 다른 언어에서도 JVM하드웨어 독립 범용 실행 플랫폼으로 활용할 수 있고, 클래스 파일프로그램을 전달하는 매체로 이용할 수 있음
  • 즉, 소스 코드를 바이트 코드로 바꿔, ‘클래스 파일’로 저장할 수 있음

자바 언어의 다양한 구문, 키워드, 상수, 변수, 연산 기호는 결국 바이트 코드 명령어 조합으로 표현된다. 그런데, 바이트 코드 명령어의 표현 능력이 자바 언어보다 뛰어나기 때문에 자바 언어에서 효과적으로 표현하지 못하는 개념(언어 특성)도 바이트 코드에서는 효과적으로 표현할 수 있다. 이렇게 자바가 지원하지 않는 개념도 다른 언어에서 제공할 수 있는 길을 열어 두었다.

1. 클래스 파일의 구조

클래스 로딩 메커니즘

💡

컴파일 시 링크까지 해야 하는 언어들과 달리 자바는 클래스 로딩, 링킹, 초기화가 모두 ‘프로그램 실행 중’에 일어난다.

자바가 동적 확장 언어 기능을 제공할 수 있는 것은 런타임에 이루어지는 동적 로딩과 동적 링킹 덕분이다.

  • 인터페이스 중심으로 작성해 두면 실제 구현 클래스를 결정하는 일을 실행 시까지 미룰 수 있음

두 가지 규칙

  1. 실질적으로 클래스 파일 하나는 자바 언어에서 말하는 클래스 또는 인터페이스 하나를 나타낸다.
  2. ‘클래스 파일’이라고 함은 디스크에 존재하는 파일이 아닌 일련의 바이너리 바이트 스트림으로 칭한다.
    1. 즉, 클래스 파일은 SSD, 네트워크, DB, Memory 어디에도 존재하고, 동적으로 생성되어도 된다.

1. 클래스 로딩 시점

💡

JVM 메모리에 로드되는 걸 시작으로, 다시 언로드 될 때까지 아래 그림의 과정을 거친다. 이 중 검증, 준비, 해석 단계를 묶어 링킹이라고 한다.

  • 로딩, 검증, 준비, 초기화, 언로딩은 반드시 순서대로 진행해야 함
  • 반면, 해석 단계는 때에 따라 초기화 후에 시작할 수 있음
    • 자바 언어의 런타임 바인딩을 지원하기 위한 것
    • ‘해석’은 ‘심벌 참조’가 실제 사용될 때 진행되기 때문에 초기화 이후에 시작될 수 있음
  • 참고: 단계별 순서의 기준은 ‘진행’이나 ‘완료’가 아니라 ‘시작’ 시점
    • 때로는 병렬로 진행되기도 함

2. 로딩

💡

자바 가상 머신은 로딩 단계에서 다음 세 가지 작업을 수행해야 한다.

  1. 완전한 이름을 보고 해당 클래스를 정의하는 바이너리 스트림가져온다.
  2. 바이트 스트림으로 표현된 정적인 저장 구조를 메소드 영역에서 사용하는 런타임 데이터 구조로 변환한다.
  3. 로딩 대상 클래스를 표현하는 java.lang.Class 객체힙 메모리에 생성한다. 이 객체는 애플리케이션이 메소드 영역에 저장된 타입 데이터를 활용할 수 있게 하는 통로가 된다.

로딩 단계가 끝나면 바이너리 스트림은 자바 가상 머신이 정의한 형식에 맞게 메소드 영역에 저장된다.

  • 타입 정보를 메소드 영역에 올바르게 저장한 다음에는 해당 java.lang.Class 객체를 자바 힙에 초기화 함
  • Class 객체는 프로그램에서 메소드 영역 안의 타입 데이터에 접근하기 위한 통로 역할을 함
  • 로딩과 링킹 단계의 일부 동작은 서로 중첩되어 진행됨

3. 링킹

💡

링킹 과정은 로드된 클래스를 JVM의 런타임 상태와 결합하여 실행 가능하게 만드는 과정으로, Verification(검증), Preparation(준비), Resolution(해석) 세 단계로 구성된다.

[검증]

검증의 목적두 가지다.

  1. 클래스 파일의 바이트 스트림에 담긴 정보가 규정한 모든 제약을 만족하는지 확인한다.
    1. 구조적으로 올바른지 확인하는 과정
  2. 이 정보를 코드로 변환해 실행했을 때 자바 가상 머신 자체의 보안을 위협하지 않는지 확인한다.

검증은 크게 네 가지거쳐 완료된다.

  1. 파일 형식 검증

바이트 스트림이 클래스 파일 형식에 부합하고 현재 버전의 가상 머신에서 처리될 수 있는지 확인한다. (매직 넘버, 버전 정보 등)

이 외에도 많지만 생략 ..

이 검증은 바이너리 바이트 스트림을 대상으로 이루어지며, 검증을 통과하면 바이트 스트림이 자바 가상 머신 메모리 중 메소드 영역에 저장된다.

따라서 이어지는 세 단계는 모두 메소드 영역에 저장된 구조가 대상이다. 바이트 스트림을 직접 읽지 않는다는 뜻이다.

  1. 메타데이터 검증

바이트 코드로 설명된 정보의 의미를 분석하여 서술된 정보가 요구 사항을 충족하는지 확인한다.

  • 주된 목적은 클래스의 메타데이터 정보에 대한 의미론적 검증
  1. 바이트 코드 검증

주된 목적은 데이터 흐름과 제어 흐름을 분석하여 프로그램의 의미가 적법하고 논리적인지 확인하는 것이다.

  1. 심벌 참조 검증

가상 머신이 심벌 참조를 직접 참조로 변환할 때 수행된다.

  • 이 변환은 링킹의 세 번째 단계인 해석 단계에서 일어남
  • 해당 클래스 자체를 제외한 모든 정보를 확인하는 것으로 보면 됨
    • 현재 클래스가 참조하는 특정 외부 클래스, 메소드, 필드 등에 접근할 권한이 있는지 봄

주된 목적은 해석을 제대로 수행할 수 있는지 확인하는 것이다.

검증 단계는 매우 중요하지만 필수는 아니다. 그래서 때론 건너뛰기도 한다.

[준비]

준비는 클래스 변수(정적 변수)를 메모리에 할당하고 초깃값을 기본값으로 설정하는 단계다.

  1. 인스턴스 변수가 아닌 클래스 변수만 할당된다. 인스턴스 변수는 객체가 인스턴스화될 때 객체와 함께 자바 힙에 할당된다.
  2. 준비 단계에서 클래스 변수에 할당하는 초깃값은 해당 데이터 타입의 제로값이다.
    1. ex. public static int value = 123;
      1. 이렇게 되어 있더라도, 준비 단계를 마친 직후 할당된 초깃값은 123이 아닌 0임
      2. 123을 할당하는 일은 ‘클래스 초기화 단계’에 가서 이루어짐
    2. ex. static final int v = 1234;
      1. 두 번째 예시와 같은 컴파일 타임 상수는 준비 단계에서 바로 1234로 초기화 됨
        1. 컴파일 타임 상수는 애초에 클래스 파일에 그 값으로 기록되기 때문
      2. static 없는 final은 x

static과 꽤 관련이 있는 것을 알 수 있는데, static은 객체 없이 사용할 수 있어야 된다. 하지만, 값이 할당되어 있지 않기에 클래스 로딩을 촉발한다.

반면, static final은 이미 클래스 파일에 그 값이 고정되어 있다. 따라서 사용 시에 클래스 로딩을 촉발하지 않는다.

[해석]

💡

해석 단계는 심벌 참조를 직접 참조로 변환하는 과정이다. 심볼 참조는 “java/lang/String”같은 이름이고, 직접 참조는 실제 메모리 주소이다.

  • JVM은 심벌 참조가 실제로 사용될 때 해석을 진행함

4. 초기화

💡

초기화 단계에서는 static 블록과 static 변수를 초기화한다.

  • 초기화 트리거 (로딩 포함)
    1. new로 객체를 생성할 때
      1. 클래스가 로드 + 초기화 됨

      Animal a = new Animal();
    • 생성자 호출 전에 클래스 초기화가 보장됨
      • 참고 : Animal 타입의 a라는 변수는 스택, new 연산자로 생성된 객체 인스턴스는 힙에 저장됨
    1. static 필드에 접근할 때

      System.out.println(MyClass.staticValue);
      static int staticValue = 10; // 초기화 시점에 실행됨
    2. static 함수를 호출할 때

      Animal.cry();
    3. 리플렉션으로 클래스를 사용할 때

      Class.forName("com.example.Animal");
    • 기본적으로 초기화까지 수행
    • Class.forName(name, false, loader) → 초기화 제외 가능
    1. 해당 클래스가 main 메소드를 가진 경우

    2. 부모 클래스 초기화가 필요한 경우

      class Parent {
          static {
              System.out.println("Parent init");
          }
      }
      
      class Child extends Parent {
          static {
              System.out.println("Child init");
          }
      }
      
      Child c = new Child();

JVM은 필요할 때만 클래스 파일을 메모리에 올린다.(Lazy Loading)

클래스 로더

💡

클래스 로더는 당연하게 클래스를 로딩한다. 그 외에도 각 클래스 로더는 독립적은 클래스 이름 공간을 지니므로 클래스 로더를 빼놓고 특정 클래스가 JVM에서 유일한지 판단할 수 없다.

  • 같은 클래스도 다른 클래스 로더로 로딩하면 다른 걸로 인식함
    • equals()같은 느낌으로 이해

0. 부모 위임 모델

💡

JVM 관점에서 클래스 로더의 종류는 딱 두 가지이다.

  1. Bootstrap Class Loader : JVM 자체의 일부이다.
  2. 그 외 모든 클래스 로더 : java.lang.ClassLoader를 상속하며, JVM 외부에 존재한다.
    1. 특히, 더 잘게 나누어 대표적으로 Platform Class Loader, Application Class Loader로 구분할 수 있다.

  • 여기서 부모-자식 관계는 상속보다 주로 컴포지션 관계로 구현하여 부모의 코드를 재사용함
  • 위임 모델에서의 클래스 로딩
    1. 클래스 로딩을 요청받은 클래스 로더는 처음부터 클래스 로드를 시도하지 않는다.
    2. 그 대신 상위 클래스 로더로 요청을 위임한다.
      1. 따라서 모든 로드 요청이 우선 최상위인 Bootstrap Class Loader로 넘겨진다.
    3. 상위 로더의 관할이 아니라면 하위 로더로 다시 넘긴다.

[장점]

  • 자연스럽게 자바 클래스들이 클래스 로더의 계층 구조를 따르게 됨
  • 즉, 한 클래스가 모두 동일한 클래스임이 보장됨
    • A 클래스의 로딩은 자연스레 최상위 클래스 로더부터 적절한 클래스 로더를 통해 로딩
    • 아무리 많은 클래스 로더가 있어도 위에부터, 맞는 걸 찾자마자 로딩하므로 같은 클래스 로더에 의한 로딩이 보장됨

1. Bootstrap Class Loader

💡

JVM에 내장되었으며, 네이티브 코드로 구현된 클래스 로더로, 핵심 Java 클래스를 로드한다. java.lang, java.util 등이 있다.

  • 지정한 경로에 위치한 파일들과 JVM이 클래스 라이브러리로 인식하는 파일들을 로드하는 책임
  • 부트스트랩 클래스 로더는 자바 프로그램에서 직접 참조할 수 없음
    • 따라서 Bootstrap Class Loader 참조 시 null 반환
    • 마찬가지로, 여기로 위임하고자 한다면 참조 대신 null 사용

2. Platform Class Loader

💡

플랫폼 클래스를 로드한다.

  • JVM 시작에는 필요하지 않지만, 표준 API로 제공하는 것들을 로드함
    • java.sql, java.xml …

3. Application Class Loader

💡

classpath에 있는 클래스들을 로드한다.

  • 내 코드 + 외부 라이브러리(classpath)를 로드하는 것

클래스 언로딩

💡

클래스가 언로딩되려면 아래 조건을 모두 충족해야 한다.

  1. 해당 클래스의 모든 인스턴스가 GC됨
  2. 해당 클래스를 로드한 ClassLoader가 GC됨
  3. 해당 클래스의 Class 객체에 대한 참조가 없음
  • 위 조건을 보면 Bootstrap/Platform/Application ClassLoader로 로드된 클래스는 사실상 언로딩이 불가능함

0개의 댓글