Java Virtual Machine

Seoyeon Kim·2023년 3월 10일
1

JDK & JRE

Java Development Kit

JDK는 JRE와 javac와 같이 개발을 위해 필요한 도구들을 포함한다.

Java Runtime Environment

JRE는 JVM이 프로그램을 동작시킬 때 필요한 라이브러리 파일들을 갖고 있다.
JRE는 JVM의 실행 환경을 구현했다고 할 수 있다.

JVM

Write Once, Run Anyware

JVM은 Java와 OS 사이의 위치에 있다고 생각하면 된다. 운영체제와 상관없이 Java 프로그래밍이 잘 동작하도록 중간에서 처리한다. Java Byte Code로 compile 될 수 있는 다른 언어도 실행할 수 있다.

프로그램 실행 과정

  1. JVM이 OS에서 필요한 메모리를 할당받고 용도에 따라 여러 영역으로 나누어 관리한다.
  2. Java Compiler(javac)가 Java Source Code(.java)를 읽어서 Java Byte Code(.class)로 변환한다.
  3. Class Loader가 .class 파일들을 JVM으로 load한다.
  4. Excution Engine이 로딩된 .class 파일들을 해석한다.
  5. 해석된 코드는 Runtime Data Area에 배치된다.

실행 과정 속에서 JVM은 필요에 따라 Thread Synchronization 또는 GarbageCollection 작업을 수행한다.

Class Loader

Runtime에 동시적으로 클래스를 RAM에 load한다. Java는 동적으로 Compile Time이 아니라 Runtime에 참조한다. 즉 클래스를 처음으로 참조할 때 해당 클래스 파일을 초기화한다.

JVM 내 데이터를 저장하는 공간이 있지만 결국 컴퓨터 하드웨어의 메모리 즉 RAM에 올라가야 프로세스로서 실행이 되기 때문에 JVM 안의 가상의 공간과 구분 지어 생각해야 한다.

Loading

적재

Class Loader의 주 역할은 compile 된 클래스를 메모리에 적재하는 것이다. Loading 작업은 보통 static main() 에서 시작된다.

Linking

연결

load 된 클래스나 인터페이스를 검증하고 준비하는 과정이다.

여러 개의 클래스 파일들이 서로 상호작용할 수 있도록 연결해 주는 작업이 필요하다. 이때 아래의 조건을 반드시 충족해야 한다.

  1. 클래스 또는 인터페이스는 link 되기 전에 반드시 load 돼야 한다.
  2. 클래스 또는 인터페이스는 다음 단계인 초기화 전에 반드시 검증과 준비가 돼야 한다.

실행 단계는 아래와 같다.

  1. Verification 단계 : Compiler가 변환한 Byte Code가 Binary Code로 잘 변환이 됐는지 확인하는 과정으로 loading 과정에서 코드는 기계가 읽을 수 있는 완벽한 상태가 아니다. 이를 실행 가능한 상태로 만들어 주는 것이 linking 과정이다. 검증 과정은 가장 복잡하고 시간이 오래 걸리는 작업이다.
  2. Preparation 단계 : 메모리를 할당하는 과정으로 static field는 기본 값으로 생성되고 초기화된다.
  3. Resolutioin 단계 : Symbolic reference가 Direct reference로 대체된다. 참조하고자 하는 대상의 이름만 가지고 참조 관계를 구성하는 것이 아니라 실제 객체의 주소를 참조하게 된다. 여기서 실제 객체의 주소를 참조한다는 것은 메모리에 할당된 실제 주소를 코드에 반영하여 실행 가능한 Binary Code가 되는 것을 말한다.

Byte Code vs Binary Code
Binary Code는 컴퓨터가 인식할 수 있는 0과 1로 구성된 이진 코드를 의미하며, Byte Code는 가상 머신이 이해할 수 있는 0과 1로 구성된 이진 코드를 말한다.

Initialization

클래스나 인터페이스의 생성자를 통한 초기화 로직이 실행된다. 이때 link 단계에서 기본 값으로 초기화된 static 변수들을 프로그래머가 설정한 값으로 정의한다.

Execution Engine

Class Loader가 JVM의 Runtime Data Area에 Byte Code를 배치하면 Byte Code는 Execution Engine에 의해 실행된다. Java Byte Code는 기계가 바로 수행할 수 있는 언어보다 사람이 보기 편한 형태를 갖는다. 그래서 Execution Engine은 이와 같은 Byte Code를 기계가 실행할 수 있는 형태로 변환한다.

Interpreter

Execution Engine은 Byte Code를 명령어 단위로 한 줄씩 읽어서 실행한다. Interpreter는 Byte Code를 더 빠르게 해석하지만 실행 속도가 느리다. 그리고 하나의 method를 여러 번 호출할 때마다 새로 해석해야 한다는 단점이 있다.

Just In Time Compiler

Java는 Byte Code로 변환한 후 다시 기계어로 변환해야 해서 상대적으로 느릴 수밖에 없었다.
이 점을 보완하기 위해 도입된 것이 JIT Compiler다.

정적 compile 방식은 프로그램을 실행하기 전에 한 번에 기계어로 바꿔 놓는 것을 말하며, 위에서 말했듯이 Interpreter 방식은 실행 중 코드를 Byte Code로 변환해서 한 줄씩 기계어로 바꾸며 읽는 것을 말한다.

그래서 Python 같은 Interpreter 방식보다 C와 같은 Compiler가 더 빠르다.

JIT Compiler는 2가지 방식을 혼합한 것으로 볼 수 있다.

  • .class 파일로 변환하는 것은 Byte Code로 변환하는 Compile 방식.
  • Byte Code에서 method call이 발생하는 것만 읽는 Interpreter 방식.

생성된 Native Code는 Cache에 저장하여 같은 함수가 여러 번 불릴 때 매번 기계어 코드를 생성하는 것을 방지한다.

Garbage Collection

참조되지 않은 객체를 찾아서 제거한다.

Runtime Data Area

프로그램을 실행하기 위해 OS로부터 할당받은 메모리 공간을 말한다.

위에서 Class Loader가 RAM에 적재한다고 했는데 이는 Runtime Data Area를 가리킨다. JVM 자체가 메모리를 사용하기 때문에 JVM 안의 Runtime Data Area에 할당하는 것이 결과적으로는 RAM에 적재하는 것과 같다고 볼 수 있다.

Method Area

JVM 당 1개만 존재하는 공유 자원 공간으로 모든 JVM Thread는 이 공간을 공유한다. compile 된 코드의 정보를 클래스와 인스턴스 단위로 저장한다.

Method Area는 Constant Pool에 상수값을 저장한다. Runtime 시 필요한 모든 숫자, 문자열, 식별자, 클래스, 메서드 등이 상수에 해당한다. Symbol Table 형식으로 정보를 저장하기 때문에 Key를 통해 Value를 가져온다.

Heap Area

Heap Area 또한 JVM 당 1개만 존재하는 공유 자원 공간으로 new 연산자를 사용해서 객체를 생성할 때 할당된다. Method Area는 메모리에 할당되는 인스턴스의 정보를 갖고 있다면 Heap Area는 실제 데이터가 위치한다.

Method Area와 Heap Area는 여러 Thread들이 공유하기 때문에 저장된 데이터가 Thread-Safe 하지 않다.

Young Generation

비교적 최근에 할당된 객체들을 갖고 있는다.

Eden Space & Survivor Space

새로 생성된 객체는 Eden Space로 이동한다. Eden 공간이 객체들로 가득 차면 Minor GC가 수행되며 살아남은 객체들은 Survivor 공간으로 이동한다.

'살아남은' 객체들이라고 한다면, 계속해서 참조가 되고 있는 객체를 말한다.

위 과정을 여러 번 반복하고 살아남은 객체들은 Old Generation으로 옮겨진다.

Old Generation

Minor GC로부터 살아남아 오랫동안 사용될 예정인 객체들이 Old Generation에 위치한다. 만약 이 공간마저 가득차게 되면 Major GC가 수행되며 가장 예전에 사용된 객체부터 가지고 간다.

Stack Area

Stack Area는 Thread 당 1개씩 할당된다. 즉 공유 자원이 아니다.

Heap 영역에서 객체가 실행되고 메서드가 필요하게 되면 Stack 영역의 Frame에 메서드가 할당된다.

Stack에는 Heap을 참조하는 메서드가 올라가고 메서드의 실행이 끝나면 Stack에서 pop 되어 빠져 나온다. 반면 Heap에는 클래스 또는 인스턴스 단위의 객체가 올라가며 인스턴스는 GC에 의해서 제거된다.

Cache Memory

Compiler에 의해 변환된 기계어가 저장된다.

PC Registers

Thread 당 1개씩 존재하며 Thread가 실행되면 현재 실행 중인 명령을 저장하고 끝나면 다음에 실행할 명령의 주소를 가리킨다.

Native Method Stack

Java Thread와 C 또는 C++ 로 작성된 코드를 mapping 한다.

Native Method는 Native Library와 연결된다. Native Library는 Java가 아닌 C와 같은 다른 언어로 작성된 것으로, Legacy 데이터 또는 성능 향상을 위해서 사용하다가 Java Library가 발전하면서 점차 쓰이지 않게 되었다.

Native Code는 프로그래머가 Java Native Interface를 통해서 호출할 수 있다.

Java Native Interface

Java Native Interface는 C 또는 C++ 로 작성된 Native Library를 제공하는 Native Method Library와 상호작용하기 위해 존재한다.

Native Method Library

Execution Engine에 필요한 Native Libraries가 모여있다. Execution Engine을 실행하기 위해 필요하다.

2개의 댓글

comment-user-thumbnail
2023년 3월 11일

대박

1개의 답글