JVM

Gunjoo Ahn·2022년 8월 3일
0

JVM에 대하여 간단하게 설명하자면 여러 OS에서 java code의 동일한 동작으로 보장해주는 가상 머신이다. JVM은 Java Virtual Machine의 약자로, 자바 가상 머신이다.

JVM은 두 가지 기본 기능이 있다. 방금 언급했던 어느 운영체제에서도 실행될 수 있게 하는 것(Java의 한 번 작성해, 어디에서나 실행 원칙), 그리고 메모리 관리이다. Java의 등장 전까지는 메모리를 개발자가 관리했으나 Java는 JVM의 Garbage Collection을 통해 메모리 관리를 자기가 “알아서” 한다.

JVM 특징

스택 기반의 가상 머신

  • 대표적인 컴퓨터 아키텍처인 인텔 x86 아키텍처나 ARM 아키텍처와 같은 하드웨어가 레지스터 기반으로 동작하는 데 비해 JVM은 스택 기반으로 동작한다.
  • stack 을 이용한 계산기와 비슷한 연산 로직

심볼릭 레퍼런스

  • 참고하는 클래스의 특정 메모리 주소를 참조 관계로 구성한 것이 아니라, 참조하는 대상의 이름만을 지칭한 것이다. 자바 바이트 코드(.class)가 JVM에 올라가게 되면 심볼릭 레퍼런스는 이름에 맞는 객체의 물리적인 주소를 찾아서 연결하는 작업을 수행한다.
  • 기본 자료형을 제외한 모든 타입을 명시적인 메모리 주소 기반의 레퍼런스가 아니라 심볼릭 레퍼런스를 통해 참조한다.

가비지 컬렉션

  • 클래스 인스턴스는 사용자 코드에 의해 명시적으로 생성되고 가비지 컬렉션에 의해 자동으로 파괴된다.

기본 자료형을 명확하게 정의하여 플랫폼 독립성 보장

  • C/C++ 등의 전통적인 언어는 플랫폼의 따라 int 형의 크기가 변한다. 반면 JVM은 기본 자료형을 명확하게 정의하여 호환성을 유지하고 플랫폼 독립성을 보장한다.

네트워크 바이트 오더

  • 자바 클래스 파일은 네트워크 바이트 오더를 사용한다. 인텔 x86 아키텍처가 사용하는 리틀 엔디안이나 RISC 계열 아키텍처가 주로 사용하는 빅 엔디안 사이에서 플랫폼 독립성을 유지하려면 고정된 바이트 오더를 유지해야하므로 네트워크 전송 시에 사용하는 바이트 오더인 네트워크 바이트 오더를 사용한다. 네트워크 바이트 오더는 빅 엔디안이다.

자바프로그램 실행 과정

그림 링크

  1. 프로그램이 실행되면 JVM은 OS로부터 메모리 할당을 받는다. JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다
  2. 자바 컴파일러가 자바 소스코드를 읽어들여 자바 바이트코드로 변환시킨다.
  3. 클래스 로더를 통해 클래스파일들을 JVM으로 로딩한다
  4. 로딩된 클래스파일들은 Execution engine을 통해 해석된다
  5. 해석된 바이트코드는 Runtime Data Areas에 배치되어 실질적인 수행이 이루어진다
  6. 실행 과정 속에서 JVM은 필요에 따라 Thread Synchronization과 GC와 같은 관리 작업을 수행한다.

바이트 코드 - 가상머신이 기계어와 같이 효율적으로 저장하고 불러올 수 있는 형식

JVM 구성

Class Loader

자바는 동적 로드, 즉 컴파일 타임이 아니라 런타임(바이트 코드를 실행할 때)에 클래스를 로드하고 링크하는 특징이 있다. 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더이다.

jar 파일 내 저장된 클래스들을 JVM위에 탑재하고 사용하지 않는 클래스들은 메모리에서 삭제한다.

자바는 컴파일 타임이 아니라 런타임에 참조한다. 즉 클래스를 처음으로 참조할 때, 해당 클래스를 로드하고 링크한다.

클래스 로더에는 로딩, 링크, 초기화 단계로 나뉘어져 있다.

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

Execution Engine

클래스를 실행시키는 역할이다. 클래스 로더가 JVM내의 런타임 데이터 영역에 바이트 코드를 배치시키고, 이것을 실행엔진이 해석, 실행한다. 메모리에 적재된 바이트 코드를 기계어로 변경하여 실행하는 것이다. Execution engine이 바이트 코드를 기계어 명령어 단위로 읽어서 수행하는데 두 가지 방식이 사용된다.

Interpreter

Execution engine은 자바 바이트 코드를 명령어 다뉘로 읽어서 실행한다. 하지만 이 방식은 인터프리터 언어의 단점을 그대로 가지고 있다. 한 줄 씩 수행하기에 느리다. 당연히 컴파일 언어보다 느리다.

의문점 - 컴파일 최적화는 언제?

⇒ 바이트 코드로 컴파일 할 때는 거의 최적화 안한다

JIT에서 최적화를 한다.

Just In Time (JIT) Compiler

인터프리터의 속도 이슈를 해결하기 위한 방법
자주 실행되는 바이트 코드 영역을 런타임 중에 기계어로 컴파일하여 사용

이후에는 더이상 해당 코드를 인터프리팅 하지 않고 기계어(네이티브 코드)로 직접 실행한다.
네이티브 코드는 캐시에 보관되어 한번 컴파일된 코드는 빠르게 수행할 수 있다.

Garbage Collector

GC를 수행한다

Runtime Data Areas


그림 링크

JVM이 운영체제 위해 실행될 때, 운영체제로부터 할당 받은 메모리 영역이며, 총 6개의 메모리 영역으로 분할하여 관리한다. 이 영역들은 스레드가 공유하는 공간인지 아닌지로 나눈다.

스레드마다 생성되는 공간

  • PC 레지스터
  • JVM 스택
  • 네이티브 메소드 스택

모든 스레드가 공유하는 공간

  • 메소드
  • 런타임 상수 풀

PC 레지스터

스레드가 시작될 때 생성된다. 메소드 안에서 바이트 코드 몇 번째 줄을 실행하고 있는지와 같은 정보를 가지고 있다. 현재 수행 중인 JVM 명령의 주소를 가지고 있는 것이다.

PC 레지스터에는 현재 실행 중인 메서드가

  • 네이티브 메서드가 아니면 현재 실행 중인 JVM 명령어의 위치가 저장되고,
  • 네이티브 메서드이면 PC 레지스터에 저장되는 값은 정의되지 않는다(undefined).

JVM 스택

스택 프레임이라는 구조체를 JVM 스택에 쌓는다. 기존 process call stack 처럼 새로운 메소드가 호출 될 때마다 push, 끝나면 pop 동작을 수행한다. 프로그램 실행과정에서 임시로 할당되었다가 메소드를 빠져나가면 사라지는 영역이다.

스레드가 쓸 수 있는 스택의 사이즈를 넘기게 되면 StackOverflowError가 발생한다.

스택 사이즈를 동적으로 확장할 수도 있는데 확장할 메모리가 부족하거나, 새로운 스레드를 만들 때 필요한 새로운 스택에 할당할 메모리가 부족하면 OutOfMemoryError가 발생한다.

스택 프레임은 다음 세 가지로 구성

  • Local Variable Array
  • Operand Stack
  • Constant Pool Reference

Local Variable Array

메소드의 로컬 변수들을 배열로 가지고 있다

  • reference는 heap의 레퍼런스를 의미한다.
  • primitive 타입은 값을 그냥 프레임에 저장한다.
    • 그래서 int나, double이 IntegerDouble보다 조금 더 빠르다.
  • doublelong은 두 칸씩 차지한다.

Operand Stack

오퍼랜드 스택은 메소드 내 계산을 위한 작업 공간이다. 스택 기반의 가상 머신이기에 사용하는 스택 공간이다. 예시는 링크 참조

Constant Pool Reference

프레임은 런타임 상수 풀의 참조를 가진다. 상수는 상수 풀에서 참조해서 사용한다.
참고) String을 new해서 쓰지 않는 이유

JVM stack은 실제 OS process에서 stack 메모리일까 heap 메모리일까?
JVM stack은 인터페이스일뿐, 실제 OS에서 어디에든 할당 할 수 있다

네이티브 메소드 스택

네이티브 메소드 스택은 자바 바이트 코드가 아닌 다른 언어로 작성된 네이티브 코드를 위한 스택이다. 성능 향상을 목적으로 작성되었다.

JVM 스택과 네이티브 스택이 나뉘어져 있다 하더라도 자바 코드를 수행하다 JNI(Java Native Interface)를 호출하면, JVM 스택에서 네이티브 메소드 스택으로 다이나믹 링킹을 통해 확장할 뿐이다.

네이티브 메소드가 네비티브 메소드 스택(OS에서 관리되는 스택)을 잡아 독자적으로 프로그램을 실행시키는 영역이다. 이 부분을 통해 C 코드를 실행시켜 커널에 접근할 수 있다.

힙 영역

모든 스레드가 공유하며, 런타임시 할당된다. new 키워드로 생성된 객체와 배열이 생성되는 영역이다. 또한, 메소드 영역에 로드된 클래스만 생성이 가능하고 Garbage Collector가 참조되지 않는 메모리를 확인하고 제거하는 영역이다.


그림 링크 1
그림 링크 2

Java 8부터 Permanent Generation 메모리 영역이 없어지고 Metaspace 영역이 생겼다.

메소드 영역

JVM이 시작될 때 생성된다. 클래스 정보를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 메모리 공간이다.

클래스 로더가 클래스 파일을 읽어 오면, 클래스 정보를 파싱하여 런타임 상수 풀, 필드와 메소드 정보, static 변수, 인터페이스, 메소드의 바이트 코드 등을 보관한다.

메소드 영역은 JVM 벤더마다 다양한 형태로 구현할 수 있으며, 오라클 핫스팟 JVM에서는 흔히 PermGen(자바 1.7 이전), MetaSpace(자바 1.8 이후)로 부른다.

메소드 영역에 대한 GC도 JVM 벤더의 선택 사항이다.

Java 8이전에 Method Area를 PermGen(Permanat Generation Space)에 할당 했다.
Java 8이후에는 PermGen이 완전히 제거 되어 Method Area는 Native Heap에 할당 된다. 과거의 PermGen은 현재 Metaspace 로 불리우는데 Method Area는 Meta 정보를 저장 하기 때문이다. - MIA_DAHAE

런타임 상수 풀

런타임 상수 풀은 메소드 영역에 포함되는 영역이긴 하지만, JVM 동작에서 가장 핵심적인 역할을 수행하는 곳이기 때문에 JVM 명세에서도 따로 중요하게 기술한다.

각 클래스와 인터페이스의 상수 뿐만 아니라, 메소드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다.

즉, 어떤 메소드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메소드나 필드의 실제 메모리 상 주소를 찾아서 참조한다.

Reference

#자바가상머신, JVM(Java Virtual Machine)이란 무엇인가?
[Java] JVM이란?
JVM 이해하기 - 1 (JVM 특징 이해하기)
JVM 메모리 구조란? (JAVA)
Back to the Essence - Java 컴파일에서 실행까지

profile
Backend Developer

0개의 댓글