JVM 구조

BK·2024년 7월 15일
0

Java

목록 보기
5/9

JVM - 구조

JVM 구조는 크게 Class Loader, Execution Engine, Runtime Data Area로 나눌 수 있다. 간략히 말하면 Class Loader는 java compiler(javac)에 의해 컴파일 된 Java byte code(.class)를 JVM에 적재하는 역할을 수행하며, 동적 로딩을 통해 필요한 class들을 로딩/링킹 하여 Runtime Data Area에 위치할 수 있도록 한다. 이렇게 Runtime Data Area에 적재된 byte code는 Execution Engine에 의해 해석/실행 되며, Garbage Collector에 의해 데이터가 관리된다.

클래스 로더(Class Loader)

JVM은 class를 메모리에 적재할 때, 동적 class 로딩을 사용한다. 즉, 필요한 모든 class를 실행 전 메모리에 적재하는 것이 아니라 필요한 시점에 필요한 class를 메모리에 올린다. JVM의 실행 엔진(Execution Engine)이 byte code를 해석(Interpret)하며 실행하다 새로운 class가 runtime data area에 필요한 경우 이 class loader를 통해 해당 class를 찾아 메모리에 적재하는 것이다.

클래스 로더 동작 과정

Class loader가 class를 동적 로딩 하는 과정은 크게 loading, linking, initializing으로 나눌 수 있다.

Loading : JVM에 class를 적재하기 위한 기초 작업을 수행한다.

  1. .class 파일을 읽어 이에 따라 binary data를 생성하고, runtime data area의 method 영역에 저장한다.
  2. 위 작업이 완료되면 로드한 class에 따라 Class 객체를 생성해 heap 영역에 저장한다. 이 객체는 reflection에 사용되는 class의 meta data를 가지는 객체이다.

Linking : Vertifying, Preparing, Resolving 단계로 구성되어 있으며, 해당 class를 검증하는 과정이다.

  1. Verifying : byte code(.class 파일)이 유효한지 확인한다.
  2. Preparing : class의 field 등에 필요한 메모리를 할당한다.
  3. Resolving : Symbolic memory reference들을 method 영역의 실제 reference로 교체한다.

Initialization : 필요한 class의 member들(static field 등)을 초기화 한다. 이때 static initializer block이 있다면 이 또한 수행한다.

💡 Class Loader는 객체(instance)를 생성하는 과정이 아닌, 객체(instance) 생성이 가능하도록 그 설계도(?)에 해당하는 class를 메모리에 적재 한다. 이 과정에서 해당 class에 해당하는 Class 객체를 heap 영역에 적재하게 되는데, heap 영역에 해당 Class 객체가 적재되는 시점을 통해 JVM의 동적 class 로딩을 확인할 수 있다.

실행 엔진(Execution Engine)

실행 엔진은 Runtime data area에 적재된 byte code를 실행하는 역할을 수행한다. Java compiler(javac)에 의해 compile 된 코드는 컴퓨터가 실행 가능한 기계어가 아닌 JVM이 해석 가능한 byte code이기에, JVM은 이를 기계어로 변환해야 하고 이를 수행하는 것이 Execution engine이다.

실행 엔진은 byte code를 실행하는 과정에서 Interpreter와 JIT Compiler를 통해 interpreter 방식과 compile 방식을 모두 사용한다.

기본적으로는 interpreter를 사용하나, 자주 호출되는 특정 method를 매번 해석하여 실행하는 것은 성능에 문제가 있기 때문에 자주 사용되는 코드를 컴파일 하여 캐싱해 두는 JIT Compiler를 도입하게 되었다.

JIT Compiler

Interpreter는 byte code를 하나씩 읽고 해석하여 실행하며, JIT Compiler는 자주 사용되는 코드를 Native Code로 compile하여 이를 캐싱해 둔다. 이렇게 캐싱된 코드를 통해, 해당 메소드가 호출될 때 interpreter가 아닌 캐싱된 native code를 바로 호출하도록 하여 java 프로그램의 성능을 향상 시킨다.

모든 method를 native code로 compile 한다면, 실행 속도에서 큰 성능 향상을 얻을 수 있으나, application의 시작 시간에 큰 영향을 미칠 수 있으며, 메모리 사용량 등의 overhead가 발생할 수 있기에 모든 code를 컴파일 하지 않고, 메소드에 대한 compile threshold를 통해, 자주 호출되는 method에게만 JIT Compiler를 trigger하도록 한다.

JIT Compiler를 통해 application을 최적화 하는 다양한 방법이 존재하기에, JIT Compiler에 대해서는 추후 더 자세히 작성해보도록 하겠다.

Garbage Collector(GC)

Java는 C/C++과 달리 메모리를 직접 할당하거나, 직접 free하지 않는다. 그렇다면 application이 실행된 후, 계속 동적으로 메모리가 할당되고 회수 되지 않는 memory leakage가 발생하게 된다. Java는 이러한 문제를 자동으로 사용되지 않는 메모리를 회수해 주는 GC(Garbage Collector)를 통해 해결하였다. GC를 통해 개발자가 직접 메모리를 관리할 필요가 없도록 하였지만, 반대로 개발자가 직접 메모리를 관리할 수도 없게 되었다.

GC는 runtime data area 중 heap 영역에 대한 검사를 수행하여, 사용되지 않는 메모리를 회수하는 작업을 수행하는데, 정해진 시간에 검사를 수행하는 것이 아니며 검사를 수행하는 방법에도 여러 알고리즘이 있으며 복잡한 구조를 가지고 있다.

GC를 통해 개발자에게 메모리 관리에 대한 책임을 지지 않도록 하였으나, 메모리 관리는 매우 중요한 부분이기에 GC 튜닝과 같은 방법을 제공하며, Java 발전에 따라 많은 변화도 있었다.

GC 동작 과정, GC 알고리즘 등 GC에 대한 내용 또한 추후에 더 자세히 작성해보겠다.

런타임 데이터 영역(Runtime Data Area)

Java application이 실행되면서 할당 받은 JVM의 메모리 영역이다. Class loader를 통해 적재된 데이터 뿐 아니라, application이 실행되면서 생성되는 data 등, java 프로그램이 실행되며 사용되는 모든 데이터가 저장되는 공간이다.

데이터 영역은 크게, PC register, Stack(Java, Native), Heap, Method 영역으로 나눌 수 있으며, 이 중 Heap 영역이 GC의 관리 대상이 된다.

PC Register, Stack 영역은 각 Thread 별 존재하며, Heap과 Method 영역은 모든 Thread에 의해 공유되는 영역이다.

Heap 영역은 런타임에 동적으로 할당되어 사용하는 영역으로, new 키워드를 통해 생성된 instance나 배열 등이 저장된다. GC에 의해 관리되는 영역으로, 각 JDK version이나 벤더에 따라 GC 방식과 메모리 구조가 상이할 수 있다.

Stack과 PC Register 영역은 thread가 실행될 때 필요한 정보들을 담는 공간으로, method stack, 지역 변수, 실행 중인 코드 등의 정보를 포함한다.

Method 영역은 Class Loader를 통해 적재한 class에 대한 정보, static 변수/method 등을 포함하여 static 영역이라고 불리기도 하며, Runtime Constant Pool을 포함한다.

데이터 영역은 constant pool, heap 영역과 GC 등 함께 알아보아야 할 부분이 많아 이 또한 추후 더 자세히 작성해보겠다.

0개의 댓글