What is JVM?

  • JVM은 Java platform의 주춧돌으로 하드웨어와 운영체제로 부터 독립성을 책임지는 기술 컴포넌트다.
  • WORA(Write Once Run Anywhere)를 구현하기 위한 기반 컴포넌트
  • java Bytecode 를 해석하고 실행한다.
  • 컴파일된 코드는 적은 용량을 갖는다.
  • 사용자를 악의적인 프로그램으로부터 방어한다.
  • JVM은 컴퓨팅 기계을 추상화했다. 진짜 컴퓨터처럼 명령어 셋과 메모리의 다양한 영역을 조작한다.
  • 스택 기반의 가상 머신 : 대표적인 컴퓨터 아키텍처인 x86과 ARM 같은 하드웨어가 레지스터 기반으로 동작하는 데 비해 JVM은 스택 기반으로 동작한다.
  • 심볼릭 레퍼런스 : 기본 자료형(primitive data type)을 제외한 모든 타입(클래스와 인터페이스)을 명시적인 메모리 기반의 레퍼런스가 아닌 심볼릭 레퍼런스를 통해 참조한다.
  • 가비지 컬렉션(garbage collection) : 클래스 인스턴스는 사용자 코드에 의해 명시적으로 생성되고 가비지 컬렉션에 의해 자동으로 파괴된다.
  • 네트워크 바이트 오더(network byte order) : 자바 클래스 파일은 네트워크 바이트 오더를 사용한다. x86 아키텍처의 리틀엔디안이나, RISC(ARM, etc..) 계열 아키텍처가 주로 사용하는 빅 엔디안 사이에서 플랫폼 독립성을 유지하기 위해 네트워크 바이트 오더를 유지해야 한다. 네트워크 전송에 주로 사용되는 네트워크 바이트 오더(=빅 엔디안)을 사용한다.

The Structure of the Java Virtual Machine

JVM

클래스 로더(Class Loader)가 컴파일된 자바 바이트코드를 런타임 데이터 영역(Runtime Data Areas)에 로드하고, 실행 엔진(Execution Engine)이 자바 바이트 코드를 실행한다.

클래스 로더

자바는 동적 로드, 즉 컴파일 타임이 아닌 런타임에 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크하는 특징이 있다. 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더이다.

클래스 로더의 특징

  • 계층 구조 : 클래스 로더끼리 부모-자식 관계를 이루어 계층 구조로 생성된다. 최상위 클래스 로더는 부트스트랩 클래스 로더(Bootstrap Class Loader)이다.
  • 위임 모델 : 계층 구조를 바탕으로 클래스 로더끼리 로드르 위임하는 구조로 동작한다. 클래스를 로드할 때 먼저 상위 클래스로더를 확인하여 상위 클래스 로더에 있다면 해당 클래스를 사용하고, 없다면 로드를 요청받은 클래스 로더가 클래스를 로드한다.
  • 가시성(visibility)제한 : 하위 클래스 로더는 상위 클래스 로더의 클래스를 찾을 수 있지만, 상위 클래스 로더는 하위 클래스 로더의 클래스를 찾을 수 없다.
  • 언로드 불가 : 클래스 로더는 클래스를 로드할 수는 있지만 언로드는 할 수 없다. 언로드 대신, 현재 클래스 로더를 삭제하고 아예 새로운 클래스 로더를 생성하는 방법을 사용할 수 있다.

각 클래스 로더는 로드된 클래스들을 보관하는 네임스페이스(namespace)를 갖는다. 클래스를 로드할 때 이미 로드된 클래스인지 확인하기 위해서 네임스페이스에 보관된 FQCN(Fully Qualified Class Name)을 기준으로 클래스를 찾는다. FQCN이 같더라도 네임스페이스가 다르면, 즉 다른 클래스 로더가 로드한 클래스이면 다른 클래로 간주한다.

클래스 로더의 위임 모델

Class_Loader_Diagram

클래스 로더가 클래스 로드를 요청받으면, ①클래스 로더 캐시, ②상위 클래스 로더, ③자기 자신의 순서로 해당 클래스가 있는지 확인한다. 이전에 로드된 클래스면 로더 캐시를 확인하고, 없으면 상위 클래스 로더를 거슬러 올라가 확인한다. 부트스트랩 클래스 로더를 확인해도 없으면 요청받은 클래스 로더가 파일 시스템에서 해당 클래스를 찾는다.

  • 부트스트랩 클래스 로더 : JVM을 기동할 때 생성되며, Object 클래스들을 비롯하여 자바 API들을 로드한다. 다른 클래스 로더와 달리 자바가 아니라 네이티브 코드로 구현되어 있다.
  • 익스텐션 클래스 로더(Extension Class Loader) : 기본 자바 API를 제외한 확장 클래스들을 로드한다. 다양한 보완 확장 기능 등을 여기에서 로드한다.
  • 시스템 클래스 로더(System Class Loader) : 부트스트랩 클래스 로더와 익스텐션 클래스 로더가 JVM자체의 구성 요소들을 로드하는 것이라 한다면, 시스템 클래스 로더는 애플리케이션의 클래스들을 로드한다고 할 수 있다. 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드한다.

웹 애플리케이션 서버(WAS)와 같은 프레임워크 웹 애플리케이션들, 엔터프라이즈 애플리케이션들이 서로 독립적으로 동작하게 하기 위해 사용자 정의 클래스 로더를 사용한다. 즉, 클래스 로더의 위임 모델을 통해 애플리케이션의 독립성을 보장하는 것이다. 이와 같은 WAS의 클래스 로더 구조는 WAS벤더마다 조금씩 다른 형태의 계층 구조를 가지고 있다.

클래스 로더의 클래스 로드 과정

Class_Load_Process.png

Loading is the process of finding the binary representation of a class or interface type with a particular name and creating a class or interface from that binary representation. Linking is the process of taking a class or interface and combining it into the run-time state of the Java Virtual Machine so that it can be executed.

  • Java Virtual Machine Specification, Java SE 15 Edition -

  • 로드 : 클래스를 파일에서 가져와서 JVM의 메모리에 로드한다.
  • 검증(Verifying) : 읽어 들인 클래스가 자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사한다. 클래스 로드의 전 과정 중에서 가장 까다로운 검사를 수행하는 과정으로서 가장 복잡하고 시간이 많이 걸린다. (참고 : JVM TCK(Technology Compatibility Kit)의 테스트 케이스 중에서 가장 많은 부분이 잘못된 클래스를 로드하여 정상적으로 검증 오류를 발생시키는지 테스트하는 부분이다.)
  • 준비(Preparing) : 클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조를 준비한다.
  • 분석(결단 or 결정)(Resolving) : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다. Resolution is the process of dynamically determining one or more concrete values from a symbolic reference in the run-time constant pool. Initially, all symbolic references in the run-time constant pool are unresolved
  • 초기화 : 클래스 변수들을 적절한 값으로 초기화한다. 즉, static initializer들을 수행하고, static 필드들을 설정된 값으로 초기화한다.

런타임 데이터 영역

Untitled.png

런타임 데이터 영역은 JVM이라는 프로그램이 운영체제 위에서 실행되면서 할당받는 메모리 영역이다. 런타임 데이터 영역은 6개의 영역으로 나눌 수 있다.

  • 스레드당 하나 : PC Register, JVM Stack, Native Method Stack
  • 스레드가 공유 : Heap, Method Area, Runtime Constant Pool

  • PC 레지스터 : PC(Program Counter)레지스터는 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. PC레지스터는 현재 수행 중인 JVM 명령의 주소를 갖는다.

  • JVM 스택 : JVM 스택은 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. 스택 프레임(Stack Frame)이라는 구조체를 저장하는 스택으로, JVM은오직 JVM 스택에 스택 프레임을 추가하고(Push) 제거하는(Pop) 동작만 수행한다. 예외 발생 시 printStackTrace()등의 메서드로 보여주는 Stack Trace의 각 라인은 하나의 스택 프레임을 표현한다.

  • 스택 프레임 : JVM 내에서 메소드가 수행될 때마다 하나의 스택 프레임이 생성되어 해당 스레드의 JVM 스택에 추가되고 메서드가 종료되면 스택 프레임이 제거된다. 각 스택프레임은 지연 변수 배열(Local Variable Array), 피연산자 스택(Operand Stack), 현재 실행 중인 메소드가 속한 클래스의 런타임 상수 풀에 대한 레퍼런스를 갖는다. 지역 변수 배열, 피연산자 스택의 크기는 컴파일 시 결정되기 때문에 스택 프레임의 크기도 메서드에 따라 크기가 고정된다.
    • 지역 변수 배열 : 0부터 시작하는 인덱스를 가진 배열이다. 0은 메소드가 속한 클래스 인스턴스의 this레퍼런스이고, 1부터는 메소드에 전달된 파라미터들이 저장되며, 메소드 파라미터 이후에는 메소드의 지역 변수들이 저장된다.
    • 피연산자 스택 : 메서드의 실제 작업 공간이다. 각 메서드는 피연산자 스택과 지역 변수 배열 사이에서 데이터를 교환하고, 다른 메소드 호출 결과를 추가하거나(Push) 꺼낸다(Pop). 피연산자 스택 공간이 얼마나 필요한지는 컴파일할 때 결정할 수 있으므로, 피연산자 스택의 크기도 컴파일 시에 결정된다.

Untitled1.png

  • 네이트 메소드 스택 : 자바 외의 언어로 작성된 네이티브 코드를 위한 스택이다. JNI(Java Native Interface)를 통해 호출하는 C/C++등의 코드를 수행하기 위한 스택으로, 언어에 맞게 C스택이나 C++스택이 생성된다.

  • 메소드 영역 : 메소드 영역은 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다.(Before initializing) JVM이 읽어들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메소드 정보, Static 변수, 메소드의 바이트코드 등을 보관한다. 메소드 영역은 JVM 벤더마다 다양한 형태로 구현할 수 있으며, 오라클 핫스팟 JVM에서는 Permanent Area(or Generation)이라고 불린다. 메소드 영역에 대한 가비지 컬렉션은 JVM 벤더의 선택 사항 The method area is analogous to the storage area for compiled code of a conventional language or analogous to the “text” segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods used in class and interface initialization and in instance initialization
    • 런타임 상수 풀 : 클래스 파일 포맷에서 constant_pool 테이블에 해당하는 영역이다. 메소드 영역에 포함되는 영역이긴 하지만, JVM 동작에서 가장 핵심적인 역할을 수행하는 곳이기 때문에 JVM 명세에서도 따로 중요하게 기술한다. 각 클래스와 인터페이스의 상수뿐만 아니라, 메소드와 필드에 대한 모든 레퍼런스를 담고 있는 테이블이다. 어떤 메소드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메소드나 필드의 실제 메모리상의 주소를 찾아서 참조한다.
  • 힙 : 인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션 대상이다. JVM 성능 등의 이슈에서 가장 많이 언급되는 공간. 힙 구성과 가비지 컬렉션 방법은 JVM 벤더의 재량이다.

실행 엔진

클래스 로더를 통해 JVM내의 런타임 데이터 영역에 배치된 바이트 코드는 실행 엔진에 의해 실행된다. 실행 엔진은 자바 바이트코드를 명령어 단위로 읽어서 실행한다. CPU가 기계 명령어를 하나씩 실행하는 것과 비슷하다. 바이트코드의 각 명령어는 1바이트짜리 OpCode와 추가 피연산자로 이루어져 있으며, 실행 엔진은 하나의 OpCode를 가져와서 피연산자와 함께 작업을 수행한 다음, 다음 OpCode를 수행하는 식으로 동작한다.

  • 인터프리터 : 바이트코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하나씩 해석하고 실행하기 때문에 바이트코드 하나하나의 해석은 빠른 대신에 인터프리팅 결과의 실행은 느리다는 단점을 갖는다. 바이트 코드라는 언어는 기본적으로 인터프리터 방식으로 동작한다.
  • JIT(Just-In-Time) 컴파일러 : 인터프리터의 단점을 보완하기 위해 도입된 것이 JIT 컴파일러다. 인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고, 이후에는 해당 메서드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행하는 방식이다. 네이티브 코드를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 수행된다.

태그:

카테고리:

업데이트:

댓글남기기