- JVM SPEC에 따르면 가상 머신이 관리하는 메모리는 다음과 같은 데이터 영역들로 구성된다.

프로그램 카운터
-
프로그램 카운터 레지스터는 작은 메모리 영역으로, 현재 실행 중 스레드의 ‘바이트코드 줄 번호 표시기’ 라고 생각하면 쉬움
-
자바 가상 머신 개념 모형(가상 머신 일반적 형태)에서 바이트코드 인터프리터는 이 카운터의 값을 바꿔 다음에 실행할 바이트코드 명령어를 선택하는 식으로 동작
- 프로그램의 제어 흐름, 분기, 순환, 점프 등을 표현 하는 것
- 예외 처리나 스레드 복원 같은 모든 기능이 바로 이 표시기를 활용해 이루어짐
-
자바 가상 머신에서 멀티스레딩은 CPU 코어를 여러 스레드가 교대로 사용하는 방식으로 구현
- 특정 시각에 각 코어는 한 스레드의 명령어만 실행 하게 됨
- 따라서, 스레드 전환 후 이전 실행하다 멈춘 지점을 정확히 복원하려면 스레드 각각에는 고유한 PC 가 필요
- 각 스레드의 카운터는 서로 영향 안주는 독립된 영역에 저장
- 이 메모리 영역을 스레드 프라이빗 메모리 라 함
-
스레드가 자바 메서드 실행 중일 때는 실행 중인 바이트코드 명령어의 주소가 프로그램 카운터에 기록
- 스레드가 네이티브 메서드를 실행 중일 때 프로그램 카운터 값은
Undefined
- 스레드가 네이티브 메서드를 실행 중일 때 프로그램 카운터 값은
-
이 프로그램 카운터 영역은 <<자바 가상 머신 명세>>에서
OutOfMemoryError조건이 명시되지 않은 유일한 영역 -
현재 실행 중인 JVM 명령어 주소(바이트 주소)를 저장
-
스레드가 어떤 명령을 실행 중인지 추적 용도
-
스레드가 자바 메서드 실행 중일 때는 실행 중인 바이트코드 명령어의 주소가 기록
-
스레드가 네이티브 메서드 실행 중일 때는 Undefined 기록
자바 가상 머신 스택
-
프로그램 카운터처럼 가상 머신 스택도 ‘스레드 프라이빗’ 하다
-
연결된 스레드와 운명을 같이 함(생성/삭제 시기 일치)
-
가상 머신 스택은 자바 메서드를 실행하는 스레드의 메모리 모델을 설명
- 각 메서드가 호출될 때마다 자바 가상 머신은 스택 프레임을 가상 머신 스택에 푸시하고, 메서드가 끝나면 팝하는 일 반복
-
자바의 메모리 역역을 힙 메모리와 스택 메모리로 구분하는 사람이 많음!
- 이 구분법은 전통적인 C, C++ 프로그램의 메모리 구조에서 기인, 자바 언어를 설명하기에는 무리 있음
- 자바의 메모리 영역 구분은 훨씬 복잡!
- 그럼에도 불구하고, 이 구분법이 널리 쓰인다는 사실은 사실 이 두가지가 객체 메모리 할당과 가장 밀접하고, 개발자가 가장 신경써야 할 영역이라는 방증
- ‘스택’이라 하면 보통 방금 이야기한 자바 가상 머신 스택 가리키는데, 그중 특히 지역 변수 테이블을 가리킬 때가 많음
-
지역 변수 테이블에는 자바 가상 머신이 컴파일 타임에 알 수 있는 다양한 기본 데이터 타입, 객체 참조, 반환 주소 타입을 저장
- 여기서 이 데이터 타입들을 저장하는 공간을 지역 변수 슬롯이라 함
- 일반적으로 슬롯 하나의 크기는 32비트
- 따라서
double타입처럼 64비트인 데이터는 슬롯을 두개 차지하고, 나머지 타임은 모두 슬롯 하나에 저장
- 따라서
- 지역 변수 테이블을 구성하는 데 필요한 데이터 공간은 컴파일 과정에서 할당
- 자바 메서드는 스택프레임에서 지역 변수용으로 할당받아야할 공간의 크기가 이미 완벽히 결정되 있음(메서드 실행 중에 절대 변하지 않음)
- 여기서 이야기한 ‘크기’는 변수 슬롯 개수
- 가상 머신이 변수 슬롯을 구현하는데 사용하는 메모리의 실제 크기는 어떻게 구현하느냐에 따라 달라질 수 있다(당근인 이야기)
-
<자바 가상 머신 명세>는 스택 메모리 영역에서 두 가지 오류가 발생 할수 있도록 정의 - 1. 스레드가 요청한 스택 깊이가 가상 머신이 허용하는 깊이보다 크다면
StackOverflowError던짐 - 2. 스택 용량을 동적으로 확장할 수 있는 자바 가상 머신에서는 스택 확장하려는 시점에서 여유 메모리 충분하지 않다면OutOfMemoryError던짐 -
메서드 호출 시, 해당 메서드의 지역 변수, 매개 변수, 리턴 주소 등이 저장
-
원시 타입 변수와 참조 변수 둘다 저장
-
LIFO 구조
-
JVM Spec 에서는 자바 스택에서 두 가지의 오류가 발생 할 수 있도록 정의
- 스레드가 요청한 스택이 가상 머신이 허용하는 것을 넘어선다면 StackOverflowError
- 스택 용량을 동적으로 확장할 수 있는 JVM의 경우에서는 스택 확장하려는 시점에 여유 메모리가 충분치 않다면 OutOfMemoryError
네이티브 메서드 스택
-
가상 머신 스택과 비슷한 역할
- 차이점이라면, 가상 머신 스택은 자바 메서드(바이트코드)를 실행할 때 사용하고
- 네이티브 메서드 스택은 네이티브 메서드를 실행할 때 사용한다는 것
-
<자바 가상 머신 명세>는 네이티브 메서드 스택에서 메서드를 어떤 구조로 어떻게 표현해야 하는지 관련 아무것도 명시 X
- 따라서, 가상 머신 구현자 마음대로 표현이 가능
- 네이티브 메서드 스택과 가상 머신 스택을 하나로 합쳐 놓은 가성 머신 있다(핫스팟 가상 머신 포함)
- 가상 머신 스택처럼 이 네이티브 메서드 스택도 스택 허용 깊이 초과시
StackOverflowEroro던지고, 스택 확장에 실패 시OutOfMemoryError던짐
-
C, C++ 로 작성된 네이티브 코드 실행 위한 공간
-
native 키워드로 정의된 메서드 실행 시 사용됨
-
OS 의존적 코드가 실행될 때 필요한 정보 저장
-
참고로 HotSpot VM 은 Native Method Stack 과 Java Stack을 구분 안하고 하나로 두고 씀
자바 힙
-
자바 힙은 자바 애플리케이션에서 사용될 수 있는 가장 큰 메모리
- 자바 힙은 모든 스레드가 공유
- 가상 머신이 구동될 때 만들어짐
- 유일한 목적: 객체 인스턴스 저장
- 자바 세계의 ‘거의’ 모든 객체 인스턴스가 이 영역에 할당
- <자바 가상 머신 명세>에는 “모든 객체 인스턴스와 배열은 힙에 할당된다”라고 적혀있음
- The heap is the run-time data area from which memory for all class instances and arrays is allocated.
- 그런데 ‘거의’라 표현한 이유는?
- 현 관점에서 자바 언어 계속 발전하면서 앞으로는 값 타입도 지원할 것으로 보임. 당장만 생각하더라도 JIT 컴파일 기술 발전하면서, 특히 탈출 분석 기술이 날로 발전하면서 스택 할당과 스칼라 치환 최적화 방식이 살짝 달라짐.
- 따라서, 모든 자바 객체 인스턴스가 힙에 할당된다는 설명이 절대적 진리라고 보기에 조금씩 애매해지고 있기 때문…(음? 이해가 안감)
-
자바 힙은 가비지 컬렉터가 관리하는 메모리 영역이기에 어떤 문헌에서는 GC 힙 이라고도 함
- 메모리 회수 과정에서 대다수 현대적 가비지 컬렉터는 세대별 컬렉션 이론(generational collection theory)를 기초로 설계됨
- 그래서, 자바 힙 설명할 때 신세대(new generation), 구세대(old gemeration), 영구 세대, 에덴 공간(eden space), 생존자 공간에서부터(from survivor space), 생존자 공간으로(to survivor space) 같은 용어 자주 등장
- 이런 개념은 이어지는 장들에서 반복해서 고고
- 여기서 짚어야 하는것은 이 영역 구분은 가비지 컬렉터들의 일반적인 특성 또는 설계 방식일 뿐, 반드시 이 형태로 메모리를 구성해야 한다는 뜻 X
- <자바 가상 머신 명세>의 자바 힙 절에는 세부 영역 구분에 관한 이야기 자체가 없음
- 메모리 회수 과정에서 대다수 현대적 가비지 컬렉터는 세대별 컬렉션 이론(generational collection theory)를 기초로 설계됨
-
여러 문헌에서 “자바 가상 머신의 힙 메모리는 신세대, 구세대, 영구 세대, 에덴, 생존자 공간 등으로 나뉜다”라 설명
- G1 컬렉터 등장한 2010년 전후로 핫스팟 가상 머신은 업계에서 확고한 주류가 되었고, 핫스팟의 가비지 컬렉터들은 모두 신세대 컬렉터와 구세대 컬렉터를 조합해 동작하는 전통적인 세대 구분을 따름. 당시라면 이 설명이 크게 틀린게 없다
- 하지만, 오늘날의 가비지 컬렉터 기술은 그 시절에 머무리지 않음!
- 심지어 핫스팟에서 세대 단위 설계를 따르지 않은 컬렉터가 포함되어있음
-
메모리 할당 관점에서 자바 힙은 모든 스레드가 공유
- 따라서, 객체 할당 효율 높이고자 스레드 로컬 할당 버퍼 여러 개로 나뉨
- 하지만, 어떤 시각에서 보든 또는 어떻게 나누든 상관없이 데이터가 자바 힙에 저장된다는 사실은 변치 않음
- 어던 세부 영역이든 객체의 인스턴스만 저장될 수 있다 힙에는!
- 자바 힙을 다시 작게 구분하는 목적은
- 오직 메모리 회수와 할당을 빠르게 하기 위함
-
<자바 가상 머신 명세> 따르면 자바 힙은 물리적으로 떨어진 메모리에 위치해도 상관없으나 논리적으로는 연속되어야 함
- 파일을 저장할 때 디스크 공간을 활용하는 방식고 같다(파일 각각은 논리적으로 연속된 공간에 저장)
- 참고로, 대다수 가상 머신이 큰 객체(주로 배열 객체)는 물리적으로도 연속된 메모리 공간 사용하도록 구현→ 저장 효율 높이고, 구현 로직 단순히 유지하기 위함
-
자바 힙은 크기를 고정할 수도, 확장할 수도 있게 구현할 수 있음
- 요즘 주류 가상 머신들은 모두 확장 가능한 형태로 구현(-Xmx, -Xms 매개 변수 사용)
- 새로운 인스턴스를 할당해 줄 힙 공간이 부족하고 힙을 더는 확장할 수 없다면
OutOfMemoryError던짐
-
인스턴스(객체)와 배열이 저장되는 공간
-
new 키워드로 생성한 객체는 힙에 저장
-
JVM의 Garbage Collector 가 관리하는 영역(GC Heap 이라고도 불림)
-
모든 스레드가 공유 하는 공간
-
새로운 인스턴스를 할당해 줄 힙 공간이 부족하고 힙을 더는 확장할 수 없다면
OutOfMemoryError
메서드 영역
-
메서드 영역도 자바 힙 처럼 모든 스레드가 공유!
- 여기는 가상 머신이 읽어 들인 타입 정보, 상수, 정적 변수 그리고 JIT 컴파일러가 컴파일한 코드 캐시등을 저장하는데 이용
- <자바 가상 머신 명세>에서는 메서드 영역도 논리적으로는 힙의 한 부분으로 기술하지만, 자바 힙과 구분하기 위해 논힙(non-heap)이라 부리기도 함
-
메서드 영역 설명하려면 영구 세대 이야기를 해야함
- 특히 JDK 7 까지는 많은 자바 개발자가 핫스팟 가상 머신용으로 프로그램 개발ㄹ하고 배포, 그리고 당시 많은 사람이 메서드 영역을 영구 세대라 부르며 두 개념을 혼동
- 이 둘은 다름
- 당시 핫스팟 가상 머신 개발팀은 가비지 컬렉터의 수집 범위를 메서드 영역까지로 확장하기로 결정
- 그래서 메서드 영역을 영구 세대에 구현했을 뿐
- 그 결과 핫스팟의 가비지 컬렉터는 메서드 영역도 마치 자바 힙처럼 관리할 수 있었고, 그 덕에 메서드 영역을 관리하는 코드가 따로 필요 없으니 작업량 줄일 수 있었다
- 하지만, BEA JRockit, IBM J9 등 다른 가상 머신에는 애초에 영구 세대라는 개념이 없었음 → <자바 가상 머신 명세>가 특정 방식 강제 않기 때문.
-
지금시점에서 메서드 영역을 영구 세대에 구현한 결정은 좋지 않음
- 이 설계 때문에 자바 애플리케이션들이 메모리 오버플로를 겪을 가능성이 커짐
- 영구 세대의 최대 크기는
XX:MaxPermSize로 제한되며, 이 값 설정하지 않더라도 기본값 정해져 있음- 예를 들어, 32비트 시스템이라면 4GB 한계까지 아무 문제 없었음. 그리고 소수이기는 하나 영구 세대 때문에 가상 머신 따라 성능이 달라지는 메서드가 생겨남(ex `String::intern())
-
BEA 인수하여 JRockit 의 소유권 얻은 오라클은 JRockit의 훌륭한 기능을 핫스팟에 이식 시작함
- 관리도구인 JMC 가 대표적
- 하지만, 두 가상 머신의 메서드 영역을 구현한 방식이 달라 쉽지 않은 일이었음
-
JDK 6 시절, 핫스팟 개발 팀은 핫스팟의 미래 위해 영구 세대 포기하고 점진적으로 메서드 영역을 네이티브 메모리에 구현할 계획 세움.
- 그래서 JDK 7에 이르러 핫스팟은 그 전까지 영구 세대에서 관리하던 문자열 상수와 정적 변수 등의 정보를 자바 힙으로 옮김. JDK 8에 와서는 영구 세대라는 개념을 완전히 지우고 JRockit, J9처럼 네이티브 메모리에 메타스페이스를 구현.
- JDK 7 까지 영구 세대에 남아 있던 모든 데이터(주로 타입 정보)를 메타스페이스로 옮긴 것
-
<자바 가상 머신 명세> 에서는 메서드 영역에 제약 거의 안둠
- 자바 힙과 마찬가지로 연속될 필요 없으며, 크기 고정 가능하고, 확장 가능하게도 가능
- 심지어 가비지 컬렉션 하지 않아도 괜찮음
- 솔직히 이 영역에서는 쓰리게를 회수할 일이 거의 없어서
- 그렇다고 메서드 영역에 한번 들어온 데이터가 영구적이라는 의미는 아님
-
메서드 영역에서 회수할 대상은 거의 대부분 상수 풀과 타입이라서 회수 효과가 상대적으로 매우 작음
- 특히 타입은 회수할 수 있는 조건이 상당히 까다롭기까지함.
- 하지만 가끔 필요할 때도 있는데, 실제로 썬 시설 핫 스팟의 버그 목록에는 메서드 영역을 완벽히 회수하지 않아 메무리가 누수되는 심각한 버그가 존재했음
-
<자바 가상 머신 명세> 따르면 메서드 영역이 꽉 차서 필요한 만큼 메모리 할당 불가 시
OutOfMemory던짐 -
정적(static) 데이터가 저장되는 영역
-
Class Area, Static Area 라고도
-
클래스 로딩 시, 클래스의 구조 정보(클래스 이름, 부모 클래스 이름, 메서드 정보, 필드 정보 등) 저장
-
static 변수, 상수, 메서드 바이트코드, 런타임 상수 풀(Runtime Constant Pool) 포함
-
Java 8 부터이 영역은 Metaspace 로 대체됨
- Metaspace로 되면서 JVM 프로세스의 메모리가 아닌 네이티브 메모리 영역에 데이터를 저장
- 기존에 클래가스 많이 로딩되면
java.lang.OutOfMemoryError: PermGen space자주 발생 - 적정 PermSize를 찾기 어려움 → 운영 중 OOM 발생 가능성 증가
- 네이티브 메모리로 옮기면서 운영체제 알아서 동적 확장등 해주도록 함
런타임 상수 풀
- 이건 메서드 영역의 일부
- 상수 풀 테이블에는 클래스 버전, 필드, 메서드, 인터페이스 등 클래스 파일에 포함된 설명 정보에 더해 컴파일타임에 생성된 다양한 리터럴, 심벌 참조가 저장됨
- 가상 머신이 클래스 로드할 때 이런 정보를 메서드 영역의 런타임 상수 풀에 저장
- 자바 가상 머신은 (상수 풀을 포함해) 클래스 파일의 각 영역별로 엄격한 규칙 정해놓음
- 예컨대, 가상 머신이 클래스 파일 로드해 실행하려면 각 바이트에는 명세가 요구하는 데이터가 들어 있어야 한다
- 다만, 런타임 상수 풀에 대해서는 명세가 요구사항을 상세히 정의하지 않아서 가상 머신 제공자가 알아서 구현 가능
- 그렇지만 클래스 파일에 기술된 심벌 참조는 물론, 심벌 참조로 부터 번역된 직접 참조 역시 런타임 풀에 저장되는게 일반적
- 클래스 파일의 상수 풀과 비교해 런타임 상수 풀의 중요한 특징이 하나 더 있음
- 바로 동적 이라는 점
- 자바 언어에서는 상수가 꼭 컴파일 타임에 생성되야한다는 규칙 없음
- 즉, 상수 풀의 내용 정부가 클래스 파일에 미리 완벽히 기술되어 있는게 아님
- 런타임에도 메서드 영역의 런타임 상수 풀에 새로운 상수가 추가될 수 있음
- 개발자들이 많이 쓰는
String::intern()에 그 특성이 반영
- 런타임 상수 풀은 메서드 영역에 속하므로 당연히 메서드 영역 넘어 확장이 불가. 그래서 상수 풀 공간 부족시
OutOfMemoryError던짐
다이렉트 메모리
- 다이렉트 메모리는 가상 머신 런타임에 속하지 않으며 명세에 정의된 영역도 아님. 하지만 자주 쓰이는 메모리 이며
OutOfMemoryError의 원인이 될 수 있어서 설명- JDK1.4 에서 NIO 가 도입되면서 채널과 버퍼 기반 I/O 메서드가 소개됨.
- NIO는 힙이 아닌 메모리를 직접 할당할 수 있는 네이티브 함수 라이브러리 이용하며, 이 메모리에 저장되어 있는
DirectByteBuffer객체 통해 작업을 수행할 수 있음 - 따라서, 자바 힙과 네이티브 힙 사이에서 데이터를 복사해 주고받지 않아도 되어 일부 시나리오에서 성능을 크게 개선
- 물리 메모리를 직접 할당하기에 자바 힙 크기의 제약과 무관하지만, 이 역시 메모리라는 사실에는 변함 없음
- 따라서, 하부 기기의 총 메모리 용량(물리 메모리, 스와프 파티션, 페이징 파일 포함)과 프로세서가 다룰 수 있는 주소 공간 못 넘어섬
- 하지만, 서버 관리자들이
-Xmx등의 매게변수 설정 시 가상 머신의 메모리 크기만 고려하고 다이렉트 메모리는 간과 하는 경우가 제법 존재- 사용되는 모든 메모리 영역의 합이 물리 메모리 한계(물리적 제약과 운영체제 수준의 제약 포함)을 넘어서면 동적 확장 시도할 때
OutOfMemoryError발생
- 사용되는 모든 메모리 영역의 합이 물리 메모리 한계(물리적 제약과 운영체제 수준의 제약 포함)을 넘어서면 동적 확장 시도할 때