Language/자바&코틀린

[자바]JVM의 동작방식과 구조

코딩공장공장장 2021. 1. 11. 15:47

JVM(Java Virtual Machine)이란


 자바 프로그램의 실행 환경을 만들어주는 가상 머신

JVM(Java Virtual Machine)은 자바 프로그램을 실행하는 가상 머신이다.

바이트 코드 기반으로 동작하는 자바 프로그램은 기계어를 다루는 CPU나 메모리 같은 자원을 직접 활용할 수 없다.

JVM 내부의 Runtime Data Area와 Execution Engine과 같은 구성 요소들이 컴퓨터 자원을 활용함으로써 자바 프로그램의 실행이 가능해진다. 

RuntimeDataArea가 바이트 코드 기반 데이터를 저장하고,

CPU는 인터프리터를 통해 바이트 코드를 실행시킴으로써 프로그램을 동작하게 한다.

 

자바의 플랫폼 독립성


 

 

자바 컴파일러는 자바파일(*.java)을 자바 바이트 코드(*.class)로 컴파일한다.

JVM은 플랫폼(OS) 별로 존재하며 자바 바이트 코드를 컴퓨터(CPU)가 처리할 수 있도록 한다.

따라서 자바는 플랫폼에 종속되지 않고 JVM 처리하는 바이트 코드라는 중간 언어플랫폼마다 존재하는 JVM을 통하여 운영체제에 상관 없이 실행할 수 있는 환경을 제공한다.

 

* 바이트 코드 : 특정 하드웨어가 아닌 가상 머신에서 사용되는 언어

* 바이너리 코드 : 컴퓨터가 이해할 수 있는 언어로 OS마다 다름

 

JVM의 동작 방식


 

JVM의 동작을 이해하기 앞서 간단히 흐름만 정리하였다.

앞으로 소개할 내용에서 각 구성요소들이 구체적으로 어떤 처리를 하는지 이해하기 위해 흐름을 파악하고 넘어가자.

 

  1. 컴파일러가 자바 소스를 바이트 코드로 컴파일 

  2. main 메서드를 포함한 클래스를 실행시킴

  3. JVM의 클래스 로더는 컴파일된 코드를 RuntimeDataArea에 적재

  4. 실행 엔진(Execution Engine)이 바이트 코드를 해석하여 실행함

 

Class Loader의 동작 방식


클래스 로더는 클래스 파일을 검증하고 JVM내의 메모리 영역에 적재하는 역할을 한다.

클래스 로더는 런타임 시점에 클래스를 메모리에 적재하는 동적 로딩 특징을 갖고 있다.

각각의 방식에 대해 알아보자.

[동적 로딩]

1. 구동 시점 동적 로딩

 
public class Main {
  public static void main(String[] args) {
      System.out.println("hello");
  }
}

java Main 이라는 명령어를 통해 위 자바 프로그램을 실행하게 되면

클래스 로더가 main 메서드를 포함한 클래스와 java.lang, java.util 등의 자바 기본 API 클래스 파일을 메모리에 적재한다.


2. 지연로딩(Lazy Loading)

public class Main {
    public static void main(String[] args) {
        System.out.println("Start main");
        Sub sub = new Sub();
        sub.doWork();
    }
}

class Sub {
    public void doWork() {
        System.out.println("work");
    }
}
Main과 기본 API 클래스를 제외한 Main과 참조 관계가 존재하는 클래스들은 실제 출되는 시점에 로드된다.

위에서 Main은 구동시점에 로드되고 Sub는 main메서드에 의해 호출되는 시점에 로드된다.

잠깐. main과 참조관계가 없는 클래스 파일의 적재 시점

main 메서드의 실행에서 참조관계가 없을 수도 있다.
예를 들어 웹 프로그램에서 클라이언트의 요청을 받아서 최초 실행되는 클래스들은 요청 시점에 로드된다.
지연로딩 방식으로 구동시점이 아닌 실행 시점에 클래스 로더가 동작할 수 있다.
JVM의 지연로딩으로 인해 런타임에서 느린 성능을 유발할 수 있다.
허나, 사용되지 않는 클래스는 메모리에 적재하지 않으므로 메모리 자원 측면에서 효율
을 가져다 줄 수 있다.

 

3. 명시적 동적 로딩

Class.forName("com.example.MyClass")

명시적 동적 로딩은 코드를 통해 직접 클래스를 로드하도록 명시할 수 있는 방식이다.

위와 같이 리플렉션을 통해 필요로 하는 클래스를 로드하게 할 수 있다.

개발자가 직접 로딩 시점을 설정할 수 있다.

 

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

 

  1. 로딩 (Loading) : 클래스 파일을 가져와서 JVM 메모리 영역(Run Time Data Area)에 적재

  2. 링크 (Linking)
    2.1 검증 (verifying) : JVM 명세에 명시된대로 구성되어 있는지 검사 -> 검증 실패시 VerifyError 발생
                                      - ex1. 유효한 바이트 코드 여부 검사 (컴파일러가 하는 문법적 검사X)
                                      - ex2. 상속관계에 있는 클래스 파일 존재 여부 파악 (상속관계 클래스 함께 가져옴, 지연로딩 X)
    2.2. 준비 (preparing) : 정적 변수를 기본값으로 할당 (0, null, false 등)
    2.3. 해석 (resolving) : 상수 풀 내 심볼릭 레퍼런스(파일 경로)를 다이렉트 레퍼런스(메모리 주소)로 변경한다.

  3. 초기화 (Initializing) : 클래스 파일의 코드를 읽으며 정적변수에 값 할당, static 블록 초기화 실행

 

Execution Engine의 동작 방식


클래스 로더에 의해 JVM 메모리에 할당된 바이트 코드를 실행시키는 엔진

코드를 실행하는 방식은 크게 2가지로 InterpreterJIT Compiler가 존재한다.

 

Interpreter

  • 바이트 코드를 한줄씩 해석하여 실행
  • 바이트 코드 한줄의 해석은 빠르지만 실행시 마다 매번 해석하여 반복 작업이 많은 경우 속도 저하
    (같은 메서드라도 여러번 호출될 때 매번 새로 수행해야 함)

* 자바는 기본적으로 인터프리터 방식으로 동작함

 

JIT (Just In Time) Compiler

  • Interpreter의 단점을 보완하기 위해 도입했음
  • 실행 시점에 바이트 코드 전체를 기계어로 변환하여 실행하는 방식
    한번 변환한 기계어는 지속적으로 재사용하므로 반복 수행 명령어가 많은 경우 인터프리터 보다 빠르다.
    허나, 명령어 한줄의 해석 시간은 인터프리터 보다 느려 반복 수행 명령어가 많은 경우에 사용이 유리하다.
JIT 활성화 조건

자바는 기본적으로 Interpreter 방식으로 바이트 코드를 실행시킨다.
허나, 임계치 도달하면 JIT 방식으로 코드를 실행을 변경시킨다.
* (임계치) = (메서드가 호출된 횟수) + (메서드 루프문 반복 횟수)
* 임계치 기본값은10,000번

 

Runtime Data Area의 구조


런타임 데이터 영역은 JVM 메모리 영역이라고 불리우며 자바 애플리케이션을 실행하며 사용하는 데이터들을 적재하는 영역이다.

 

Method Area - 클래스 구조(메서드, 생성자, 필드)와 static 변수 및 메서드, 상수 저장
- 정적 영역이라고 불리움
Heap Area - new 연산자로 생성된 인스턴스(참조 타입)와 배열 저장
- 가비지 컬렉터의 관리 대상
Stack Area - 메서드 실행시 발생되는 임시 정보( 매게변수, 지역변수, 연산, 리턴값, 참조주소 ) 저장 공간
- 원시타입 변수는 스택 영역에 직접 값을 갖고, 참조 타입은 heap 영역의 주소를 갖고 있다.
PC Register
(Program Counter) 


- 현재 실행 해야할 바이트 코드 명령어 주소를 저장
- 스레드 단위로 관리되기에 스레드가 시작될 때 초기화 되며 스레드 종료시 제거됨
Native Method Stack Area - 자바 외 언어로 작성된 네이티브 코드를 위한 Stack
- 네이티브 메소드의 매개변수, 지역변수 등을 바이너리 코드로 저장
- JIT에 의해 실행되는 명령어가 이 영역에서 실행된다.

 

스레드 공유 여부

 

스택 영역과 PC 레지스터는 스레드마다 생성되는 영역으로 스레드 종료시 사라지는 영역이며,

힙 영역과 메서드 영역은 모든 스레드가 공유하는 영역으로 프로그램 종료시 제거되는 영역이다.

 

1. Heap Area

 Heap 영역은 Young Generation과 Old Generation으로 나뉜다. 

 

Young Generation

  • Eden : new 연산자를 통해서 생성된 객체가 위치
  • Survivor 0 / Survivor 1 : Eden 영역에서 GC에 의해 제거되지 않고 살아남은 객체가 S0, S1으로 차례대로 이동됨
                                         (S0에서 GC 처리 후 살아남은 객체가 S1으로 이동)
  • 마이너 가비지 컬렉션이 사용됨


Old Generation

  • Young Generation에서 살아남은 객체가 존재
  • Old 영역은 Young 영역보다 크게 할당되어 있으며 old 영역에서는 가비지컬렉터가 young 영역보다 적게 발생
  • 메이저 가비지 컬렉션 사용됨

 

2. Stack

메서드를 실행하며 발생하는 변수들의 정보가 저장되는 영역으로 메서드 호출시마다 메서드마다 스택 프레임이 생성되고

매개변수, 지연변수, 리턴 값 및 연산 결과들이 저장되고 메서드 종료시 스택 프레임은 삭제된다.

스택과 힙의 차이

1. 사이즈 결정 시점
스택의 사이즈는 컴파일 타임에 크기가 결정, 힙 사이즈는 런타임에 결정된다.
메서드 단위로 생성되는 스택프레임은 지역변수 배열, operand(피연산자) 스택, 상수풀 레퍼런스로 이루어져있다.
지역변수 배열과 operand 스택은 컴파일 타임에 크기가 결정되기에 스택은 컴파일 타임에 크기가 결정된다.

2. 메모리 관리
스택은 메서드 종료시 스택 프레임이 제거되므로 할당된 변수가 빠르게 제거된다.
허나, 힙은 가비지 컬렉터에 의해 시간의 흐름에 따라 참조되는지 여부를 따져 제거하는 방식을 따른다.

3. 지역성 및 연속성 - 성능
스택 프레임은 LIFO 자료구조로 스택에 데이터를 연속적으로 할당하는 특징을 지닌다.
이는 실제 메모리상에 인접한 위치에 데이터를 할당하는 것을 의미한다. 
반면, 힙의 경우 지역성과 연속성을 보장하지 않고 임의의 위치에 할당한다.
이는 CPU의 접근 성능에 차이를 나타내는데. 지역적이고 연속적인 공간에 저장된 데이터에 접근 하는것이 더욱 빠르므로 스택이 힙보다 빠른 성능을 보인다.

 

3. PC 레지스터

CPU에 PC 레지스터가 존재함에도 JVM은 별도의 PC 레지스터 공간을 갖는다.
CPU의 레지스터 공간은 기계어만 처리할 수 있기에 중간 언어인 바이트 코드는 처리할 수 없다.
따라서 PC 레지스터 공간에 바이트 코드 주소를 저장시키고 인터프리터에 의해 해석되어 수행되도록 한다.

 

[Register-base와 Stack-base 연산 비교]

PC 레지스터의 이해를 돕기 위해 Cpu Register를 활용하는 방식과 그렇지 않은 방식에서
1+2 연산을 어떻게 수행하는지 비교해보자.

  • Register-base 
    1) 피연산자(operand)인 1과 2가 cpu에 입력되고 이를 레지스터에 저장함
    2) 더하기라는 연산이 입력되고 레지스터의 1과 2를 가져와 더하는 명령(instrcution)을 수행함
        - 이때 모든 연산은 기계어이기에 레지스터에 저장할 수 있음

  • Stack-base
    1) 피연산자인 1과 2를 operand 스택에 적재

    2) 1+2 연산을 스택에 저장
        - 이때 1), 2)는 모두 바이트 코드 명령어로 이루어짐

    3) PC Register에 1+2 연산이 저장된 스택의 메모리 주소가 저장됨
    4) CPU는 PC Register에 저장된 메모리 주소를 참조하여 인터프리터에게 해석하도록 함
        즉, 연산을 CPU가 직접 하는 것이 아니라 PC Register의 주소를 참조하여 인터프리터가 수행하도록 하는 것.

Register-base를 사용하는 방식은 기계어를 사용하는 경우이고,
기계어가 아닌 경우 Stack-base와 같이 중간어를 기계어로 변환시킬 수 있는 자원을 활용하도록 한다.

 

따라서 자바에서도 인터프리터 방식으로 사용되는 경우에만 스택 기반으로 동작하고,
JIT방식으로 변환된 코드는 레지스터 기반 방식으로 동작한다는 것이다.

 

4. 네이티브 메서드 스택 

네이티브 메서드 스택 영역은 메서드 스택 영역과 역할을 같지만 네이티브 코드를 위한 스택이다.

JIT에 의해 실행되는 명령어가 이 영역에서 실행된다.

 

JVM 을 설명하며 가비지 컬렉터에 대한 설명은 하지 않았는데 가비지 컬렉터에 대한 설명은 아래 포스팅에서 진행하겠다.

 

https://developer111.tistory.com/entry/%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98

 

가비지 컬렉션

 

developer111.tistory.com

 

반응형