[JAVA] JVM 구조

RisingJade의 개발기록·2022년 3월 12일
0

JVM 구조


JAVA 작동 원리

시작하기 앞서 JAVA 작동 원리를 알아보자

  • 간단하게 설명하자면. 자바는 C, C++와 같이 컴파일러가 각 머신에 맞는 기계어로 컴파일하여 바로 돌리는 방법이 아니라 자바컴파일러(Javac)에서 자바 소스코드를 컴파일 후 생성된 자바 바이트 코드(.class) 파일을 해석(Interpret)과 Link없이 바로 JVM에 적재되고 JVM내의 실행 엔진이 JVM 메모리에 올라온 바이트 코드 명령어들을 하나씩 읽어서 해석하고 실행한다.
  • 이때, 가상머신은 자바 바이트코드를 어느 플랫폼에서나 동일한 형태로 실행시킬 수 있기 때문에, 자바로 개발된 프로그램은 CPU나 운영체제의 종류에 상관없이 JVM을 설치할 수 있는 시스템에서는 어디서나 실행할 수 있으며, 이 점이 웹 어플리케이션의 특성과 맞아떨어져 많이 쓰이게 되었다.

조금 더 자세한 설명

1. 개발자가 타이핑한 자바 소스(.java 파일)을 자바 컴파일러를 통해 자바 바이트 코드(.class)파일을 만들어 준다.

  • 자바 바이트코드: JVM이 읽는 코드로 아직 컴퓨터가 읽을 수 있는 기계어는 아니다. 자바 바이트 코드의 각 명령어는 1바이트 크기의 Opcode와 추가 피연산자로 이루어져 있다.

2. 컴파일된 바이트코드를 JVM의 클래스 로더에 전달

  • 2-1. 로드: 클래스 파일을 가져와서 JVM 메모리에 로드한다.
    2-2. 검증: 자바 언어 명세(Java Language Specification)및 JVM에 명세대로 구성됬는지 검사
    2-3. 준비: 클래스가 필요로 하는 메모리 할당(필드, 메서드, 인터페이스 등)
    2-4. 분석: 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경
    2-5. 초기화: 클래스 변수들을 적절한 값으로 초기화(ex. static 필드)

3. JVM의 클래스 로더는 동적 로딩(Dynamic Loading)을 통해 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역(Runtime Data area), 즉 JVM의 메모리에 올린다.

4. 실행엔진(Execution Engine)은 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행합니다. 이때, 실행엔진은 2가지 방식으로 동작할 수 있습니다.
  • 4-1. 자바 인터프리터 : 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하나하나의 실행은 빠르나, 전체적인 실행 속도가 느리다는 단점을 가진다.
  • 4-2. JIT 컴파일러(Just-In-Time Compiler) : 인터프리터의 단점을 보완하기 위해 도입된 방식으로 바이트 코드 전체를 컴파일하여 바이너리 코드로 변경하고 이후에는 해당 메서드를 더이상 인터프리팅 하지 않고, 바이너리 코드로 직접 실행하는 방식이다. 하나씩 인터프리팅하여 실행하는 것이 아니라 바이트 코드 전체가 컴파일된 바이너리 코드를 실행하는 것이기 때문에 전체적인 실행속도는 인터프리팅 방식보다 빠르다.

그래서 JVM 이란?

자바 가상 머신(영어: Java Virtual Machine, JVM)은 자바 바이트코드를 실행할 수 있는 주체이다. 일반적으로 인터프리터나 JIT 컴파일 방식으로 다른 컴퓨터 위에서 바이트코드를 실행할 수 있도록 구현되나 jop 자바 프로세서처럼 하드웨어와 소프트웨어를 혼합해 구현하는 경우도 있다. (이론적으로는 100% 하드웨어 구현도 가능하나 비효율적이다) 자바 바이트코드는 플랫폼에 독립적이며 모든 자바 가상 머신은 자바 가상 머신 규격에 정의된 대로 자바 바이트코드를 실행한다. 따라서 표준 자바 API까지 동일한 동작을 하도록 구현한 상태에서는 이론적으로 모든 자바 프로그램은 CPU나 운영 체제의 종류와 무관하게 동일하게 동작할 것을 보장한다.
-위키피디아


크게 3개의 subsystem으로 나뉜다

1. 자바 클래스 로더(class loader)

  • 클래스를 메모리에 올리는 클래스 로딩 기능을 담당한다.
    자바는 동적으로 클래스를 읽어온다. 모든 클래스는 그 클래스가 참조되는 순간 동적으로 JVM에 링크되며, 메모리에 로딩된다.

1) 로딩 : 클래스 종류와 경로에 따라 어떤 클래스로더에 의해 로드 될지 경정

2) 링킹 :

  • verify: Bytecode verifier로 javac로 생성된 자바 바이트 코드가 적절한지 검증
  • prepare: 모든 static 변수의 메모리가 할당 & default값들 할당
  • Resolve

3) Initialization :

  • 모든 Symbolic memory reference가 Method Area에 있는 original references로 대체

2. Runtime Data Areas

Runtime Data Area는 메모리에 올라와있는 자바 프로그램으로써
크게 5개의 주요 구성요소로 나뉘어져있습니다.

  • Method Area : static 변수들을 포함해서 클래스 수준의 모든 데이터가 이곳에 저장될 것입니다. JVM마다 오직 하나의 method area가 있으며 공유되는 리소스들입니다.

  • Heap Area : 모든 Objects와 그에 상응하는 instance 변수 그리고 배열들이 이곳에 저장됩니다. 물론 JVM마다 하나의 Heap Area가 존재합니다. Method areas 와 Heap areas는 여러 스레드들간에 공유되는 메모리입니다. 즉, 이 곳에 저장된 데이터들은 스레드에 안전하지 않습니다.

  • Stack Area : 각각의 스레드를 위해 분리된 런타임 스택이 생성됩니다. 메소드 호출마다, Stack Frame이라 불리는 하나의 Entry가 Stack memory가 생성됩니다. stack area는 스레드에 안전하지만 공유되는 리소스가 아닙니다. 또, Stack Frame은 3가지 Subentities로 나뉘어집니다.

    • Local Variable Array
    • Operand stack
    • Frame data
  • PC Register : 각 스레드는 현재 실행중인 상태 정보를 저장하는 개별적인 PC Register들을 가질 것이고 진행이 되면 지속적으로 갱신됩니다.

  • Native Method stacks : Native Method Stack 는 native method 정보를 가지고 있습니다. 각 스레드는 개별의 native method stack 이 생성될 것입니다.

3. Execution Engine

  • Runtime Data Area에 할당된 Java ByteCode를 Execution Engine이 실행시킵니다.
    The Execution Engine 은 bytecode 를 읽고 한줄 한줄 실행시킵니다.

Interpreter

  • interpreter는 bytecode를 빨게 해석하지만 느리게 실행한다. interpreter의 단점은 하나의 메소드를 여러번 호출할 때, 매번 새로운 해석이 요구된다는 것이다.

JIT Compiler

JIT Compiler는 interpreter의 단점을 없애준다. Execution Engine은 interpreter가 bytecode를 변환하는데 사용하지만 반복되는 코드를 발견했을 때 전체 bytecode를 컴파일하고 그것을 native code로 바꾸는 JIT compiler를 사용한다. 여기서 생성된 native code는 반복되는 메소드 호출에 대해 성능을 개선하기위해 직접적으로 사용됩니다.

Garbage Collector

참조되어 있지 않는 Objects들을 모으고 제거하는 역할을 합니다. Garbage Collection은 System.gc() 를 호출하므로 발생합니다. 하지만 실행이 보장되지는 않습니다.
Java Native Interface (JNI)
JNI 는 Native Method Libraries와 상화작용하며 Execution Engin을 위해 Native Libraries를 제공합니다.

Native Method Libraries

Execution Engine에서 필요한 Native Libraries의 모음입니다.

Reference:

profile
언제나 감사하며 살자!

0개의 댓글