C와 C++은 객체 (C에는 객체가 없지만)의 생성, 관리, 삭제까지 모두 관리할 책임을 가지지만, 자바에서는 JVM이 대체적으로 이 역할을 수행한다. JVM에 대해서 배우면서 가장 먼저 자바 메모리 영역에 대해서 배워보고자한다.
2장은 약간 개관에 대해서 다루고 있고, 다른 장에서 각 분야마다 좀 더 세세하게 다루는 구성으로 이루어져있는 듯하다.

자바의 실행과정은 위와 같다. 물론 여기서 다 알기엔 좀 문제가 있고, 중요한 것은
1. 소스코드는 컴파일 후에 바이트코드로 전환되어 JVM에게 전해진다.
2. 클래스들은 클래스 로더에 의해 JVM에게 전해진다.
정도만 우선적으로 알아두고 시작하면 좋을듯하다.
※ 내용이 실제와 다를 수 있습니다.
런타임 데이터 영역
JVM은 프로그램을 실시하는 동안 필요한 메모리를 몇 개의 공간으로 나눠서 관리한다. 각자의 영역은 생성 시점과 목적이 각기 다르며, 이를 총칭하는 영역이 '런타임 데이터 영역'이다.

대충 이런 구조를 가지고있다. 노란색은 모든 스레드가 공유하고, 하얀색은 스레드마다 공유하는 영역이다. 이제부터 하나씩 알아보도록한다.
프로그램 카운터 레지스터
위에서의 그림 중 차지하는 비중이 큰 것과 달리 작은 메모리영역이다. 운영체제에서의 프로그램 카운터와 비슷한 개념으로 사용된다.(동작방식은 다소 다르다.) 현재 실행 중인 메모리의 주소를 저장하고, 계속해서 이 값을 바꿔가는 것 통해 다음 명령어의 위치를 선택한다. GPT에게 물어본 결과 현재의 명령어 길이만큼 주소값에 더하면 다음 명령어가 나온다고한다. JVM이 제어, 분기, 순환, 예외처리, 스레드 복원 등의 기능을 이것을 통해 수행한다. JVM이 알아서 명령어를 수행하는 것은 프로그램 카운터 레지스터 덕분이다.
나중에 배우겠지만 자바의 명령어들은 클래스로더에 의해서 바이트코드 변경되어 올라가는데, JVM은 바이트코드 인터프리터를 통해 이 바이트코드를 읽는다. 그럼 바이트코드 인터프리터는 명령어를 해석하는 한편 PC 레지스터에 접근하여 다음에 실행할 명령어를 선택하는 식으로 동작한다.
자바에서 멀티스레드 환경을 구성하면 CPU 코어를 여러 스레드가 공유하는 것처럼 구현되기 때문에, 각기 스레드마다 다음 명령어의 위치가 다르다. 그렇기 때문에 각 스레드의 프로그램 카운터 레지스터는 서로 영향을 주지 않는 독립적인 영역에 저장된다. 그리고 이 메모리 영역을 스레드 프라이빗 메모리라고 칭한다. 그렇기 때문에 각 스레드의 프라이빗 메모리는 스레드와 그 운명을 같이한다.
스레드가 자바 메서드를 실행할 때 프로그램 카운터 레지스터는 현재 실행 중인 바이트코드의 주소가 저장되며, 스레드가 네이티브 메서드 (자바 이외의 언어로 작성된 코드)를 실행 중이라면 PC카운터의 값은 Undefined로 지정된다.
자바 가상 머신 스택
이 부분도 '스레드 프라이빗'한 영역이다. 그래서 자바 가상 머신 스택은 같이 딸린 스레드가 생성될 때 생성되고, 사라질 때 사라진다.
가상 머신 스택은 메서드를 실행하는 스레드의 메모리 모델을 기술한다.
흔히 메서드를 처음 배울 때 나오는 '스택 프레임'이 자바 가상 머신 스택에 의해 생성된다. 즉, 메서드의 스택 프레임을 생성하고, 그 메서드의 지역변수, 피연산자, 반환값 등의 정보를 저장하고, 메서드가 다른 메서드를 호출하면 그 메서드를 스택프레임 위에 올리고, 실행이 끝나면 프레임에서 제거하고 하는 일을 자바 가상 머신 스택에서 관리한다.
흔히 자바의 메모리 구조를 단순화하여 설명할 때 나오는 '스택 메모리'와 '힙 메모리' 중, 스택 메모리, 또는 단순한 '스택'이라는 표현이 지칭하는 곳이 바로 이곳, 그 중에서도 특히 지역 변수 테이블을 가리킬 때가 많다. 스택에 대해서는 뒷장에서 자세하게 다룬다고한다.
지역 변수 테이블에는, JVM이 컴파일 타임에 알 수 있는 데이터타입(8개 원시타입), 객체 참조(참조 변수가 아니다. 힙메모리에 참조 중인 객체가 저장된 주소값이다.), 반환 주소 타입(메서드가 끝나면 호출한 곳으로 돌아가야하는데, 그곳의 명령어 주소)을 저장한다. 즉, 지역 변수에 대한 메타데이터를 저장하는 곳이다. 이 정보들은 모두 컴파일 과정에서 할당한다.
지역 변수 테이블에서도 이 데이터들, 지역 변수를 저장하는 곳을 지역 변수 슬롯이라고 한다. 일반적인 슬롯 하나에는 32비트, 그러니까 4바이트가 부여되며, double처럼 64비트가 필요한 변수는 2개의 슬롯을 차지한다.
그렇기 때문에 자바에서 지역 변수용으로 저장할 지역 변수 슬롯의 크기(개수)는 컴파일 과정에서 이미 결정되었고, 메서드의 실행과정에서 크기가 변하지 않는다.
이 스택 메모리 영역에서 발생할 수 있는 오류는 2가지이다.
- StackOverflowError - 스택의 깊이가 가상 머신의 허용범위보다 깊을 경우.
- OutOfMemoryError - 스택 용량을 동적으로 확장할 수 있는데, 만약 확장할 여유 메모리가 부족한 경우
네이티브 메서드 스택
네이티브 메서드는 자바가 아닌 C, C++을 통해 작성된 메서드이다.
이 점을 제외하면 자바 가상 머신 스택과 크게 다른 것이 없기 때문에 두 개의 스택을 하나로 합친 가상 머신 모델도 있다.
역시나 스택오버플로우나 OutOfMemory 에러가 발생할 수 있다.
자바 힙
애플리케이션이 사용할 수 있는 가장 큰 메모리이다. 모든 스레드가 공유하며, 그래서 JVM이 구동될 때 만들어진다.
자바에서 생성되는 거의 모든 객체 인스턴스와 배열을 저장한다. 그리고 GC의 대상이다.
모든 스레드가 하나의 힙을 공유하기 때문에 객체할당의 효율성을 좀 높이고자 스레드 로컬 할당 버퍼 여러 개로 나뉜다. 이 부분에 대해서는 세대별 컬렉션 이론 등도 좀 알고 있어야하기 때문에 우선은 패스. (뒷장에서 등장할 예정) TLAB이라고 한다.
힙은 물리적으로 떨어진 메모리에 위치해도 상관없으나, 논리적으로는 연속되어야한다. 이 "논리적 연속"이란, JVM에서 특정 메모리크기를 요청하면 (예를 들면 1GB라던가..) OS가 이를 확인하고 메모리를 1GB를 가져와 JVM에게 할당한다. JVM은 이렇게 받은 메모리를 내부적으로 메모리주소를 만들어 할당한다.
"물리적연속"이란 이 OS가 가져다주는 주소가 연속적인 것, "논리적 연속"이란 OS가 가져와서 JVM에게 할당할 때, JVM이 내부적으로 사용하는 메모리 주소값이 연속이어야한다는 것이다.
힙의 크기는 고정도 가능하고 확장도 가능하다. 주류는 확장이 대세. 힙을 더 이상 확장할 수 없다면 OutOfMemory 에러가 발생한다.
메서드 영역
메서드 영역도 모든 스레드가 공유한다.
가상 머신이 읽어들인 타입 정보, 상수, 정적 멤버, JIT 컴파일러가 컴파일한 코드 캐시를 저장한다. JIT는 네이티브 메서드 컴파일 용도로 사용된다.
특히 그 중에서도 클래스의 자세한 정보를 저장하게 되는데, 클래스의 이름, 필드정보, 메서드구조, 상속관계 등을 전부 보관한다.
JVM에게 클래스 정보를 가져다주는 클래스 로더가 있는데, 이 클래스 로더가 클래스를 하나하나 읽어가며 메서드 영역에 klass (class가 아니다) 구조체에 저장한다.
2025-12-26 17:54 수정
이 klass 구조체에는 클래스명, 필드정보, 메서드구조, 상속관계들을 저장하는데 이 필드정보에 해당 변수가 참조형인지 원시형인지 저장한다. 아직 klass 구조체는 클래스 로딩 시점에서 작성되기 때문에 아직 실제 객체가 생성되지 않아 실제 참조값은 저장되지 않는다. 단지 참조형인지 원시형인지 구분해주는 용도
https://mangkyu.tistory.com/448
[JVM] JVM 내부의 힙 객체 헤더(Heap Object Headers on JVM Internals)
1. JVM 내부의 힙 객체의 헤더 (Heap Object Header on JVM Internals) [ 객체의 메모리 레이아웃과 객체 헤더 ]객체의 메모리 레이아웃다음의 HotSpot JVM 객체 메모리 구조에서 보이듯이, 모든 자바 객체는 인
mangkyu.tistory.com
이 글을 참고하면 도움이 될 듯하다. 다소 어렵다.
나중에 클래스 인스턴스가 생성되면 JVM에서 클래스의 이름을 확인하고 로딩여부를 확인한다. (클래스를 로딩하는 클래스 로더의 내부 캐시값으로 로딩여부를 확인할 수 있다.) 만약에 이미 로딩된 클래스라면 캐시에 저장된 klass 구조체를 통해 클래스 정보를 가져온다. 클래스의 정보가 없는 경우는 심벌참조를 사용하는데, 이는 런타임 상수 풀에서 다루겠다.
힙과 마찬가지로 연속될 필요가 없으며 크기에도 제한이 없다. 고정해도되고 확장해도 된다. GC도 구현하지 않아도된다. 어차피 메서드 영역에서는 타입 정보와 상수 풀이 대부분이라 GC 효과가 제한적이기도 하고
이곳에서도 OutOfMemoryError가 발생한다.
런타임 상수 풀
메서드 영역의 일부다.
메서드 영역에는 클래스의 자세한 정보가 저장된다고했는데, 클래스로더라는 것이 코드를 읽고 메서드영역에 클래스를 전달할 때 같이 전달된다.
위에서 클래스를 로드하는 과정이 간략하게 나왔는데, 만약 참조 관계에 있는 두 클래스가 있다고 치면,
- A 클래스가 B 클래스를 참조 및 생성할 때, 클래스 로더는 A 클래스를 메서드영역으로 보내며 B 클래스가 메서드영역에 저장되어있는 클래스인지 조사한다.
- A 클래스에서 B 클래스를 선언만 하고 생성하기 전까지는 실제로 클래스의 레퍼런스를 참조하는 것이 아니라 심벌 참조, Symbolic Reference를 참조한다. 이 심벌 참조가 런타임 상수 풀에 저장된다.
- 그리고 이후 실제 B 클래스가 생성되면 심벌 참조를 실제 레퍼런스로 바꿔치기한다. 여기서 '바꿔치기'는 심벌참조를 위의 'klass 포인터'로 바꾸는 것을 의미한다. 보통은 이 바꿔치기된 주소도 런타임 상수 풀에 저장한다.
- 메서드 영역은 GC의 대상이 아니므로 심벌참조는 참조가 해제되어도 계속 메모리를 점유한다.
11-26 23:00 수정
객체와 클래스를 명확히 구분해야한다. 여기서 해당하는 사항들은 모두 '클래스가 로드되는 과정'이다. 그렇기 때문에 객체가 힙메모리에 저장되는 과정과는 아예 무관하다. 객체가 생성될 때는 B 클래스의 인스턴스를 잠시 null로 설정했다가 B 클래스를 생성하고 그 주소값이 A의 인스턴스로 저장되는 형태로 가동된다.
심벌 참조를 왜 사용하는지, 그리고 그 장점에 대한 글을 다음 글을 참고하면 좋을 듯하다. 우선 가장 큰 이유는 성능향상.
https://00shin.tistory.com/142
JVM 밑바닥까지 파헤치기(심벌참조)
심벌 참조(Symbolic Reference)란? 심벌 참조(Symbolic Reference)는 JVM이 클래스, 필드, 메서드 등의 이름을 문자열 등의 심벌(기호)로 저장하고, 필요할 때 실제 메모리 주소로 변경하는 방식을 의미한다.J
00shin.tistory.com
이렇게
1. 메서드 영역에서는 클래스로더가 가져온 클래스의 구체적인 정보를 저장하고,
2. 메서드 영역 내부에 각 클래스마다 구현된 런타임 상수 풀에 그 클래스의 문자열 리터럴과 문자열 이외의 원시값 리터럴을 저장하고, (문자열 리터럴은 런타임 상수 풀에 저장되지만, 그 이외의 원시값들은 필요한 경우에만 저장된다.)
3. 해당 클래스가 참조 중인 객체의 심벌참조 및 직접참조된 주소값을 저장한다.
여기서 문자열 리터럴의 저장도 중요한데, 실제 String 객체는 원시타입이 아니므로 힙메모리 내부에 String constant Pool에 저장되고, 그 참조값이 런타임 상수 풀에 저장된다. 이렇게함으로 같은 문자열의 경우에는 메모리 낭비를 줄일 수 있고, 아무런 참조도 되지 않는 문자열의 경우에는 GC 대상이 되도록 할 수도 있다.
단, 생성자를 사용해 문자열을 생성하면 String constant Pool에 값이 저장된다. 따라서 성능과 메모리 최적화를 위해 이를 지양해야한다.
런타임 상수 풀도 역시 크기를 동적으로 잡을 수 있으며, 새로운 상수가 추가될 수 있다.
원래 자바에서는 어느 상수 풀이던 간에 컴파일 시점에서 모든 상수가 생성되어야하지는 않다. 동적으로 생성하고 나중에 풀에 넣어도된다.
https://dev-dx2d2y-log.tistory.com/124
[CS] 자바의 정석 독서 #16 - String 클래스 파헤치기
왜 200쪽짜리 자몽살구클럽은 2시간만에 읽고 150쪽짜리 java.lang 패키지와 유용한 클래스는 며칠동안 읽고있지...? https://dev-dx2d2y-log.tistory.com/122 [CS] 자바의 정석 독서 #15 - java.lang 패키지와 Object 클
dev-dx2d2y-log.tistory.com
대표적으로 이전에 String에 대해서 다룰 때 다루지 못한 메서드 중에 .intern() 메서드가 있다.
이 메서드는 String 풀에 해당 문자열을 추가하는 역할을 한다.
다만 런타임 상수 풀 역시 지정된 메모리를 벗어나서 저장하려하면 OutOfMemoryError을 발생시킨다.
다이렉트 메모리
런타임 영역에 속하진 않지만 I/O 과정에서 힙메모리가 아닌 직접 로컬메모리를 할당할 수 있는데, 이 과정에서 사용자의 로컬 메모리의 크기를 벗어나려하면 OOME가 발생한다. 그렇기 때문에 힙메모리만 고려하는 것이 아니라 로컬 메모리, 다이렉트 메모리 역시 고려해야한다.
이렇게 메모리에 대해서 알아봤다.
음.. 우선은 책으로는 한 7쪽 정도 되는 짧은 구간인데 되게 어려웠다. 아직 운영체제를 모르기도하고 메모리 관련 지식도 부족하기도하고. 하지만 그래도 자바 메모리가 JVM 구동의 기초가 되는 부분일테니 꾸준히 알아둔다면 좋을 것 같다.
내용이 많이 난해하고 복잡한데 이 부분은 다른 장과 절을 읽고 상호보완해나가며 메꿀 예정이다. 발화 블로그에서는 아예 이리저리 장, 절 구분없이 섞인 블로그 게시글이 올라가겠지.
'CS > JVM' 카테고리의 다른 글
| [JVM] GC의 객체회수과정은 어떻게 일어나는가? - 마크-스윕, 마크-카피, 마크-컴팩트 (0) | 2025.12.26 |
|---|---|
| [JVM] 참조 카운팅 알고리즘과 파이썬의 순환 검출 알고리즘에 대해서 (0) | 2025.12.25 |
| [JVM] JVM 밑바닥까지 파헤치기 독서 #4 - 메모리 영역 별 오버플로우 (1) | 2025.12.06 |
| [JVM] JVM 밑바닥까지 파헤치기 독서 #3 - 자바는 어떻게 객체를 불러오는가? (0) | 2025.12.05 |
| [JVM] JVM 밑바닥까지 파헤치기 독서 #2 - 자바는 어떻게 객체를 저장하는가? (0) | 2025.11.27 |
