본문 바로가기

Develop/JAVA

JVM(Java Virtual Machine)의 메모리 구조에 대한 설명

1. JVM(Java Virtual Machine)이란?

  • 자바 바이트 코드를 실행할 수 있는 주체
  • 자바와 OS 사이에서 중개자 역할을 수행하며 자바 프로그램을 운영체제에 구애받지 않고 실행할 수 있게 도와준다.
  • GC(Garbage Collector)를 통해 메모리 관리를 자동으로 수행한다.
  • 스택 기반의 가상머신
  • 기본 자료형을 제외한 모든 타입(클래스와 인터페이스)을 명시적인 메모리 주소 기반의 레퍼런스가 아니라 심볼릭 레퍼런스를 통해 참조한다.

자바 프로그램 실행 단계

개발자가 작성한 자바 소스 코드를 자바 컴파일러(javac)가 읽어들여 바이트 코드로(.class) 변환한다.

JVM은 이러한 바이트 코드를 읽어서 일련의 과정을 거쳐 해당 운영체제가 이해할 수 있는 기계어로 바꿔주는 역할을 한다.

2. JVM 구조

JVM 내부는 크게 4가지로 나뉜다.

1) Class Loader

JVM 내로 클래스 파일을 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈이다.

쉽게 풀어서 설명하면 Person.class와 같은 클래스 파일들을 엮어서 JVM이 운영체제로부터 할당 받은 메모리 영역(Runtime Data Area)에 적재하는 역할을 수행한다. 이때 런타임에 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크하게 된다. (lazy loading)

 

자바 클래스 로더의 특징은 다음과 같다.

 

  • 계층 구조: 클래스 로더끼리 부모-자식 관계를 이루어 계층 구조로 생성된다. 최상위 클래스 로더는 부트스트랩 클래스 로더이다.
  • 위임 모델: 계층 구조를 바탕으로 클래스 로더끼리 로드를 위임하는 구조로 동작한다. 클래스를 로드할 때 먼저 상위 클래스 로더를 확인하여 상위 클래스 로더에 있다면 해당 클래스를 사용하고, 없다면 로드를 요청받은 클래스 로더가 클래스를 로드한다.
  • 가시성 제한: 하위 클래스 로더는 상위 클래스 로더의 클래스를 찾을 수 있지만, 상위 클래스 로더는 하위 클래스 로더의 클래스를 찾을 수 없다.
  • 언로드 불가: 클래스 로더는 클래스를 로드할 수는 있지만 언로드할 수는 없다. 언로드 대신, 현재 클래스 로더를 삭제하고 아예 새로운 클래스 로더를 생성하는 방법을 사용할 수 있다.

클래스 로더의 위임 모델

각 클래스 로더는 로드된 클래스를 보관하는 네임스페이스를 가진다.

클래스를 로드할 때 이미 로드된 클래스인지 확인하기 위해 네임스페이스에 보관된 FQCN을 기준으로 클래스를 찾는다.

FQCN이 같더라도 네임스페이스가 다르다면 다른 클래스로 간주한다. (다른 클래스 로더가 로드한 경우)

 

클래스 로더가 클래스 로드를 요청 받으면, 클래스 로더 캐시 -> 상위 클래스 로더 -> 자기 자신의 순서로 해당 클래스가 있는지 확인한다.

즉, 이전에 로드된 클래스인지 클래스 로더 캐시를 확인하고, 없으면 상위 클래스 로더를 거슬러 올라가며 확인한다. 부트스트랩 클래스 로더(최상위 클래스 로더)까지 확인해도 없으면 요청받은 클래스 로더가 파일 시스템에서 해당 클래스를 찾는다.

 

계층 구조에 있는 각 클래스 로더의 특징은 다음과 같다.

 

  • Bootstrap Class Loader
    • JVM을 시작할 때 가장 먼저 로드된다.
    • Object 클래스를 비롯한 자바 API들을 로드한다.
    • Extension Class Loader를 로드한다.
    • 다른 클래스 로더와 달리 자바가 아닌 네이티브 코드로 구현되어 있다.
  • Extension Class Loader
    • 기본 자바 API를 제외한 확장 클래스들을 로드한다.
    • System Class Loader를 로드한다.
  • Application(System) Class Loader
    • 개발자가 애플리케이션 구동을 위해 작성한 대부분의 클래스들을 로드한다.
참고) https://d2.naver.com/helloworld/1230

2) Execution Engine

앞선 Class Loader에 의해 메모리에 적재된 클래스(바이트 코드)들을 기계어로 변경해 명렁어 단위로 읽어서 실행한다.

이때 명령어를 실행하는 방식은 2가지이다.

 

  • Interpreter 방식
    • 바이트 코드를 명령어 단위로 읽어서 한 줄씩 실행한다.
    • 인터프리터 언어의 단점을 그대로 가지고 있다. (속도가 느리다)
  • JIT(Just-In-Time) 방식
    • Interpreter 방식의 단점을 보완하기 위해서 도입되었다.
    • 인터프리터 방식으로 실행하다가 적절한 시점에 바이트 코드 전체를 컴파일해서 네이티브 코드로 변경하여 실행한다.
    • 이때 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 빠르게 실행된다.
    • JVM은 내부적으로 해당 메서드가 얼마나 실행되는지 체크하고 일정 수준을 넘어설 때 JIT 컴파일을 수행한다.

3) Garbage Collector

Heap 메모리 영역에 생성된 객체 중 참조되지 않는 객체들을 탐색 후 제거하는 역할을 한다.

이때 GC가 언제 동작하는지는 정확히 알 수 없다. (반은 맞고, 반은 틀린 말이라고 한다)

참고) GC 동작 방식
https://d2.naver.com/helloworld/1329
https://javabom.tistory.com/7?category=835783

4) Runtime Data Area

프로그램을 수행하기 위해 OS에서 할당 받은 JVM의 메모리 영역이다.

Runtime Data Area는 크게 5가지로 나뉜다.

  • Method Area
    • 모든 쓰레드가 공유하는 메모리 영역이다.
    • 클래스, 인터페이스, 메서드, 필드, static 변수와 같은 바이트 코드를 보관한다.
  • Heap
    • 모든 쓰레드가 공유하는 메모리 영역이다.
    • new 키워드를 통해 생성된 객체와 배열을 보관한다. 이때 생성되는 객체는 Method Area에 로드된 클래스여야 한다.
  • PC register
    • 쓰레드가 시작될 때 생성되며, 쓰레드마다 하나씩 존재한다.
    • 쓰레드가 어디 부분을 어떤 명령으로 실행해야할지에 대한 기록을 하며, 현재 수행 중인 JVM 명령의 주소를 가진다.
  • Stack Area
    • 메서드 호출 시마다 각각의 스택 프레임이 생성된다. (해당 메서드만을 위한 공간)
    • 지역 변수, 파라미터, 리턴 값, 연산에 사용되는 임시 값 등을 보관하는 영역이다.
    • 해당 메서드 수행이 끝나게 되면 스택 프레임 단위로 삭제한다.
  • Native method stack
    • 자바 외 언어로 작성된 네이티브 코드를 위한 메모리 영역이다.