JVM과 JVM 메모리 구조

JVM이란?

Java Virtual Machine의 약자로 직역하면 자바 가상 머신이다.

자바 가상 머신이라는 의미를 알기 위해선 프로그래밍 언어를 컴퓨터가 어떻게 읽고 이해하는지 알면 이해하기 쉽다.

컴퓨터는 기계어(0, 1)만 직접 이해할 수 있기 때문에 사람이 쓰는 언어인 Java, C, Python 등을 직접 이해하지 못한다.

따라서 사람이 작성한 소스 코드는 번역 과정을 거쳐야 한다.

먼저 C나 C++ 같은 언어는 컴파일러가 소스 코드를 한 번에 기계어로 바꿔서 실행 파일을 만든다.

파이썬, 자바스크립트 같은 언어는 인터프리터가 코드를 한 줄씩 읽으면서 실행한다.

하지만 자바는 위 두 개의 방식을 절충해 하이브리드 언어라고도 불리는데 이유는 다음과 같다.

먼저 소스를 바이트코드 라는 중간 형태로 컴파일하고, 그 뒤에 JVM이 바이트코드를 운영체제에 맞게 해석해준다.

 

JVM이란 정확히 무엇인가?

JVM은 자바 프로그램을 실행하기 위한 가상의 컴퓨터이다.

실제 물리적인 컴퓨터가 아니라, 소프트웨어로 만들어진 가상의 실행 환경을 의미한다.

즉, 자바 프로그램이 돌아갈 수 있는 가상의 공간을 제공하는 것이 JVM이다.

마치 도커 컨테이너가 애플리케이션을 위한 독립적인 실행 환경을 제공하는 것처럼, JVM도 자바 프로그램만을 위한 표준화된 실행 환경을 만들어주는 것이라고 생각하면 된다.

 

즉, JVM은 자바 프로그램이 어떤 환경에서도 실행될 수 있도록 운영체제와 자바 바이트코드 사이에서 중개자 역할을 하는 실행 환경이다.

 

JVM의 구성 요소와 실행 과정

JVM의 구성 요소

Class Loader

컴파일된 .class 파일(바이트코드)을 JVM 메모리에 불러오는 역할을 한다.

마치 택배 기사가 주문한 물건(.class 파일)을 집(JVM 메모리)으로 배달해주는 것과 같다.

프로그램 실행 시 필요한 클래스들을 동적으로 로딩하며, 한 번에 모든 클래스를 로드하지 않고 필요할 때마다 로딩하는 지연 로딩 방식을 사용한다.

 

Runtime Data Area(메모리 영역)

JVM이 프로그램을 실행하면서 사용하는 메모리 공간이며 3개로 분리된다.

 

힙 : 객체와 배열이 저장되는 공간으로, 모든 스레드가 공유한다

String name = new String("백수왕");  // "백수왕" 객체가 힙에 저장
int[] numbers = {1, 2, 3, 4, 5};    // 배열이 힙에 저장

스택 : 메서드 호출 시 생성되는 지역 변수와 매개변수가 저장되는 공간

public void calculate(int a, int b) {  // 매개변수 a, b가 스택에 저장
    int result = a + b;                // 지역변수 result가 스택에 저장
}  // 메서드 종료 시 스택에서 자동 제거

메서드 영역: 클래스 정보, 상수, static 변수 등이 저장되는 공간 각 영역은 서로 다른 목적과 생명주기를 가지고 있어 효율적인 메모리 관리가 가능하다.

public class Student {
    static int studentCount = 0;       // static 변수가 메서드 영역에 저장
    final String SCHOOL = "한국대학교"; // 상수가 메서드 영역에 저장
}  // 클래스 정보도 메서드 영역에 저장

 

** 참고 **

메모리 영역에 대한 부분은 자바의 정석 책에서 제공하는 플래시 프로그램이 있다.

다음 사진과 같이 플래시 프로그램으로 단계별로 어느 영역에 올라가는지 자세히 알 수 있어 학습에 큰 도움이 됐다.

자바의 정석 제공 플래시 프로그램

Excution(실행 엔진)

로딩된 바이트코드를 실제 기계어로 번역하여 실행하는 핵심 부분이다.

인터프리터 방식과 JIT 컴파일러를 함께 사용해 성능을 최적화한다.

처음엔 인터프리터로 실행하다가, 자주 사용되는 코드는 JIT 컴파일러가 미리 기계어로 번역해두어 실행 속도를 향상시킨다.

더보기

프로그램을 실행하는 시점에 필요한 부분을 즉시 컴파일하는 컴파일러다.

인터프리터처럼 코드를 한 줄씩 실행하다가, 자주 사용되는 코드를 발견하면 JIT 컴파일러가 해당 부분을 통째로 기계어로 변환하여 저장한다.

이렇게 미리 번역해둔 코드는 다음번에 같은 코드가 실행될 때 훨씬 빠르게 작동한다.

 

가비지 컬렉터

더 이상 사용되지 않는 메모리를 자동으로 찾아 삭제해 주는 자동 메모리 관리 시스템이다.

개발자가 직접 메모리를 삭제할 필요 없이 JVM이 알아서 처리해주어 메모리 누수를 방지하고 개발 편의성을 높여준다.

 

JVM 실행 과정

지금까지 알아본 JVM의 구성 요소들이 어떤 방식으로 협력하여 자바를 실행하는지 알아보자.

 

1단계: 소스 코드 컴파일

개발자가 작성한 .java 파일을 javac 컴파일러가 .class 파일(바이트코드)로 변환한다.

여담으로 실제로 연차가 오래된 개발자분이 .class 파일을 "클래쯔 파일"이라고 부르시는 걸 들은 적이 있다.

// Hello.java
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

 

2단계: 클래스 로딩

JVM이 시작되면 클래스 로더가 필요한 .class 파일들을 메모리에 로딩한다.

  1. 로딩(Loading): .class 파일을 찾아 바이너리 데이터를 읽어 JVM 메모리에 올린다.
  2. 링킹(Linking): 로딩된 .class 파일이 유효한지 검증하고, 필요한 메모리 공간을 할당한다.
  3. 초기화(Initialization): 클래스의 static 변수를 초기화하고 static 블록을 실행한다.
    초기화가 끝나면 클래스가 완전히 생성되어 실행 가능한 상태가 된다.

 

3단계: 메모리 할당

로딩된 클래스 정보가 메모리 영역에 적절히 배치된다.

  1. 메서드 영역: 클래스 정보(메서드, 변수 등)가 저장되는 곳으로, Hello 클래스와 main 메서드 정보가 저장된다.
  2. 스택: 메서드가 호출될 때마다 사용되는 공간으로, main 메서드의 매개변수와 지역 변수들이 저장된다.
  3. 힙: new 키워드로 생성된 객체들이 저장되는 곳으로, 예시 코드의 "Hello World!"와 같은 문자열 객체가 여기에 저장된다.

 

4단계: 실행

실행 엔진이 바이트코드를 읽어서 실제로 실행하는 단계다.

인터프리터가 바이트코드를 한 줄씩 읽고 실행하다가, 자주 사용되는 코드는 JIT 컴파일러가 미리 읽고 성능을 최적화한다.

 

5단계: 메모리 정리

프로그램 실행이 끝난 후, 가비지 컬렉터 더 이상 사용하지 않는 객체들을 메모리에서 자동으로 제거하여 효율적으로 관리한다.

 

마무리

JVM은 자바의 정석에서 제공하는 플래시 프로그램을 통해 확실하게 학습했다고 생각했다.

하지만 시간이 지나고 막상 질문을 받자 JVM이 뭔지조차 기억하지 못해 다시 찾아보게 됐고, 이번 기회에 JVM의 구성 요소와 실행 과정을 더 깊이 이해하고자 학습 후 복습차 정리한 글이다.

알고있던 내용보다 더 많은 기능을 하고있었고 이번 정리를 통해 앞으로 또 질문이 들어온다면 바로 대답할 수 있을 것 같다는 생각이 든다.