[Java] 자바의 심장 JVM(자바 가상 머신)

하원·2024년 6월 21일
post-thumbnail

안녕하세요, 하원입니다.
오늘은 자바의 핵심 특징이라고 말할 수 있는 JVM(Java Virtual Machine)에 대해 소개해 보겠습니다.


JVM이란?

JVM은 OS에 종속 받지 않고 자바를 실행시키기 위한 가상 머신이라고 할 수 있습니다.

위 그림과 같이 개발자가 java 소스코드인 .java 파일을 작성하게 되면, Java compiler가 해당 .java 파일을 JVM이 인식할 수 있는 java bytecode(.class 파일)로 변환시켜 줍니다.
그러면 해당 .class 파일을 JVM이 OS가 이해할 수 있는 기계어(binary code)로 변환시켜 주는 겁니다. 이 과정으로 인해 플랫폼 독립성이라는 자바의 강력한 장점이 등장하게 됩니다.


JVM의 핵심

그러면 JVM은 어떤 특징을 가지고 있을까요?

1. 플랫폼 독립성

위에서 말했듯이 자바의 가장 강력한 장점인 플랫폼 독립성입니다. 즉, OS에 종속되지 않고 어느 환경에서든 동일하게 실행된다는 의미입니다. JVM이 .class 파일을 OS가 이해할 수 있는 기계어로 변환해서 OS로 넘기기 때문에 여러 환경 속에서 동일하게 실행될 수 있습니다.

또한, OS뿐만 아니라 CPU 제조사(대표적으로 인텔과 ARM)마다 기계어의 설계가 다르기 때문에 하드웨어에도 종속되지 않는다고 할 수 있습니다.

이처럼 해당 환경에 맞는 JVM만 설치되어 있으면, 자바는 운영체제와 하드웨어에 종속되지 않고 독립적으로 동일하게 실행될 수 있습니다. 이를 플랫폼 독립성이라고 부릅니다.

ISA

ISA는 Instruction Set Architecture의 약자로, CPU가 이해할 수 있는 명령어의 집합이라고 말할 수 있습니다.
대표적으로 Intel의 x86, ARM이 존재합니다.
x86CISC방식, ARMRISC 방식으로 명령어 체계가 구성되어 있기 때문에 CPU에 대한 명령어 설계 방식이 다릅니다.


2. Garbage Collection을 통한 메모리 관리

GC라고 불리는 Garbage Collection은 JVM의 Heap 영역 메모리에서 동적으로 할당된 모든 객체를 관찰하며 프로그램에서 더 이상 참조되지 않는 객체를 찾아 삭제하는 프로세스입니다.

C언어에서는 mallocfree를 통해 개발자가 직접 메모리를 할당 및 해제하지만, 자바에서는 Garbage Collector가 자동으로 처리해 주기 때문에 개발자가 메모리 해제와 같은 메모리 관리를 하지 않아도 됩니다.

이처럼 JVM의 Garbage Collection은 개발자가 메모리 관리를 따로 해주지 않아도 된다는 장점을 제공합니다. 또한, 메모리에 대한 직접적인 접근을 제한하기 때문에 시스템의 보안성이 향상된다고 할 수 있습니다.

Garbage Collection

GC의 처리 방식은 Mark, Sweep, Compact로 나뉘고, GC의 대상이 되는 Heap 영역은 Young GenerationOld Generation으로 나뉩니다. 하지만 GC와 관련된 내용은 조금 더 복잡하고 양이 많기 때문에 다음에 따로 포스팅하도록 하겠습니다!


JVM의 구조

JVM은 위와 같이 크게 Class Loader, Execution Engine, Runtime Data Area로 구성되어 있습니다.


1. Class Loader

런타임 시, 클래스 파일(bytecode)을 읽어 JVM의 Method 영역에 동적으로 로드합니다.

클래스를 메모리에 적재하는 순서 및 과정

  1. 로딩 : 클래스 파일을 JVM의 Runtime Data Area에 저장
    • 클래스의 변수 및 메서드 정보
    • 필요한 경우에만 메모리에 로드하는 동적 로딩이기 때문에, static 변수나 사용되지 않는 클래스는 로드되지 않는다.
    • static final은 상수로 취급되기 때문에 JVM의 Constant Pool에서 관리

  2. 링크 : 읽어 들인 클래스 파일에 대한 검증 / 준비 / 분석 단계 실행
    • 검증 : 클래스가 JVM 명세를 지켰는지 검사
    • 준비 : 클래스가 필요로 하는 메모리를 할당 & 정적 필드를 기본값으로 초기화
    • 분석 : Symbolic Reference(논리적 주소)를 Method 영역에 있는 실제 고정된 주소 값으로 변경

  3. 초기화 : static으로 지정된 클래스 변수들을 초기화

2. Execution Engine

Class Loader를 통해 클래스를 메모리에 적재한 경우, 해당 클래스 파일(bytecode)을 기계어로 변환하여 실행합니다. 기계어로 변환할 때는 Interpreter 또는 JIT compiler 방식을 사용합니다.


Interpreter

  • Interpreter 방식은 bytecode를 명령어 단위로 한 줄씩 읽어서 실행합니다.
  • 소스 코드 전체를 한 번에 기계어로 변환하는 컴파일 방식과는 다르게 코드를 한 줄씩 읽어서 실행하기 때문에 실행 속도가 느리다는 단점이 존재합니다.
  • 하지만 빌드 과정이 없어 코드를 변경할 때 바로 실행할 수 있다는 장점이 존재합니다.

Java는 Interpreter 언어인가요?

초반에 말씀드렸다시피, 개발자가 .java 파일을 작성하면 Java compiler가 .class 파일로 변환시켜 줍니다. 그리고 JVM이 .class 파일인 bytecode를 Interpreter 방식으로 코드를 한 줄씩 읽어 실행시켜 줍니다.
즉, Java는 Compile 방식과 Interpreter 방식 둘 다 사용하는 언어입니다.


JIT compiler

  • Just In Time의 약자로, 동적 컴파일을 의미합니다.
  • Interpreter 방식으로 기계어 코드를 생성하다가, 해당 코드가 JIT compiler의 컴파일 대상이 되면 컴파일 후 해당 코드를 캐싱하게 됩니다.
  • JIT 컴파일은 코드가 실행되는 과정에서 실시간(Just In Time)으로 발생하며, 필요한 부분만 캐시에 저장하기 때문에 다음에 컴파일 없이 재사용이 가능합니다.
  • JIT 컴파일 조건은 코드가 컴파일 임계치만큼 실행됐는가이며, 컴파일 임계치는 메서드가 호출된 횟수 그리고 Loop를 반복하는 횟수를 기반으로 정해집니다.
  • 따라서 자주 사용되는 메서드는 JVM 시작 직후에 컴파일 되고, 자주 사용되지 않는 메서드는 나중에 컴파일 되거나 아예 컴파일 되지 않는 경우도 있습니다.
  • JIT 컴파일러 내부에는 C1 컴파일러C2 컴파일러가 존재합니다.
    - JVM은 코드들을 수행 빈도나 복잡도에 따라 4가지 레벨로 분류합니다.
    - C1 컴파일러 : 1~3 레벨의 복잡도를 가진 코드를 낮은 수준의 최적화 후 캐시에 저장
    - C2 컴파일러 : 4 레벨의 복잡도를 가진 코드를 높은 수준의 최적화 후 캐시에 저장

그냥 Compiler 방식을 사용하면 안 되나요?

Compiler 방식은 플랫폼에 종속되기 때문에 자바의 철학인 Write Once and Run Anywhere를 위배하게 됩니다. 또한, 초기 실행 속도가 Interpreter에 비해 느리다는 단점이 존재합니다.


3. Runtime Data Area

Runtime Data Area는 JVM이 운영체제로부터 할당받은 메모리 영역을 의미합니다.
그림과 같이 Method Area, Heap Area, Stack Area, PC Register, Native Method Stack 총 5개의 영역으로 구성되어 있습니다.

이중 Method AreaHeap Area는 JVM이 시작될 때 생성되며 모든 Thread들이 공유하는 영역입니다. 각각의 Thread가 시작되면 Thread별로 Stack Area, PC Register, Native Method Stack이 생성됩니다. 이 3가지 영역은 서로 공유하지 않는 영역이며 해당 Thread가 종료될 때 사라집니다. 마지막으로 Method Area와 Heap Area는 JVM이 종료될 때 사라지게 됩니다.

각 영역별로 어떤 데이터를 저장하는지 자세하게 알아보겠습니다.


1. Method Area

  • Class Loader가 클래스 파일을 메모리에 로드할 때 사용되는 메모리 영역입니다.
  • JVM이 종료될 때 메모리가 해제됩니다.
  • 데이터 종류
    - 클래스와 인터페이스의 Type 정보
    - Runtime Constant Pool
    - 필드, 메서드 정보
    - static으로 선언된 변수

2. Heap Area

  • new 키워드를 통해 생성된 객체참조형 데이터 타입(array, enum)이 저장되는 메모리 영역입니다.
  • Garbage Collector에 의해서 메모리가 관리됩니다.
  • 데이터 종류
    - 객체
    - array, enum, class, interface

3. Stack Area

  • 기본 자료형, 지역 변수, 매개 변수, 리턴 값이 저장되는 메모리 영역입니다.
  • 데이터 종류
    - int, boolean
    - Heap 영역에 생성된 데이터의 참조 값
    - 지역 변수, 매개 변수, 리턴 값

4. PC Register

  • 현재 JVM이 실행하고 있는 명령어의 주소를 저장합니다.
  • Thread가 시작될 때 생성되며, Thread마다 1개씩 생성됩니다.

5. Native Method Stack

  • 자바 이외의 언어로 작성된 코드가 실행될 때 사용되는 메모리 영역입니다.
  • JNI(Java Native Interface)를 통해 bytecode로 변환하여 저장합니다.

자바의 심장 JVM

이처럼 자바는 굉장히 복잡하게 구성되어 있는 JVM이라는 기술을 가지고 있습니다. 사실상 JVM이 자바의 핵심 기술이라고 볼 수 있습니다.

지금까지 저는 자바가 인기 있는 언어이다, JVM으로 인해 플랫폼 독립성이라는 강한 장점을 가지고 있다 정도의 특징만 알고 있었습니다.

현재 스프링을 배우고 있는 입장에서 자바를 가장 많이 사용하고 있음에도 불구하고, 자바의 동작 원리나 내부 메모리 구조에 대해 알지 못했고 알려고 노력하지도 않았던 것 같습니다.
이번 기회를 통해 JVM의 메모리 구조, 동작 원리, Garbage Collector 등 자바의 다양한 모습들을 학습할 수 있었습니다.

포스팅을 작성하면서 자바는 꽤 복잡하게 만들어진 언어임을 알 수 있었습니다. 여러분들도 기회가 되신다면 JVM에 대해서는 한 번쯤 탐구해 보시길 바랍니다!


마무리

앞으로는 내가 사용하는 기술들을 왜 사용하는지 그리고 어떤 방식으로 동작하는지 정도는 학습하며 사용해야겠다고 결심하였다.

또한, 포스팅을 작성하면서 블로그를 찾아보던 중 꽤 많은 곳에서 다른 작성자의 글을 그대로 베껴오는 경우가 많았다. 최소한 이해는 하고 본인 스타일대로 글을 쓰는 게 맞다고 보는데 요즘 취업 시장에서 블로그까지 평가하다 보니 포스팅 수를 억지로 늘리려고 하는 것 같기도 하다.


참고

profile
호기심 저장소

0개의 댓글