업데이트:

10 분 소요

Java Virtual Machine

개요

JVM은 자바 애플리케이션을 동작시키기 위한 런타임 엔진이다.

이는 자바 코드에서 의 메인 메서드라고 불리는 것으로, JVM(Java Virtual Machine)은 JRE(Java Runtime Environment)의 한 부분이다.

JVM은 추상적 가상 컴퓨팅 머신 이다. JVM은 안에 있는 것을 수행하도록 짜여진 프로그램에게 기계(machine)처럼 보인다.

이런 방식으로, Java 프로그램은 동일한 인터페이스 및 라이브러리 세트에 작성된다.(This way, Java programs are written to the same set of interfaces and libraries.) 특정한 운영체제를 위한 각각의 JVM 구현(implementation)은 자바 프로그래밍 구조를 실행되고 있는 로컬 운영체제의 지시와 명령으로 번역한다.

이런 방식으로 자바 프로그램은 플랫폼 독립성을 갖춘다.

JVM의 가장 첫 시제품 구현은 현대의 PDA(Personal Digital Assistant)를 닮은 휴대용 기기에 의해 구동 되는(hosted by) 소프트웨어에서 JVM 구조 셋을 가상 구동(emulated)한 Sun Microsystems에 의해 완성됐다.

오라클의 최근 구현은 JVM을 휴대폰, 컴퓨터와 서버 기기에서 가상 구동(emulate)한다. 그러나 JVM은 어떤 특정한 구현 기술이나 구동 기기, 구동 OS를 가정하지 않는다.( = JVM은 구동 환경에서 독립적이다.)

구현자가 만들어낸 소스코드 즉, .java파일은 먼저 Class Loader에 의해 컴파일된다. 이를 통해서 자연어(?)로 적혀진 소스 코드들은 바이트 코드를 가지고 .java 파일의 이름과 같은 이름을 가지는 .class 파일을 만들어낸다. 이러한 .class 파일은 우리가 어플리케이션을 실행시킬 때 다양한 단계를 거치게 된다.

자바 코드의 실행 과정

.class 코드는 다음과 같은 실행 과정을 거친다.

[https://www.geeksforgeeks.org/jvm-works-jvm-architecture/](https://www.geeksforgeeks.org/jvm-works-jvm-architecture/)

출처 : https://www.geeksforgeeks.org/jvm-works-jvm-architecture/

먼저 컴파일된 바이트 코드(=.class)는 Class Loader 서브 시스템을 만나게 된다.

Class Loader SubSystem

클래스 로더는 다음과 같은 작업을 수행한다.

1. Loading

클래스 로더는 .class 를 읽고 해당 바이너리 데이터를 생성해 메서드 영역에 저장한다. 각 .class 파일에 JVM은 메서드 영역에 다음과 같은 정보를 저장한다.

  • 로드된 클래스 및 직계 부모 클래스의 정규화된 이름
  • .class 파일이 클래스나 인터페이스, Enum과 관계 있는 파일인지에 대한 여부
  • 수정자, 변수, 메서드에 관한 정보 등

.class 파일을 읽고 난 뒤, JVM은 힙 메모리안에 이 파일을 표현하기 위해 클래스 타입의 객체를 만든다. 이 클래스 객체는 부모의 이름, 메서드, 변수 정보와 클래스와 같은 클래스 단계의 정보를 얻으려는 프로그래머에 의해 사용될 수 있다. 이 객체의 참조를 얻기 위해 우리는 객체 클래스의 메서드인 getClass() 메서드를 사용하면 된다.

2. Linking

Linking 작업에서는 다음과 같은 작업을 수행한다.

  • Verification

    .class 파일과의 동일성을 확인한다. 즉, 유효한 컴파일러에 의해 제대로된 형식을 갖춘채로 파일이 생성되었는지 확인한다. 이 작업이 실패하게 되면, run-time Exception인 java.lang.VerfityError를 마주치게 된다. 이러한 행위는 ByteCodeVerifier 컴포넌트에 의해 일어나며 이 작업이 성공적으로 수행되면 클래스파일은 컴파일될 준비를 마친것이다.

  • Preparation

    JVM은 클래스 static 변수를 위한 메모리를 할당하고 메모리를 초기값으로 초기화한다.

  • Resolution

    타입으로 부터의 상징적 참조가 직접 참조로 대체되는 과정이다. 이는 참조된 엔티티의 위치를 표시하기 위해 메서드 영역을 탐색함으로써 수행된다.

3. Initialization
이 단계에서는 모든 static 변수들이 코드와 static 블록 안에서 정의된 자신들의 값을 가지고 할당된다. 이는 클래스에서 top to bottom 식으로 수행되며 클래스 구조에서는 parent to child 방식으로 수행된다.

보통, 세 개의 클래스 로더가 존재한다.

  • Bootstrap class loader

    모든 JVM 구현에는 신뢰할 수 있는 클래스를 로드할 수 있는 부트스트랩 클래스 로더가 있어야 한다. Bootstrap clss loader는 JAVA_HOME/jre/lib 디렉토리에 표현되어 있는 자바 core API를 불러온다. 이 경로는 bootstrap 경로로 잘 알려져있다. 또한 이는 native 언어인 C, C++ 로 구현되어 있다.

  • Extension class loader

    Extension class loader는 bootstrap class loader의 자식이다. JAVA_HOME/jre/lib/ext(확장 경로) 이나 java.ext.dirs 의 시스템 속성에 명시 되어있는 다른 디렉토리에 표현되어 있는 클래스를 불러온다. 이는 sun.misc.Launcher$ExtClassLoader class에 의해 java로 구현되어 있다.

  • System/Application class loader System/Application class loader는 extension class loader의 자식 이다. System/Application class loader는 애플리케이션 클래스 경로에서 클래스를 로드하는 역할을 한다. java.class.path. 에 연결되어 있는 환경변수를 내부적으로 이용하며 Extension class loader와 같이 sun.misc.Launcher$ExtClassLoader class에 의해 java로 구현되어 있다.

이와 같은 클래스 로더는 동적 로딩을 통해 필요한 클래스들을 로드 및 링크(연결)해 런타임 데이터 영역(JVM의 메모리)에 올린다.

Run Time Data Area

https://media.geeksforgeeks.org/wp-content/uploads/jvm-memory-2.jpg

JVM은 프로그램 실행 중 사용되는 다양한 데이터 영역을 정의한다. 데이터 영역의 몇몇은 JVM의 실행과 함께(start-up) 만들어지며 JVM의 탈출(exits)과 함께 파괴된다. 다른 데이터 영역은 스레드 단위로 생성, 파괴된다.(Other data areas are per thread. Per-thread data areas are created when a thread is created anad destroyed when the thread exits.)

PC(Program Counter) 레지스터


JVM은 실행에 대한 많은 스레드를 한번에 지원할 수 있다 (JLS §17). 각 JVM 스레드는 자신만의 PC(Program Counter) 레지스터를 가진다. 어떤 시점에서든지, 각 JVM 스레드는 단일 메서드, 즉 해당 스레드(§2.6)에 대한 현재 메서드의 코드를 실행한다.


Program Coutner?
중앙처리장치 내부에 있는 레지스터(기억장치) 중 하나.  
다음에 실행될 명령어의 주소를 가지고 있어 실행할 기계어 코드의 위치를 지정한다.  
이로 인해 명령어 포인터라고도 불린다.

만약 그 메서드가 native 하지 않으면, pc 레지스터는 최근에 실행된 JVM 명령의 주소를 포함한다.
만약 스레드에 의해 최근에 실행된 메서드가 native 하다면, JVM의 pc 레지스터의 값은 정의되지 않는다.

JVM의 pc 레지스터는 반환주소(return Address)를 유지하거나 특정 플랫폼에 대한 native 포인터를 유지하기에 충분히 넓다.

JVM Stacks


각 JVM 스레드는 스레드가 만들어짐과 동시에 비공개 JVM 스택을 지닌다.

JVM 스택은 frame에 저장된다. JVM 스택은 지역 변수와 부분적인 결과를 유지하고 메서드 호출과 반환의 역할을 하는 C와 같이 전통적인 언어의 스택과 유사하다. JVM 스택은 frame을 푸시하고 팝하는 것을 제외하고 절대 직접 조작되지 않기 때문에 프레임은 힙에 할당될 수 있다. JVM 스택을 위한 메모리는 연속적이지 않아도 된다.


Frames?

프레임은 데이터 및 부분적 결과를 저장하고 동적 연결(dynamic linking)을  
수행할 뿐만 아니라 메서드에 대한 값을 반환하고 예외를 전달하는 데 사용된다.

새로운 프레임은 메서드가 호출될 때 마다 생성되며
그 메서드 호출이 정상적으로 완료되었는지 비정상적으로 완료되었는지에 관계 없이 호출이 완료 되면 파괴된다.

프레임은 프레임을 만드는 스레드에 대한 JVM 스택으로 부터 할당된다.  
각 프레임은 그 자신들만의 최근 메서드의 클래스에 대한 지역변수 배열, 피연산자 스택, run-time constant pool로의 참조를 가진다.

초판 JVM 명세에서, JVM 스택은 자바 스택으로써 알려지지 않았다.

이 명세(Specification)는 JVM stack이 계산에 필요한대로 고정되거나 동적으로 확장, 수축되는 것을 허락한다.

만약 JVM(Java Virtual Machine) 스택의 크기가 고정되었다면 각 JVM(Java Virtual Machine) 스택의 크기는 해당 스택이 생성될 때 독립적으로 선택될 수 있다.

JVM구현은 프로그래머 또는 사용자에게 JVM 스택의 초기 크기에 대한 제어를 제공할 수 있을 뿐만 아니라 JVM스택을 동적으로 확장하거나 축소하는 경우 최대 및 최소 크기에 대한 제어를 제공할 수 있다.

아래의 예외 상황은 JVM 스택과 관련있다.

  • 만약 스레드에서의 계산이 허가된 것 보다 더욱 큰 JVM 스택을 요구로 한다면 JVM은 StackOverFlowError 예외를 던진다.
  • JVM 스택이 동적으로 늘어날 수 있는 상황이고 팽창이 시도되고 있지만 팽창을 위한 메모리가 불충분하다거나 새로운 스레드를 위한 JVM stack 초기화에 사용할 메모리가 불충분하다면 JVM은 OutOfMemoryError 예외를 던진다.

Heap


JVM은 모든 JVM 스레드 사이에서 공유될 수 있는 heap을 가진다. 힙은 모든 클래스 인스턴스 및 배열에 대한 메모리가 할당되는 런타임 데이터 영역이다.

힙은 JVM이 시작되며 생성된다. 객체를 위한 힙 저장공간은 garbage collector로 알려진 automatic storage management 시스템에 의해 내장된다.

객체들은 결코 명시적으로 비할당되지 않는다. JVM은 automatic storage management system의 특정한 타입을 가정하지 않고 storage management 기술은 구현자의 시스템 요구사항에 따라 선택될 수 있다.

힙은 고정되거나 계산에 의해 필요에 따라 팽창 되거나 큰 크기의 힙이 불필요하다면 줄어들 수 있다. 힙을 위한 메모리는 연속적일 필요 없다.

JVM구현은 프로그래머 또는 사용자에게 JVM 힙의 초기 크기에 대한 제어를 제공할 수 있을 뿐만 아니라 JVM 힙을 동적으로 확장하거나 축소하는 경우 최대 및 최소 크기에 대한 제어를 제공할 수 있다.

다음의 예외 상황은 heap과 관련있다.

  • 만약 스레드에서의 계산이 automatic storage management system(garbage collector)에 의해 사용가능한 힙 보다 더욱 많은 힙을 요구한다면 JVM은 OutOfMemoryError예외를 던진다.

메서드 영역


JVM은 모든 JVM 스레드 사이에서 공유되는 메서드 영역을 가진다.
메서드 영역은 전통적인 언어의 컴파일된 코드를 담기 위한 저장 공간이나 운영체제 프로세스 에서의 “text” 세그먼트와 유사하다.
메서드영역은 run-time constant pool, 필드와 메서드 데이터, 생성자와 메서드를 위한 코드 그리고 클래스와 인스턴스 초기화와 인터페이스 초기화에 사용되는 특정한 메서드(§2.9) 와 같은 클래스 단위의 구조를 저장한다.

메서드 영역은 JVM이 실행과 함께 만들어진다. 메서드 영역이 논리적으로 힙의 일부이긴 하나, 단순한 구현들은 garbage collect 를 하지 않게 하거나 경량화하지 않게 선택할 수 있다.
현재 버전의 JVM 명세는 메서드 영역의 위치나 컴파일된 코드를 관리하는데 사용되는 정책을 요구하지 않는다.

데이터 영역은 고정된 크기를 가지거나 계산에 따라 필요에 의해 팽창될 수 있고 더욱 큰 메서드 영역이 불필요하게 되면 줄어들 수 있다. 메서드 영역을 위한 메모리는 연속적일 필요 없다.

JVM구현은 메소드 영역의 초기 크기에 대한 프로그래머 또는 사용자 제어를 제공할 수 있을 뿐만 아니라 다양한 크기의 메소드 영역의 경우 최대 및 최소 메소드 영역 크기에 대한 제어를 제공할 수 있다.

아래는 메서드 영역과 관련된 예외 상황이다.

  • 만약 할당 요청을 충족하기 위한 메서드 영역의 메모리를 사용할 수 없다면 JVM에서 OutOfMemoryError 예외를 던진다.

Run-Time Constant Pool


run-time constant pool 은 클래스 파일 안의 클래스 단위 혹은 인터페이스 단위의 런타임 표현이다.

여기에는 컴파일 타임에 알려진 숫자 리터럴에서 런타임에 확인되어야 하는 메서드 및 필드 참조에 이르기까지 여러 종류의 상수가 포함된다.
런타임 상수 풀은 전통적인 프로그래밍 언어에 대한 전형적인 상징 테이블의 데이터 범위보다 더 넓지만 상징 테이블과 닮은 함수를 제공한다.

각 run-time constant pool 은 JVM의 메서드 영역으로부터 할당된다. (§2.5.4) 클래스나 인터페이스에 대한 run-time constant pool는 클래스나 인터페이스가 생성될 때 JVM에 의해 구성되어진다.

다음은 클래스, 인터페이스에 대한 run-time constant pool의 구조와 관련된 예외 상황에 대한 예시이다.

  • 클래스나 인터페이스를 만들 때 run-time constant pool의 구조가 더욱 많은 메모리를 요구로 하고 JVM 안의 메서드 영역에서 사용 가능한 메모리보다 더욱 많은 메모리를 필요로 한다면 OutOfMemoryError예외를 던진다.

Native Method Stacks


JVM의 구현은 native 메서드(자바 이외의 언어로 작성된 메서드)를 지원하기 위해 구어체로 C 스택이라고 불리는 전통적인 스택을 사용할 수 있다.
Native method stack은 또한 C와 같은 언어에서 JVM의 명령 셋에 대한 interpreter의 구현에도 사용될 수 있다.

native method를 읽어들일 수 없고 그들 스스로 전통적인 스택에 의존하는 JVM 구현은 native method stack을 제공할 필요가 없다. 제공되는 경우, native method stack은 일반적으로 각 스레드가 만들어질 때 스레드 단위로 할당된다.

이 명세는 native method stack이 고정되거나, 동적으로 계산에 의해서 확장, 수축되는 것을 허용한다. native method stack의 크기가 고정되어 있는 경우, 각 native method stack 에 대한 크기는 이것이 만들어질 때 각각 독립적으로 선택될 수 있다.

JVM구현은 native method stack 의 초기 크기에 대한 프로그래머 또는 사용자 제어를 제공할 수 있을 뿐만 아니라 다양한 크기의 native method stack의 경우 최대 및 최소 메소드 영역 크기에 대한 제어를 제공할 수 있다.

다음은 native method stack과 관련된 예외적 상황이다.

  • 스레드에서의 계산이 허가된 것 보다 더욱 큰 native method stack을 요구로 한다면 JVM은 StackOverFlowError 예외를 던진다.
  • native method stack이 동적으로 팽창할 수 있고 이것이 불충분한 메모리만 사용가능 하거나, 또는 불충분한 메모리가 새로운 스레드에 대한 초기 native method stack 을 만드는데 불충분한 메모리가 사용가능하다면, JVM은 OutOfMemoryError를 던진다.

Execution Engine

이렇게 런타임 데이터 영역에 배치된 바이트 코드를 실행하기 위해서 JVM 내에는 이를 명령어 단위로 읽어 실행하는 실행 엔진(Execution Engine)이 있다.

실행 엔진은 바이트 코드를 한 줄 한 줄 읽고 다양한 메모리 영역에 있는 데이터와 정보를 사용하여 명령을 실행한다. 이러한 실행 엔진은 세 개의 부분으로 구분된다.

  • Interpreter

    인터프리터는 바이트코드를 한 줄씩 해석하고 실행한다. 한 메서드가 여러번 호출되는 경우 이를 호출 될 때마다 해석해야한다는 단점을 가진다.

  • Just-In-Time Compiler(JIT)

    JIT 컴파일러는 Interpreter의 효율성을 증가시키기 위해 사용된다. 이는 전체 바이트코드를 컴파일하고 native code로 변환시켜 인터프리터가 반복된 메서드 호출을 감지할 때면 JIT가 직접적인 direct code를 제공해 재해석이 필요하지 않도록해 효율성을 증가시킨다.

  • Garbage Collector

    더이상 참조되지 않는 객체를 파괴한다. 이에 대한 설명은 이곳에서 더욱 자세히 확인 가능하다.

Summary, HotSpot Architecture

HotSpot JVM은 기능과 기능의 강력한 기반을 지원하고 고성능 및 대규모 확장성을 실현할 수 있는 기능을 지원하는 아키텍처를 보유한다.

예를 들어, HotSpot JVM JIT 컴파일러는 동적 최적화를 생성한다. 즉, 자바 애플리케이션이 동작하는 동안 최적화 결정을 한다. 또한 시스템 구조 기반 아래를 목표로 하는 고성능 native 머신 명령을 생성한다.

게다가 , 성숙한 진화와 런타임 환경에서 의 끊임없는 엔지니어링 그리고 다중 스레드 Garbage Collector를 통해 HotSpot JVM은 높은 확장성과 가장 큰 컴퓨터 시스템에서도 가장 큰 사용 가능성을 보여준다.

JVM 의 주요 요소는 클래스 로더, 런타임 데이터 영역, 그리고 실행 엔진을 포함한다.

Key Hotspot Components


'JVM HotSpot Arch'

JVM 의 성능과 관련된 중요 요소는 다음의 이미지에 표시되어 있다. 성능을 조정할 때 중점을 두는 JVM의 세 가지 구성 요소가 있다.

힙은 오브젝트 데이터가 저장되는 곳이다. 이 영역은 garbage collector에 의해 관리되며 시작 시에 선택된다. 대부분의 튜닝 옵션은 힙의 사이즈 설정과 적절한 상황에서 적절한 Garbage Collector를 사용하는 것에 관계있다. JIT 컴파일러 또한 성능에 큰 영향을 미치지만, 최신 버전의 JVM으로는 크게 신경쓰지 않아도 된다.

성능에 대해

일반적으로 Java 애플리케이션을 튜닝할 때 초점은 응답성 또는 처리량이라는 두 가지 주요 목표 중 하나에 있다.

Responsiveness(응답성)


응답성은 어플리케이션이나 시스템이 요청된 데이터에 대해 얼마나 빠르게 반응하는가와 관련이 있다.

예시는 다음과 같다.

  • 데스크톱 UI가 이벤트에 얼마나 빠르게 응답하는지
  • 웹사이트가 얼마나 빠르게 페이지를 반환하는지
  • 데이터베이스 쿼리가 얼마나 빠르게 반환되는지

반응성의 시점으로 본 어플리케이션에는 긴 일시정지 시간은 허용될 수 없다.

중요한 것은 짧은 시간동안 반응하는 것이다.

Throughput(처리량)


처리량은 특정한 시기(시간)에 있는 어플리케이션에 의한 작업의 양을 최대화 시키는 것에 중점을 맞춘다. 처리량의 측정 방법은 아래에 있는 예시와 같다.

  • 주어진 시간동안 완료된 트랜잭션의 수
  • 한 시간 안에 배치 프로그램이 완료할 수 있는 작업의 수
  • 한 시간 안에 완료될 수 있는 데이터베이스 쿼리의 수

높은 정지 시간은 처리량의 관점으로 보았을 때 허용될 수 있다. 높은 처리량을 가진 어플리케이션이 긴 시간 동안 수행되는 성능 테스트(benchmark)에 초점을 맞추기 때문에 빠른 응답 시간은 고려할 사항이 되지 않는다.

Reference.

How JVM Works - JVM Architecture? - GeeksforGeeks

Chapter 2. The Structure of the Java Virtual Machine

[JAVA] JVM 동작원리 및 기본개념

댓글남기기