JVM 메모리 아키텍쳐

블러거·2025년 12월 22일

JVM

목록 보기
1/26

모든 객체에 대해 메모리의 할당/해제를 책임져야 하는 C/C++ 과 달리 Java JVM 의 장점은 메모리 관리를 자동으로 해준다는 것이다.
이는 반대로 자동으로 해주는 JVM 의 메모리 관리를 잘 알지 못하면, 메모리에 문제가 생겼을때 대처하기 힘들다.

Runtime Data Area

JVM 가 사용하는 메모리 영역을 Runtime Data Area 라 한다.
이는 모든 스레드가 공유하는 데이터 영역과 스레드 본인만 사용하는 thread private memory 로 나눌 수 있다.

  • thread share memory
    • heap
    • method area
  • thread private memory
    • virtual machine stack
    • native method stack
    • program counter(pc) register

Thread Private Memory

이 영역에 포함되는 메모리들은 스레드가 생길때 만들어지며 스레드가 없어질때 같이 파괴된다.

Program Counter Register

소위 PC 라고 부르는 프로그램 카운터는, CPU 의 한 부분이다.

RISC 아키텍쳐를 검색해 보자. CPU 구조에 PC 가 있는걸 볼 수 있다.

Program Counter 에는 다음 실행할 명령어의 주소값이 들어있다.
CPU 는 PC 에 등록된 주소값으로 명령어를 찾아 실행한다.

하지만 JVM 멀티스레딩 모델에서는 여러 스레드가 하나의 CPU 를 번갈아 사용한다. 즉 CPU 는 한번에 하나의 스레드 명령어만 처리한다.
각각의 스레드는 자신이 실행하던 명령어 주소를 각자 가지고 있다가 자기가 CPU 에서 실행될 차례가 되었을때, 이 명령어부터 실행시켜줘 라고 하면서 CPU PC 에 명령어 주소를 줘야 한다.
또한 실행되다가 다른 스레드에게 CPU 사용을 넘겨줄 때, 본인이 실행하던 명령어 주소를 나중에 자기 차례가 되었을때 PC 에 주기 위해 잘 저장하고 있어야 한다.

따라서 스레드는 각자 Program Counter Register 라는 영역에 본인이 실행중인 명령어의 주소값을 저장한다.

자바 가상 머신 스택

하나의 스레드가 프로그램을 실행하면 코드 측면으로 보면 진입점인 메서드 하나 를 실행하게 된다.
그리고 그 메서드 내부에는 여러 메서드가 스택 형태로 중첩되는게 우리가 알고 있는 일반적인 코드 구조다.

그리고 지역변수라는건 하나의 메서드 블록 안에서만 생존한다.

자바 가상 머신 명세에는 이러한 메서드 실행과정에서 필요한 메모리의 구조를 virutal machine stack 이라는 이름의 스택 형태로 정의되어 있다.

한마디로 정의하자면, JVM 이 메서드를 표현하는 방법이라 할 수 있다.

자바 가상 머신 스택

스레드 하나당 생성된다. 하나의 스택 구조이며, 타입은 스택 프레임이다.

스택 프레임

메서드가 하나 실행될 때 마다 스택 프레임을 만들어 자바 가성 머신 스택에 push 하고 메서드가 끝나면 pop 한다.
스택 프레임은 메서드 하나를 실행하기 위해 필요한 데이터를 메모리에 저장하기 위한 구조다.
대표적으로 지역 변수 테이블, 피연산자 스택, 동적 링크, 메서드 반환값 등이 있다.

자세한 내용은 다른 포스팅에서 다룰 예정.

지역 변수 테이블

메서드에서 정의된 지역변수는 이 테이블 구조로 스택 프레임 안에 저장된다.
지역 변수 테이블은 기본적으로 32bits 의 슬롯이라는 단위로 저장되며, 하나의 슬롯에는 기본 타입 데이터, 객체 참조, 반환 주소를 저장할 수 있다.

슬롯에 저장 가능한 데이터들은 다른 포스팅에서 자세히 다룰 예정이며, 저장 가능한 데이터 타입은 다음과 같다.

  • boolean
  • byte
  • char
  • short
  • int
  • float
  • long
  • double : double 은 64bits 데이터 타입이다. 슬롯 2개를 필요로 한다.
  • 객체 reference
  • return address

지역변수 테이블의 크기 즉, 슬롯의 개수는 컴파일타임에 완벽히 정해지며 런타임에는 변경되지 않는다.

자바 가상 머신 스택의 에러

  1. StackOverflowException : 스레드가 메서드를 실행하며 만든 스택의 깊이가 허용치보다 높아지면 발생
  2. OutOfMemoryError : 스택 용량을 동적으로 확장할 수 있는 JVM 이 스택 용량을 조정하여 새로 스택을 쌓다가 메모리가 부족하면 발생
    • 우리가 주로 쓰는 hot spot 은 스택 용량을 동적으로 조정하지 못하기에 발생하지 않음.

네이티브 메서드 스택

역할은 자바 가상 머신 스택과 비슷하지만, 이 영역은 네이티브 메서드를 실행할 때 사용된다.
다만 자바 가상 머신 명세에서는 이 영역에서 메서드를 어떻게 표현해야하는지 명시하지 않아, 가상머신 구현자가 마음대로 구현할 수 있다. 그래서 네이티브 메서드 스택을 자바 가상머신 스택과 합쳐놓은 가상머신도 존재한다.
마찬가지로 StackOverflowException, OutOfMemoryException 이 발생할 수 있다.

공유 Memory

자바 가상 명세서에 언급되는 공유 메모리 영역은, 힙과 메서드 영역으로 나눌 수 있다.

Heap

객체 인스턴스를 저장하기 위한 공간.
모든 객체 인스턴스와 배열은 힙에 할당된다.
요즘들어 값타입, 탈출 분석 기술 발전으로 스택 할당과 스칼라 치환 최적화 방식이 달라지며 이 정의가 애매해짐.

메모리 회수 관점에서 세대별 컬렉션 이론(generational collection theory) 가 주류
하지만 자바 가상 머신 명세는 힙의 세부 사항에 대한 언급이 없으므로 이 세대별 구분이 필수는 아님.

메모리 할당 관점에서 모든 객체 인스턴스는 스레드들이 공유 가능한 힙 영역에 저장된다. 그리고 메모리 회수와 할당을 빠르게 하기 위해 힙을 세부 영역으로 나눌 수 있다.
스레드 로컬 할당 버퍼가 그것이다.

자바 힙의 메모리는 물리적으로는 연속될 필요는 없으나 논리적으로 연속되어야 한다. 디스크에 파일을 저장하는 것과 같다.

힙 크기는 -Xmx, -Xms 로 조절할 수 있으며 메모리가 부족하면 OutOfMemoryException 을 던진다.

메서드 영역

가상 머신이 읽은 타입 정보, 상수, 정적 변수, JIT 컴파일러가 컴파일한 코드 캐시등을 저장하는 메모리 영역이다.

메서드 영역은 힙과 같이 명세에 기술된 추상적인 개념이다. 자바 가상 머신 명세에는 메서드 영역을 어떻게 구현하라는 내용을 명시하지 않았다. 이에 따라 실제로는 JVM 구현에 따라 정적 변수가 힙영역에 생성되든 다른곳이든 구현의 차이에 따라 다를 수 있다. 기존에 재대로 알기 전에는 정적 변수는 메서드 영역이니, 힙이 아닌 다른 곳에 저장되겠지..? 라고 막연히 생각했었다.

이 메서드 영역의 데이터가 저장되는 메모리상의 실제 위치는 HotSpot 기준으로 JVM 버전에 따라 달라졌는데, 이에 대해 설명해 보겠다.

영구세대 (permanent generation)

자바 가상 머신 명세 를 보면, 메서드 영역은 논리적으로 힙의 일부이다. 하지만 메서드 영역의 GC 는 선택사항이라고 명시해 놓았음을 볼 수 있다.

JDK6 까지는, 메서드영역의 구현이 Permuanent Generation(영구 세대) 라는 곳에 저장되었다. 가상 머신이 읽은 타입 정보, 상수, 정적 변수, JIT 컴파일러가 컴파일한 코드 캐시, 런타임 상수 풀, String 풀 모두가 힙 영역 안에 있었다.

이는 힙의 Garbage Collection 영역에 메서드 영역을 포함시켜서, 따로 메서드 영역의 메모리 관리를 안해도 되어서 편해 보였다.

하지만, 힙의 영구 세대로서 메모리 한도가 정해졌기에 OOM 이 발생할 건덕지가 하나 늘어났으며, 오라클이 JRockit 을 소유한 뒤 JMC 같은 기능들을 HotSpot 에 이식하기 시작하고 부터는 영구 영역이라는게 없던 JRockit 과의 메서드 영역 구조 차이가 불편했다.

결국 JDK 7 에 HotSpot 은 영구 영역의 문자열 상수(String::intern()), 정적 변수 등을 힙으로 옮겨버렸다.
JDK 8 에는 HotSpot 은 영구 영역을 아예 삭제했으며, 영구 영역에 남아있던 타입 정보 등의 데이터는 네이티브 메모리인 Metaspace 영역을 새로 구현하여 옮겼다.

런타임 상수 풀

자바 코드를 컴파일러로 컴파일 하여 클래스 파일로 만들어보자.
해당 파일을 javap 로 해석해보면, Constant Pool 이라는 부분을 볼 수 있다.

이를 직역하여 상수 풀이라고 부르며, 클래스 명이나 상수값 등 코드의 리터럴 값들이 저장된다.
이 부분을 자세히 알기 위해서는 컴파일 과정을 자세히 알아야 한다.

여튼 이 상수 풀에는 여러가지 리터럴 값들(사용되는 변수의 FQCN, 상수값, 문자열 리터럴 등..) 이 jvm 이 정해놓은 규칙을 따라 심볼 참조 방식으로 저장되는데, 런타임이 아닌 컴파일 시점 이라는걸 염두해 두어야 한다.

클래스로더가 클래스 파일을 로드하게 되면, 클래스 파일의 상수 풀의 값들을 결국 메모리에 올려야 할 것이다. 그때 컴파일 타임의 심볼 참조를 직접 참조로 바꾸면서 런타임 상수 풀이라는 부분에 넣게 된다.

이 직접 참조에는 클래스 파일에서 사용하는 외부 객체에 대한 참조 도 포함된다.
즉, 런타임 상수 풀에는 힙에 존재하는 외부 객체에 대한 참조가 존재한다.

클래스파일의 상수 풀 정보가 런타임시 런타임 상수 풀로 가게 되는 것이며, 이 때 심볼 참조들이 메모리 직접 참조 값으로 바뀌어서 메모리에 올라가게 된다.

메타스페이스

메타스페이스는 다이렉트 메모리로 저장되는 부분이다. HotSpot 에서는 런타임 상수 풀등이 저장된다.

메서드 영역과 런타임 상수 풀이 자바 가상 명세에 명시된 부분임에 반하여,
메타스페이스는 HotSpot 이 구현한 상세 내용에 해당한다. HotSpot 은 가상 명세에 명시된 런타임 상수 풀과 메타 정보들을 구현하기 위해 이 영역을 만들었다.

String Constant Pool 이라는거도 공부하다 보면 나온다. String.intern() 메서드와 관계가 있다. String 값의 1비교를 O(1) 로 하기 위해서 인터닝이라는 기법을 사용한다. 문자열 객체는 리터럴이 같다면 메모리상의 같은 객체를 사용하게 만들어 문자열의 비교를 효율적으로 수행할 수 있다.

자바에서 String 을 만들면 인터닝이 자동으로 적용되며, HotSpot 에서는 이 구현을 힙 영역에 String Constant Pool 영역을 두고 문자열 객체를 저장한 후 이후 String 객체 생성시 intern() 메서드를 실행하여 객체의 재사용을 구현한다.

다이렉트 메모리 영역

자바 가상 머신 명세에 정의된 영역은 아니지만 실제로 쓰이며 사용중 OOM 이 발생할 수 있는 영역이다.

NIO 가 대표적인 예인데, NIO 는 비동기로 주고받은 데이터를 더 효율적으로 관리하는 방법으로 다이렉트 메모리 영역을 사용한다.
바이트를 코드로 나타내기 위해 일반적으로는 힙 영역에 ByteBuffer 같은 객체를 만들어야겠지만 이런 힙 객체는 결국 물리 메모리를 직접 사용하기 위해 가상머신 메모리를 물리 메모리에 복사해 넣어야 한다.
NIO 의 DirectByteBuffer 는 네이티브 메모리 영역을 바로 써서 성능을 개선했다.

profile
안녕하세요!

1개의 댓글

comment-user-thumbnail
2026년 2월 12일

좋은 글 감사합니다.

답글 달기