본문 바로가기
Develop/Java

Java JVM 아키텍쳐 (1)

by 독서왕뼝아리 2023. 7. 16.
먼저 Virtual Machine 이란? 

수업을 듣다보면 가상 머신을 다뤄 본 적이 있을 것이다. 자신의 하드웨어 위에 또 다른 OS(Guest OS)를 실행하게 해주는 것이 VM의 역할이다. 그런 VM이 자바에도 있다! 연관해서 생각해 본 적이 없었는데, 생각해보니 JVM의 VM이 virtual machine 이다. 

 

 

JVM은 무엇인가?

JVM은 추상 머신이다. 자바 바이트 코드를 런타임 환경에서 실행할 수 있게 해준다. JVM은 많은 하드웨어나 소프트웨어에서 사용 가능하다. Implementation(JRE, Java Runtime Environment)에 따라서 JVM 동작 알고리즘을 선택할 수 있다. 

 

무슨일을 하는가?

VM인만큼 스스로 메모리 관리를 한다.

  • Loads code
  • Verifies code
  • Executes code
  • Provides runtime environment

이하 기능들을 제공한다.

  • Memory area
  • Class file format
  • Register set
  • Garbage-collected heap
  • Fatal error reporting etc.

 

JVM 아키텍쳐

 

 

Class Loader

.java 파일을 컴파일 하면 바이트 코드로 .class 파일로 변환된다. 그 .class 파일을 로딩할 때 사용된다. 우리가 자바 프로그램을 실행시키면, 가장 먼저 클래스로더에서 처리가 된다. 클래스로더는 세 가지 단계로 구분되어 있다.

 

  • Loading

1. Bootstrap Class Loader : java.lang 같은 모든 JSE(Java Standard Edition) .class 파일을 포함해 jar 파일을 로드한다. 

 

2. Extension Class Loader : 부트스트랩 클래스로더의 자식이자, 시스템 클래스로더의 부모이다. $JAVA_HOME/jre/lib/ext 디렉토리 안의 자바 표준 라이브러리를 로드한다.

 

3. System/Application Class Loader : 클래스 파일을 로컬 classpath에 따라 로드시킨다. 

 

만약 부모 클래스로더가 클래스 파일을 찾을 수 없다면 자식 클래스로더한테 작업을 위임한다. 마지막 자식까지도 찾지 못한다면 NoClassDefFoundError 또는 ClassNotFoundException을 발생시킨다.

 

 

  • Linking

클래스가 메모리에 적재되면 링킹이 시작된다.

1. Verification : .class 파일의 구조적인 옳음(?)을 검증한다.  검증에 실패하면 VerifyException을 발생시킨다. (java 11로 빌드된 코드가 java8로 run할 때)

 

2. Preparation : JVM은 static fields를 메모리에 할당하고 초기화되지 않은 전역 변수들을 기본값으로 초기화 한다.

 

3. Resolution : symbolic references are replaced with direct references present in the runtime constant pool. (다른 클래스나 constant 변수를 참조하고 있다면 이때 참조를 실제 값으로 변경시킨다는 뜻인 듯)

 

 

  • Initialization

class method나 interface를 초기화 한다. class의 생성자를 호출하며, static 블록을 실행하고, 모든 static 변수를 할당하는 과정을 진행한다. 클래스 로딩의 마지막 단계이다!

 

 

이렇게 .java 코드를 선언했다고 하자. preparation 단계에선 enabled를 boolean 타입의 기본 값인 false로 저장한다. 그리고 initialization 단계에서 실제 값인 true로 할당한다!!!!!

private static final boolean enabled = true;

아하!!!!!

 

 

Runtime Data Area
  • Class(Method) Area

런타임 constant pool, 필드, 메소드 데이터 같은 모든 데이터들이 이곳에 저장된다. (메모리의 코드영역과 비슷한 것 같다)

공간이 충분하지 않다면 OutOfMemoryError를 발생시킨다.

 

 

  • Heap

객체 변수에 해당하는 object들이 저장된 런타임 데이터 영역이다. JVM의 힙영역은 오직 하나이다. 멀티스레드 환경에서는 힙과 method 영역을 공유하므로 thread-safe하게 만들 것

 

 

  • Stack

Java Stack은 프레임을 저장한다. 지역 변수나 부분적인 결과를 가지고 있고, 메소드 실행 등의 작업을 수행한다. 각 스레드는 각각의 JVM 스택을 가지고 있다. 

 

 

  • Program Counter Register

PC register는 현재 실행 중인 JVM의 명령 주소를 가지고 있다. 스레드는 각각의 PC register를 가진다. 명령이 실행되면, PC register는 다음 명령을 위해 업데이트 된다.

 

 

  • Native Method Stack

application의 native한 메소드를 가지고 있다.(??) 이 메소드들은 자바가 아닌 C나 C++같은 언어로 작성되어 있다. 스레드는 또한 각각의 native method stack을 가진다.

 

 

Execution engine

바이트 코드가 메인 메모리에 적재가 되고 런타임 데이터 영역에서 자세하게 처리가 된 후, 그 다음에 프로그램이 동작된다. Execution Engine은 런타임을 handle한다.

하지만! 프로그램 동작 전에 바이트 코드는 machine-language 명령으로 변환되어야 한다. JVM은 인터프리터나 JIT 컴파일러를 이용해 변환작업을 수행한다.

 

  • Interpreter

인터프리터는 바이트 코드를 한 줄 씩 읽고 수행한다. 당연히 느릴 수밖에 없다. 인터프리터의 또 다른 단점으로 한 메소드가 여러 번 호출될 때마다 인터프리테이션이 필요하다. 

 

  • JIT Compiler

JIT Compiler는 그런 인터프리터의 단점들을 극복한다! Execution Engine은 바이트 코드를 처음엔 인터프리팅하지만 반복적으로 호출되면 JIT 컴파일러에서 찾는다. JIT 컴파일러는 바이트 코드 전체를 native machine code로 변경한다. 반복되는 작업들을 자바 시스템이 향상된 작업을 위해 존재!

1. Intermediae Code Generator

2. Code Optimizer 

3. Target Code Generator

4. Profiler

 

그럼 모든 코드를 컴파일해서 JIT Compiler에 저장하고 있으면 되지 않나? 싶지만 자바의 모~~~~든 코드를 컴파일 하면 느려지게 된다. 따라서 자주 사용되는 코드들만 미리 컴파일한다.

또한 lazy-loading(실제 호출될 때 로딩된다)이기 때문에 사용할 때만 인터프리팅 하는 것이 이득이다.

 

> 인터프리터와 JIT 컴파일러 추가적인 내용

더보기
int sum = 10;
for(int i = 0 ; i <= 10; i++) {
   sum += i;
}
System.out.println(sum);

An interpreter will fetch the value of sum from memory for each iteration in the loop, add the value of i to it, and write it back to memory. This is a costly operation because it is accessing the memory each time it enters the loop.

 

However, the JIT compiler will recognize that this code has a HotSpot, and will perform optimizations on it. It will store a local copy of sum in the PC register for the thread and will keep adding the value of i to it in the loop. Once the loop is complete, it will write the value of sum back to memory.

 

Note: a JIT compiler takes more time to compile the code than for the interpreter to interpret the code line by line. If you are going to run a program only once, using the interpreter is better.

 

휴 GC부턴 다음 글에

 

 

 

 

참조

https://www.freecodecamp.org/news/jvm-tutorial-java-virtual-machine-architecture-explained-for-beginners/