JVM 메모리 구조란?

한민욱·2024년 6월 1일

JVM 메모리 구조를 설명하기 전에 JVM이 무엇인지 알아야 합니다.

JVM은 Java Virtual Machine의 약자로, 자바 가상 머신이라고 부릅니다. 그리고 자바와 운영체제 사이에서 중개자 역할을 수행하며, 자바가 운영체제에 구애 받지 않고 프로그램을 실행할 수 있도록 도와줍니다. 또한, 가비지 컬렉터를 사용한 메모리 관리도 자동으로 수행하며, 다른 하드웨어와 다르게 레지스터 기반이 아닌 스택 기반으로 동작합니다.

레지스터 기반과 스택 기반도 알아보고 갈까요?

레지스터 기반 VM

레지스터 기반 아키텍처는 프로세서가 데이터를 저장하고 처리하기 위해 레지스터(고속 메모리)를 사용하는 방식입니다. 일반적인 물리적 CPU와 동일한 방식으로 동작합니다.

레지스터 기반 아키텍처의 특징

  • 레지스터: CPU 내부에 있는 소형 고속 메모리 저장소입니다. 연산에 필요한 데이터를 레지스터에 저장하고, 연산 결과도 레지스터에 저장합니다.
  • 명령어: 명령어는 레지스터를 직접 참조하여 연산을 수행합니다.

레지스터 기반 아키텍처의 장점과 단점

1. 장점

빠른 속도: 레지스터 접근 속도가 매우 빠릅니다.
효율성: 명령어가 더 적은 공간을 차지할 수 있어 명령어 처리 속도가 빠릅니다.

2. 단점

복잡성: 레지스터의 수가 제한적이기 때문에, 레지스터 할당 및 관리가 복잡할 수 있습니다.
이식성: 특정 하드웨어 아키텍처에 종속적일 수 있습니다.

간단한 예제~

MOV R1, 5      ; 레지스터 R1에 5를 저장
MOV R2, 3      ; 레지스터 R2에 3을 저장
ADD R1, R2     ; R1에 R1 + R2 결과를 저장

스택 기반 아키텍처

스택 기반 아키텍처는 연산을 수행하기 위해 데이터를 스택에 저장하고, 스택을 이용해 연산을 처리하는 방식입니다. 자바 가상 머신(JVM)은 이 방식을 사용합니다.

스택 기반 아키텍처 특징

  • 스택: 데이터가 후입선출(LIFO) 방식으로 저장되는 메모리 구조입니다. 연산에 필요한 데이터는 스택에서 푸시(push)하고 팝(pop)합니다.
  • 명령어: 명령어는 주로 스택을 참조하여 연산을 수행합니다. 스택에서 데이터를 꺼내서 연산하고, 결과를 다시 스택에 넣습니다.

스택 기반 아키텍처의 장점과 단점

1. 장점

  • 단순성: 레지스터 할당과 같은 복잡한 관리를 피할 수 있어 설계와 구현이 단순합니다.
  • 이식성: 하드웨어에 독립적이므로 다양한 플랫폼에서 쉽게 이식 가능합니다.

2. 단점

  • 성능: 레지스터 기반에 비해 상대적으로 연산 속도가 느릴 수 있습니다. 메모리 접근이 더 빈번하게 일어나기 때문입니다.
  • 명령어 효율성: 동일한 작업을 수행하는 데 더 많은 명령어가 필요할 수 있습니다.

즉 !! 자바 속도가 느린 이유 중 하나가 되시겠다!

예를 들어볼까요?

int a = 5;
int b = 3;
int c = a + b;

요것이

iconst_5          ; 상수 5를 스택에 푸시
istore_1          ; 스택의 값을 로컬 변수 1에 저장
iconst_3          ; 상수 3을 스택에 푸시
istore_2          ; 스택의 값을 로컬 변수 2에 저장
iload_1           ; 로컬 변수 1의 값을 스택에 푸시
iload_2           ; 로컬 변수 2의 값을 스택에 푸시
iadd              ; 스택에서 두 값을 꺼내 더하고 결과를 스택에 푸시
istore_3          ; 스택의 값을 로컬 변수 3에 저장

JVM에서 바이트코드로 컴파일된당.
뭐 이를 통해서 자바 프로그램의 플랫폼 독립성을 유지할 수 있는 것이라 보시면 되겠다.
이거보니 컴파일과 런타임도 정리 함 해봐야겠다는 생각이

우선 자바 프로그램의 실행 단계입니다.

먼저, 자바 컴파일러에 의해 자바 소스 파일은 바이트 코드로 변환됩니다. 그리고 이러한 바이트 코드를 JVM에서 읽어 들인 다음에, 이것저것 복잡한 과정을 거쳐서 어떤 운영체제든간에 프로그램을 실행할 수 있도록 만드는 것입니다.

만약, 자바 소스 파일은 리눅스에서 만들었고 윈도우에서 이 파일을 실행하고 싶다면, 윈도우용 JVM을 설치만 하면 됩니다. 여기서 JVM은 운영체제에 종속적이라는 특징을 알 수 있습니다.

JVM 메모리 구조

위에서 설명하였듯이, 자바 소스 파일은 자바 컴파일러에 의해서 바이트 코드 형태인 클래스 파일이 됩니다. 그리고 이 클래스 파일은 클래스 로더가 읽어들이면서 JVM이 수행됩니다.

(1) Class Loader

JVM 내로 클래스 파일을 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈입니다. 런타임 시에 동적으로 클래스를 로드합니다.

(2) Execution Engine

클래스 로더를 통해 JVM 내의 Runtime Data Area에 배치된 바이트 코드들을 명렁어 단위로 읽어서 실행합니다. 최초 JVM이 나왔을 당시에는 인터프리터 방식이었기때문에 속도가 느리다는 단점이 있었지만 JIT 컴파일러 방식을 통해 이 점을 보완하였습니다. JIT는 바이트 코드를 어셈블러 같은 네이티브 코드로 바꿈으로써 실행이 빠르지만 역시 변환하는데 비용이 발생하였습니다. 이 같은 이유로 JVM은 모든 코드를 JIT 컴파일러 방식으로 실행하지 않고, 인터프리터 방식을 사용하다가 일정한 기준이 넘어가면 JIT 컴파일러 방식으로 실행합니다.


(3) Garbage Collector

Garbage Collector(GC)는 힙 메모리 영역에 생성된 객체들 중에서 참조되지 않은 객체들을 탐색 후 제거하는 역할을 합니다. 이때, GC가 역할을 하는 시간은 언제인지 정확히 알 수 없습니다.

(4) Runtime Data Area

JVM의 메모리 영역으로 자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역입니다. 이 영역은 크게 Method Area, Heap Area, Stack Area, PC Register, Native Method Stack로 나눌 수 있습니다.

(1) Method area

모든 쓰레드가 공유하는 메모리 영역입니다. 메소드 영역은 클래스, 인터페이스, 메소드, 필드, Static 변수 등의 바이트 코드를 보관합니다.

2. Heap area

모든 쓰레드가 공유하며, new 키워드로 생성된 객체와 배열이 생성되는 영역입니다. 또한, 메소드 영역에 로드된 클래스만 생성이 가능하고 Garbage Collector가 참조되지 않는 메모리를 확인하고 제거하는 영역(중요)입니다.
GC 설명할 때 Static도 같이 할 거예요.

3. Stack area

메서드 호출 시마다 각각의 스택 프레임(그 메서드만을 위한 공간)이 생성합니다. 그리고 메서드 안에서 사용되는 값들을 저장하고, 호출된 메서드의 매개변수, 지역변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장합니다. 마지막으로, 메서드 수행이 끝나면 프레임별로 삭제합니다.

4. PC Register

쓰레드가 시작될 때 생성되며, 생성될 때마다 생성되는 공간으로 쓰레드마다 하나씩 존재합니다. 쓰레드가 어떤 부분을 무슨 명령으로 실행해야할 지에 대한 기록을 하는 부분으로 현재 수행중인 JVM 명령의 주소를 갖습니다.

이 JVM 명령의 주소가 대체 무슨 뜻이냐? 예시를 통해 알아볼까요?😊

0: iconst_10       // 정수 10을 스택에 푸시
1: istore_1        // 스택의 값을 로컬 변수 1에 저장 (a = 10)
2: iconst_20       // 정수 20을 스택에 푸시
3: istore_2        // 스택의 값을 로컬 변수 2에 저장 (b = 20)
4: iload_1         // 로컬 변수 1의 값을 스택에 푸시
5: iload_2         // 로컬 변수 2의 값을 스택에 푸시
6: iadd            // 스택의 두 값을 더하여 결과를 스택에 푸시 (c = a + b)
7: istore_3        // 스택의 값을 로컬 변수 3에 저장 (c = 결과)
8: getstatic       // System.out 객체를 스택에 푸시
11: iload_3        // 로컬 변수 3의 값을 스택에 푸시
12: invokevirtual  // println 메서드를 호출
15: return         // 메서드 종료

여기서 PC Register의 역할은 초기 PC = 0 (iconst_10)을 실행 후
다음 명령어 PC = 1 (istore_1)로 업데이트가 됩니다.

즉 맨 처음 스택에 10을 푸시하고 PC 레지스터가 1로 업데이트합니다.
이를 통해 JVM은 자바 바이트코드를 순차적으로 실행할 수 있습니다.

쓰레드도 다음에...

5. Native method stack

자바 외 언어로 작성된 네이티브 코드를 위한 메모리 영역입니다.

네이티브 코드도 알아볼까요..?

네이티브 코드에 대해

자바는 일반적으로 네이티브 코드를 생성하지 않고, 플랫폼 독립적인 바이트코드로 컴파일됩니다. 하지만 자바의 실행 과정에서 네이티브 코드가 사용되는 경우가 있습니다.
왜 쓰이냐하면 JVM이 성능 최적화를 위해서 JIT(Just-In-time) 컴파일러를 사용하는데 이 친구는 자주 실행되는
바이트코드를 네이티브 코드로 변환합니다. 이렇게 변환된 네이티브 코드는 캐시되어서 이후 동일한 코드가 실행될 때 빠르게 실행됩니다. 이를 통해 매번 컴파일하는 오버헤드를 줄일 수 있죠!

물론 직접 호출 할 수도 있어요
JNI(Java Native Interface)로다가.

JNI 예시를 들어볼게요!

public class Example {
    // 네이티브 메서드 선언
    public native void sayHello();

    // 네이티브 라이브러리 로드
    static {
        System.loadLibrary("Example");
    }

    public static void main(String[] args) {
        new Example().sayHello();
    }
}

즉 네이티브 코드는 자바의 성능을 최적화하거나 특정 플랫폼 기능을 사용할 때 매우 매우 유용하답니다.

쓰다보니 좀 길어졌네요... 다음 포스팅은 쓰레드로 갈게요.

💕참고자료
https://steady-coding.tistory.com/305

profile
나날이 성장하고 싶은 백엔드 개발자

0개의 댓글