본문 바로 가기

JVM Runtime Data Area - 쓰레드 영역

들어가며

JVM Runtime Data의 스레드별 영역, 즉 Java Stack, PC Register, Native Method Stack에 대해 알아보려 합니다. HotSpot JVM (JDK 17 기준) 소스 코드를 통해 이들이 실제로 어떻게 관리되는지, 그리고 그 과정에서 제가 겪었던 궁금증과 새롭게 알게 된 사실들을 공유하고자 합니다.

📌 TL;DR (핵심 요약)

✅ OS는 스택 메모리 공간만 제공하며, 이 공간을 어떻게 사용할지는 컴파일러의 역할입니다.
✅ Native Method Stack: Java Stack과 물리적으로 동일한 OS 레벨의 스택을 공유합니다.

JVM 스레드별 런타임 데이터 영역: 무엇이 있을까?

JVM 명세에 따르면, 각 스레드는 다음과 같은 자신만의 런타임 데이터 영역을 가집니다.

  1. Java Stack: 메서드 호출 시마다 프레임(지역 변수, 피연산자 스택, 메서드 반환 값 등)을 저장합니다.
  2. PC Register: 현재 스레드가 실행 중인 JVM 명령어의 주소를 가리킵니다. Native 메서드를 실행 중이라면 undefined 상태입니다.
  3. Native Method Stack: Java 코드가 아닌 Native 코드(C/C++ 등)로 작성된 메서드를 실행할 때 사용되는 스택입니다.

이 세 가지 영역이 JVM 레벨에서 어떻게 통합되고 관리되는지, HotSpot JDK 17 소스 코드를 통해 자세히 살펴보겠습니다.

환경

OpenJDK 17 (HotSpot JVM)

1. Java Stack

Java Stack은 각 스레드마다 메서드 호출 정보를 저장하는 스택 프레임들을 위한 공간입니다. HotSpot JVM에서는 이 Java Stack을 어떻게 표현하고 있을까요?

Thread.hpp 에 정의된 필드를 보면 알 수 있습니다.

address          _stack_base;      // Base of allocated Java stack
size_t           _stack_size;      // Size of Java stack (in bytes)
  • _stack_base: 스레드의 Java Stack 메모리 영역의 시작 주소를 가리키는 포인터입니다. (스택은 낮은 주소로 자라므로, 여기가 "bottom" 즉, 가장 높은 주소 값입니다.)
  • _stack_size: 할당된 Java Stack의 크기를 나타냅니다. (바이트 단위)

이 변수들을 통해 JVM은 각 스레드의 Java Stack 위치와 크기를 관리합니다.

2. PC Register

PC Register는 현재 실행 중인 JVM 명령의 주소를 가리킵니다. 이 값은 스레드 컨텍스트 스위칭 시 다음 실행할 명령을 정확히 찾아가기 위해 필수적입니다.

thread.hpp 파일 내 JavaThread 클래스에서 관련 코드를 찾아볼 수 있습니다.

JavaFrameAnchor _anchor;

JavaThreadJavaFrameAnchor 타입의 멤버를 가지고 있습니다. 이 JavaFrameAnchor를 따라가 보면, PC Register와 관련된 변수를 찾을 수 있습니다.

volatile address _last_Java_pc; 
  • _last_Java_pc: 마지막으로 실행된 Java 바이트코드 명령어의 주소를 저장합니다. volatile로 선언되어 있어 멀티스레드 환경에서의 가시성을 보장합니다. 실질적인 PC Register 의 역할을 수행합니다.

3. Native Method Stack

Java Stack과 PC Register는 코드 상에서 비교적 명확하게 그 존재를 확인할 수 있었지만, Native Method Stack과 관련된 명시적인 변수는 Thread 또는 JavaThread 클래스 내에서 찾기 어려웠습니다.

이 의문을 해결하기 위해 조사하던 중, Stack overflow handling in HotSpot (by Andrei Pangin) 이라는 글을 발견했습니다.

In HotSpot, Java stack and native C stack are the same. HotSpot does not maintain a separate native method stack. When a Java method calls a native method, it simply continues on the same stack.

(HotSpot에서 Java 스택과 네이티브 C 스택은 동일합니다. HotSpot은 별도의 네이티브 메서드 스택을 유지하지 않습니다. Java 메서드가 네이티브 메서드를 호출하면, 단순히 동일한 스택에서 계속 진행됩니다.)

즉, Java Stack과 Native Method Stack은 물리적으로 같은 스택을 공유하며, 개념적으로만 구분될 뿐입니다. 따라서 JVM 코드에서 Native Method Stack을 위한 별도의 _stack_base_native 같은 변수를 찾을 수 없었던 것입니다. 이 스택은 OS가 관리하는 영역이므로, JVM은 이를 직접 관리하기보다는 OS가 제공하는 스택을 활용하는 형태입니다.

새롭게 알게 된 점과 근본적인 의문 해결

이번 조사를 통해 몇 가지 중요한 사실을 깨닫고, 기존의 오해를 바로잡을 수 있었습니다.

  1. Java Stack의 실제 위치: 저는 처음에 Java Stack이 Java 힙 메모리 내에 별도로 할당되는 공간이라고 어렴풋이 생각했습니다. 하지만 실제로는 C언어에서처럼 OS가 스레드 생성 시 할당해주는 네이티브 스택 메모리 영역을 사용한다는 것을 알게 되었습니다.
  2. OS 스택과 JVM의 스택 프레임 관리: 여기서 근본적인 의문이 생겼습니다. "OS가 관리하는 스택 메모리인데, JVM이 어떻게 마음대로 스택 프레임을 만들고, 메서드 호출 시 정보를 push하고, 메서드 종료 시 pop하는 등의 작업을 할 수 있을까?"

기존의 오해: 저는 C 프로그램을 작성하면 OS가 함수 호출 시 스택에 프레임을 올리고, 함수 종료 시 pop 하는 모든 과정을 직접 관여하는 줄 알았습니다.

새로운 이해: 하지만 실제로는 OS는 스레드 생성 시 일정 크기의 스택  메모리 공간을 할당해줄 뿐, 그 내부를 어떻게 구성하고 사용할지(스택 프레임 생성, push/pop 등)는 해당 스택을 사용하는 프로그램의 책임이라는 것을 깨달았습니다. 프로세스(JVM)가 OS로부터 할당받은 스레드 스택을 완전히 제어할 수 있는 것입니다.

정리하며: 알게 된 점 요약

이번 조사를 통해 JVM의 스레드별 런타임 데이터 영역에 대해 다음과 같은 점들을 명확히 알 수 있었습니다.

  • OS의 역할과 프로그램의 역할: 스레드가 생성될 때 OS는 해당 스레드를 위한 스택 메모리 공간을 할당합니다. 이 공간 내에서 스택 프레임을 어떻게 구성하고 관리할지는 컴파일된 코드(여기서는 JVM)에 의해 결정됩니다.
  • 물리적 스택 공유: HotSpot JVM에서 Java Stack과 Native Method Stack은 별도의 메모리 영역이 아닌, OS가 할당한 단일 물리적 스택을 공유하여 사용합니다.

Reference

https://medium.com/@njishtha.19/overview-of-jvm-f3b76a60ffb5
https://pangin.pro/posts/stack-overflow-handling
https://github.com/openjdk/jdk/blob/jdk-17%2B35/src/hotspot/share/runtime/thread.hpp
https://github.com/openjdk/jdk/blob/jdk-17%2B35/src/hotspot/share/runtime/javaFrameAnchor.hpp