본문 바로가기

자바

[Java] 자바 가상 머신 이해(1) - JVM 구조

자바 개발자라면 모를 수 없는 키워드, JVM.

대학 졸업할 때까지 “JVM 은 자바로 작성한 클래스를 기계어로 번역해주는 역할을 한다” 정도로 이해하고 있었다. 하지만 기술 면접을 찾아보면 JVM에 대해 물어보는 경우가 많다. 학교 생활에 충실했느냐를 판단하고자 물어보는 줄 알고 적당히 알고 넘어가려고 하다가 성능과도 연결되는 것을 알게 되었다.

jvm 메모리 영역의 구분과 각 영역의 역할에 대한 이해는 자바 성능 최적화에 중요하다.

 

이에 차근차근 공부한 내용을 정리해보고자 한다

개요

자바 가상 머신(Java Virtual Machine), JVM이 등장하기 이전 모든 컴퓨터 프로그램은 특정 운영체제에 맞게 작성되었다. 또한 프로그램 메모리는 소프트웨어 개발자가 관리했다. 이런 기존의 틀을 깨부수고 등장한 것이 바로 JVM이다.

JVM은 시스템 메모리를 관리하고 최적화하는 자바 기반 애플리케이션 실행 환경을 제공한다. 또한 “한번 작성해 어디서나 실행한다”는 유명한 원칙이 있다. 어느 운영체제 상에서도 실행될 수 있도록 한다는 것이다. 이것은 이후로 나온 현대 프로그래밍 언어에서도 적용되기 시작한다.

 

JVM의 정의는 크게 두가지 관점으로 나뉜다

  1. 기술적 관점
    • 코드를 실행하고 해당 코드에 대해 런타임 환경을 제공하는 소프트웨어 프로그램에 대한 사양(Specification)
  2. 일반적 관점
    • 자바 프로그램을 실행하는 방법으로 실행 중에 프로그램 리소스를 관리한다.

JVM의 역할은 자바 애플리케이션을 클래스 로더를 통해 읽어 자바 API와 함께 실행하는 것이다.

그렇다면 이건 어떻게 가능할 수 있는 걸까.

 

동작

  1. 자바 프로그램을 실행하면 OS로부터 JVM은 메모리를 할당받는다.
  2. 자바 컴파일러가 자바 소스(.java) 를 자바 바이트코드(.class)로 컴파일한다.
  3. Class Loader는 동적 로딩을 통해 필요한 클래스를 로딩 및 링크한다.
  4. 그 결과물은 Runtime Data Area로 올린다.
  5. Runtime Data Area에 로딩된 바이트 코드는 Execution Engine(실행엔진)을 통해 해석된다
  6. 이 과정에서 실행 엔진에 의해 GC(Garbage Collector, 가비지 콜렉터)의 작동과 스레드 동기화가 이뤄진다.

정리해서 생각해보면 JVM은 자바를 1차적으로 컴파일하고 운영체제로 넘겨주는 미들웨어 역할을 한다.

OS로부터 할당받은 메모리를 효율적으로 사용하여 최고의 성능을 내기 위해서 JVM에 대한 이해가 필요한 것이다.

JRE 는 자바를 실행하기 위한 환경의 집합이다. 그 안은 자바에서 제공하는 라이브러리를 포함하고 있다.

JDK는 자바 애플리케이션을 구축하기 위한 핵심 플랫폼 구성요소이다.

여기까지 공부해봤을 때 JVM은 자바를 위한 컴퓨터속의 가상 컴퓨터처럼 느껴졌다. (개인적으로)

클래스 파일을 클래스 로더를 활용해 Runtime Data Area의 메서드 영역으로 불러오고, 메모리 영역을 관리해서 데이터를 기억하고, Excecution Engine은 클래스 파일과 같은 바이트코드를 실행가능하도록 해석하고… (많이 들어봤을 GC(Garbage Collector)는 Excecution Engine에 위치한다.)

그렇다고 JVM 내부에 독립적인 OS 가 있는 것은 아니다. JVM을 관리되는 런타임 환경(Managed Runtime Environment) 또는 프로세스 가상 머신이라고 정의하는 것이 더 정확할 수도 있다.

구조

JVM은 다음과 같이 구성되어 있다.

Class Loader, Execution, Runtime Data Areas

Class Loader

클래스 로더는 JVM 내로 클래스 파일(*.class)를 동적으로 로드하고 링크를 통해 배치하는 작업을 수행한다. 즉, 로드된 바이트 코드(.class)를 맥락에 따라 액세스 가능한 서버 같은 것으로 JVM 내부의 Runtime Data Area에 배치한다.

클래스를 메모리에 올리는 로딩 기능은 어플리케이션에서 필요한 경우 동적으로 메모리에 적재하게 된다.

개발자의 관점에서 근본적인 클래스 로더 매커니즘은 대개 블랙박스다.

내부 동작은 위와 같은 순서로 진행된다.

  1. 클래스 파일을 가져와서 JVM 메모리에 로드한다.
  2. 클래스 파일을 사용하기 위해 검증을 거친다.
    • Verifying: 클래스가 JVM 명세대로 구성되었는지 검사한다.
    • Preparing: 클래스가 필요로 하는 메모리를 할당한다.
    • Resolving: 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
  3. 클래스 변수들을 적절한 값으로 초기화한다.
    • static 필드 초기화 등

💡 심볼릭 레퍼런스?

  • 참고하는 클래스의 특정 메모리 주소로 참조 관계를 구성한 것이 아닌, 참조 대상의 이름만을 지칭한 것
  • 이름을 통해 메모리 주소를 이끌어내는 방식에서 활용
  • 기본 자료형(primity data type)을 제외한 모든 타입(클래스와 인터페이스)의 경우 활용

 

Excecution Engine

위에서 클래스를 로딩하는 작업이 끝나면 Excecution Engine은 각 클래스에 있는 바이트 코드를 실행하기 시작한다.

실행 엔진은 JVM 실행에 필수적인 JVM 인스턴스다.

GC가 미사용 메모리를 해제하는 과정도 여기서 이뤄진다.

실행 엔진은 프로그램 실행과 운영체제 가운데에서 메모리, 파일 시스템 액세스, 네트워크 입출력을 위한 리소스를 관리한다.

또한 JVM이 여러 운영체제에서 호환되기 때문에 각각 운영체제 환경에 즉각 대응할 수 있어야 한다.

 

자바 바이트 코드(*.class)는 가상머신이 이해할 수 있는 중간 레벨로 컴파일된 코드로 실행엔진은 이것을 기계가 실행할 수 있는 형태로 변환한다. 이 때 두가지 방식으로 진행된다.

  1. Java Interpreter 방식하나씩 해석하고 실행하기 때문에 바이트 코드 하나하나의 해석은 빠르지만, 해석된 결과의 실행은 느리다.
  2. 바이트 코드를 명령어 단위로 읽어서 실행한다.
  3. JIT 컴파일러 방식이후 한번 컴파일 한 메서드는 네이티브 코드 형태로 캐시에 보관하여 한번 컴파일되면 빠르게 수행한다.
  4. 인터프리터 방식으로 실행하다가 일정 시간이 지나면 바이트 코드 전체를 컴파일하여 네이티브 코드로 변경한다.

 

💡 네이티브 코드: JAVA에서 부모가 되는 C언어, C++, 어셈블리어로 구성된 코드

실행 엔진이 이런 형식을 따르기 때문에 JIT 컴파일러를 사용하는 JVM들은 내부적으로 해당 메서드가 얼마나 자주 수행되는지 체크하고, 일정 정도를 넘을 때만 컴파일을 수행한다. 한번만 실행되는 코드라면 컴파일 대신 인터프리팅 방식을, 자주 사용한다면 컴파일을 통해 캐시에 저장해 활용하는 것이다.

런타임 데이터 영역 Runtime Data Area

위의 자료에서 익숙한 용어들이 보인다. Thread, PC, stack, Heap … 등등…

하나하나 들여다보자.

Method Area, Heap Area, Stack Area, PC Register, Native Method Stack

Method Area

  • Class area, Static area
  • 클래스 정보를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 메모리 공간
  • Runtime Constant Pool 영역을 통해 상수 자료형을 저장하여 참조하고 중복을 방지한다.
  • 클래스 데이터를 위한 공간

Heap Area

  • 자바로 구성된 객체 및 JRE 클래스가 탑재되는 영역
  • String Pool, 실제 데이터를 가진 인스턴스, 배열 등 저장
  • 해당 영역이 가진 데이터는 모든 JVM 영역에서 참조되고 스레드 사이에서 공유된다.
  • 영역이 가득 차면 OutOfMemoryError 발생

JVM Stack Area

  • 프로그램 실행 과정에서 임시로 할당되었다가 메소드를 나가면 소멸되는 데이터 저장
  • 메소드 호출할 때마다 스택 프레임이 생성되고 메서드 수행이 끝나면 프레임 별로 삭제한다.
  • 메소드 안에서 사용되는 값, 메소드의 매개변수, 지역변수, 리턴값 및 연산 시 일어나는 값 임시저장

💡 스택 프레임? 그 메서드만을 위한 공간

 

PC Register

  • Thread가 시작될 때 함께 생성되는 공간으로 스레드마다 존재
  • 현재 수행중인 JVM 명령의 주소를 갖는다.

Native method stack

  • 실제로 수행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역
  • 다른 프로그래밍 언어로 작성된 메서드 호출 코드를 수행하는 스택
  • 일반 프로그램처럼 커널이 스택을 잡아 독자적으로 프로그램을 실행하는 영역
  • 이 부분을 통해 C code 를 실행시켜 커널에 접근할 수 있다.