스터디/Java 스터디

[Java] JVM이란?

제이온 (Jayon) 2022. 1. 29.

java-study에서 스터디를 진행하고 있습니다.

자바 코드 실행 과정

 

위 그림은 자바 코드의 실행 과정을 간략하게 보여 준다.

 

  1. 프로그램이 실행되면 JVM은 OS로부터 이 프로그램이 필요로 하는 메모리를 할당받는다. (JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.)
  2. 자바 파일(.java)이 자바 컴파일러에 의해 자바 바이트 코드(.class)로 변환된다.
  3. 클래스 로더를 통해 자바 바이트 코드를 JVM으로 필요한 시점에 로딩한다.
  4. 해석된 바이트 코드는 런타임 데이터 영역에 배치되어 실질적인 수행이 이루어지게 된다.
  5. 실행 과정 속에서 JVM은 필요에 따라 GC와 같은 관리 작업을 수행한다.

 

자바 코드가 실행되는 과정을 살펴보면 C/C++과는 다르다는 것을 알 수 있다. C/C++은 운영체제 별로 컴파일러가 존재하여 해당 운영체제가 인식할 수 있는 기계어로 변환된다. 반면 자바는 자바 컴팡일러 하나만 존재하며, 기계어가 아닌 중간 단계의 바이트 코드로 변환된다. 또한 운영체제가 아닌 JVM에 의해 실행된다.

 

바이트 코드란?

바이트 코드란 JVM에서 작동하도록 만든 이진 코드이다. 즉, JVM이 이해할 수 있는 언어로 변환된 코드이며 명령어의 크기가 1 바이트라서 자바 바이트 코드라고 불리고, 자바 코드를 배포하는 가장 작은 단위이다. 확장자는 .class이다.

 

JVM의 특징

  • 스택 기반의 가상 머신
    • 대표적인 컴퓨터 아키텍처인 인텔 x86 아키텍처나 ARM 아키텍처와 같은 하드웨어가 레지스터 기반으로 동작하는 데 비해 JVM은 스택 기반으로 동작한다.
  • 심볼릭 레퍼런스
    • 참고하는 클래스의 특정 메모리 주소를 참조 관계로 구성한 것이 아니라, 참조하는 대상의 이름만을 지칭한 것이다. 자바 바이트 코드(.class)가 JVM에 올라가게 되면 심볼릭 레퍼런스는 이름에 맞는 객체의 물리적인 주소를 찾아서 연결하는 작업을 수행한다.
    • 기본 자료형을 제외한 모든 타입을 명시적인 메모리 주소 기반의 레퍼런스가 아니라 심볼릭 레퍼런스를 통해 참조한다.
  • 가비지 컬렉션
    • 클래스 인스턴스는 사용자 코드에 의해 명시적으로 생성되고 가비지 컬렉션에 의해 자동으로 파괴된다.
  • 기본 자료형을 명확하게 정의하여 플랫폼 독립성 보장
    • C/C++ 등의 전통적인 언어는 플랫폼의 따라 int 형의 크기가 변한다. 반면 JVM은 기본 자료형을 명확하게 정의하여 호환성을 유지하고 플랫폼 독립성을 보장한다.
  • 네트워크 바이트 오더
    • 자바 클래스 파일은 네트워크 바이트 오더를 사용한다. 인텔 x86 아키텍처가 사용하는 리틀 엔디안이나 RISC 계열 아키텍처가 주로 사용하는 빅 엔디안 사이에서 플랫폼 독립성을 유지하려면 고정된 바이트 오더를 유지해야하므로 네트워크 전송 시에 사용하는 바이트 오더인 네트워크 바이트 오더를 사용한다. 네트워크 바이트 오더는 빅 엔디안이다.

 

JVM의 구조

 

클래스 로더

자바는 동적 로드, 즉 컴파일 타임이 아니라 런타임(바이트 코드를 실행할 때)에 클래스를 로드하고 링크하는 특징이 있ㄷ. 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더이다. 클래스 로더에는 로딩, 링크, 초기화 단계로 나뉘어져 있는데, 간단히 설명하면 다음과 같다.

 

  • 로드
    • 각 자바 바이트 코드(.class)는 JVM에 의해 메소드 영역에 다음 정보를 저장한다.
      • 로드된 클래스를 비롯한 그의 부모 클래스의 정보
      • 클래스 파일과 Class, Interface, Enum의 관련 여부
      • 변수나 메소드 등의 정보
  • 링크
    • 검증: 읽어 들인 클래스가 자바 언어 명세 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사한다.
    • 준비: 클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메소드, 인터페이스를 나타내는 데이터 구조를 준비한다.
    • 분석: 심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체한다.
  • 초기화
    • 클래스 변수를 적절한 값으로 초기화한다. 즉, static 필드들이 설정된 값으로 초기화된다.

 

런타임 데이터 영역

런타임 데이터 영역은 JVM이 운영 체제 위에서 실행될 때, 할당 받는 메모리 영역이며 총 6개의 영역으로 나눌 수 있다. 이 영역들은 스레드가 공유하는 공간인지 아닌지로 나눈다.

 

  • 스레드마다 하나씩 생성되는 공간
    • PC 레지스터
    • JVM 스택
    • 네이티브 메소드 스택
  • 모든 스레드가 공유하는 공간
    • 메소드
    • 런타임 상수 풀

 

PC 레지스터

  • PC 레지스터는 각 스레드마다 하나씩 존재하며, 스레드가 시작될 때 생성된다.
  • PC 레지스터는 메소드 안에서 바이트 코드 몇 번째 줄을 실행하고 있는지와 같은 정보를 갖고 있다.

 

JVM 스택

  • JVM ㅅ택은 각 스레드마다 하나씩 존재하며, 스레드가 시작될 때 생성된다.
  • JVM 스택은 스택 프레임이라는 구조체로 이루어져 있는데, 새로운 메소드가 호출될 때마다 push, 메소드 실행이 끝나면 pop 동작을 수행한다.
  • 각 스택 프레임은 지역 변수 배열, 피연산자, 프레임 데이터를 갖는다.
    • 프레임 데이터는 현재 실행 중인 메소드가 속한 클래스의 런타임 상수 풀, 이전 스택 프레임에 대한 정보, 현재 메소드가 속한 클래스, 객체에 대한 참조 등을 말한다.

 

 

네이티브 메소드 스택

  • 네이티브 메소드 스택은 자바 바이트 코드가 아닌 다른 언어로 작성된 네이티브 코드를 위한 스택이다.
  • 성능 향상을 목적으로 작성되었다.
  • JVM 스택과 네이티브 스택이 나뉘어져 있다 하더라도 자바 코드를 수행하다 JNI를 호출하면, JVM 스택에서 네이티브 메소드 스택으로 확장할 뿐이다.

 

 

힙 (Heap)

  • 힙은 모든 스레드가 공유하는 영역이다.
  • 힙은 프로그램을 실행하면서 생성된 모든 인스턴스 또는 객체를 저장하는 공간이다.

 

 

메소드 영역

  • 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다.
  • 클래스 로더가 클래스 파일을 읽어 오면, 클래스 정보를 파싱하여 런타임 상수 풀, 필드와 메소드 정보, static 변수, 메소드의 바이트 코드 등을 보관한다.
  • 메소드 영역은 JVM 벤더마다 다양한 형태로 구현할 수 있으며, 오라클 핫스팟 JVM에서는 흔히 PermGen(자바 1.7 이전), MetaSpace(자바 1.8 이후)로 부른다.
  • 메소드 영역에 대한 GC도 JVM 벤더의 선택 사항이다.

 

런타임 상수 풀

  • 런타임 상수 풀은 메소드 영역에 포함되는 영역이긴 하지만, JVM 동작에서 가장 핵심적인 역할을 수행하는 곳이기 때문에 JVM 명세에서도 따로 중요하게 기술한다.
  • 각 클래스와 인터페이스의 상수 뿐만 아니라, 메소드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다.
  • 즉, 어떤 메소드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메소드나 필드의 실제 메모리 상 주소를 찾아서 참조한다.

 

실행 엔진

  • 실행 엔진은 메모리에 적재된 바이트 코드(.class)를 기계어로 변경하여 명령어(instruction) 단위로 실행 및 바이트 코드를 운영체제에 맞게 해석해주는 역할을 수행한다. 실행 엔진이 바이트 코드를 명령어 단위로 읽어서 수행하는데 크게 두 가지 방식이 사용된다.
    • 인터프리터
      • 런타임 중에 바이트 코드를 한 줄씩 읽고 실행한다.
      • 컴파일보다 속도가 느리다.
    • JIT (Just In Time)
      • 인터프리터의 속도 이슈를 해결하기 위해 같이 사용한다.
      • 자주 실행되는 바이트 코드 영역을 런타임 중에 기계어로 컴파일하여 사용한다.

 

 

 

  • 정확히는 JIT 컴파일러가 “번역 안할래”는 아니고 인터프리터가 해당 바이트 코드를 읽지 않아도 된다는 것을 뜻한다.

 

바이트 코드 분석

바이트 코드를 분석하며, 스레드가 런타임 데이터 영역을 어떻게 활용하는지 살펴보자.

 

코드

public class Main {

    public static void main(String[] arge) {
        double a = 10.0;
        double b = 10.0;
        double c = 10.0;

        a = b + c * 100;
    }
}

 

바이트 코드

Code: 
0: ldc2_w #2 // double 10.0d 
3: dstore_1 
4: ldc2_w #2 // double 10.0d 
7: dstore_3 
8: ldc2_w #2 // double 10.0d 
11: dstore 5 
13: dload_3 
14: dload 5 
16: ldc2_w #4 // double 100.0d 
19: dmul 
20: dadd 
21: dstore_1 
22: return

 

  • 0: ldc2_w #2
    • 상수 풀 2번 인덱스에 저장된 10.0을 연산자 스택에 push한다.
  • 3: dstore_1
    • 연산자 스택에 저장된 10.0을 pop하여 지역 변수 배열 1번 인덱스에 저장한다.

 

 

  • 4: ldc2_w #2
    • 상수 풀 2번 인덱스에 저장된 10.0을 연산자 스택에 push한다.
  • 7: dstore_3
    • 연산자 스택에 저장된 10.0을 pop하여 지역 변수 배열 3번 인덱스에 저장한다.

 

 

  • 8: ldc2_w #2
    • 상수 풀 2번 인덱스에 저장된 10.0을 연산자 스택에 push한다.
  • 11: dstore_5
    • 연산자 스택에 저장된 10.0을 pop하여 지역 변수 배열 5번 인덱스에 저장한다.

 

 

  • 13 : dload_3
    • 지역 변수 배열 3번 인덱스에 저장된 10.0을 연산자 스택에 push 한다.
  • 15 : dload_5
    • 지역 변수 배열 5번 인덱스에 저장된 10.0을 연산자 스택에 push 한다.
  • 16 : ldc2_w #4
    • 상수 풀 4번 인덱스에 저장된 100.0을 연산자 스택에 push 한다.

 

 

  • 19 : dmul
    • 연산자 스택에 저장된 100.0과 10.0을 pop하여 곱하기 연산을 한 뒤 다시 push 한다.
  • 20 : dadd
    • 연산자 스택에 저장된 1000.0과 10.0을 pop하여 더하기 연산을 한 뒤 다시 push 한다.
  • 21 : dstore_1
    • 연산자 스택에 저장된 1010.0을 pop하여 지역 변수 배열 1번 인덱스에 저장한다.

 

 

  • 22: return
    • 메소드를 종료한다.

 

출처

 

예상 면접 질문 및 답변

자바의 실행 방식

  1. 자바 프로그램이 실행되면 JVM은 OS로부터 프로그램이 필요로 하는 메모리를 할당 받는다.
  2. 자바 파일이 자바 컴파일러에 의해 자바 바이트 코드로 변환된다.
  3. 클래스 로더를 통해 자바 바이트 코드를 JVM으로 필요한 시점에 로딩한다.
  4. 해석된 바이트 코드는 런타임 데이터 영역에 배치되어 수행이 이루어지게 된다.
  5. 실행 과정 속에서 JVM은 필요에 따라 GC같은 관리 작업을 수행한다.

 

JVM의 정의

자바 프로그램은 자바 컴파일러에 의해 자바 바이트 코드로 번역되는데, JVM은 OS를 대신해서 컴파일된 자바 바이트 코드를 실행하는 가상의 운영체제 역할을 한다. 따라서, 자바 프로그램은 플랫폼에(=운영체제, CPU)에 종속되지 않는다. 다만, JVM은 플랫폼에 의존하기 때문에 OS에 맞는 JVM을 설치해야 한다.

 

JVM의 특징

  • 스택 기반의 가상 머신
  • 심볼릭 레퍼런스
  • 가비지 컬렉션(garbage collection)
  • 기본 자료형을 명확하게 정의하여 플랫폼 독립성 보장

 

JVM의 구조

JVM은 크게 세 가지 영역으로 나눌 수 있다.

 

  • 클래스 로더
  • 런타임 데이터 영역(메모리)
  • 실행 엔진

 

댓글

추천 글