자바 가상 머신(JVM)이 관리하는 메모리 영역
런타임 데이터 영역은 하기와 같이 분할되어 있다.
메서드 영역(런타임 상수 풀), 가상 머신 스택, 네이티브 메서드 스택, 힙, 프로그램 카운터(pc) 레지스터
힙은 생성된 객체들이 존재하는 걸로 알고 있고,
그림에 따르면 힙과 실행 엔진이 상호작용을 하며(60p),
[ 실행엔진 → 네이티브 라이브러리 스페이스],
네이티브 라이브러리 스페이스와 프로그램 카운터(pc) 레지스터가 상호작용을 하고,
[ 네이티브 라이브러리 스페이스 → 로컬 메서드 라이브러리 ] 이렇게 정의되어 있다.
메서드 영역(런타임 상수 풀), 힙, 실행엔진, 네이티브 라이브러리 인터페이스는 모든 스레드가 공유하는 데이터 영역이다.
각 스레드의 독립적인 메모리 영역
1. 프로그램 카운터(pc) 레지스터
프로그램 카운터(pc) 레지스터(, 자바 가상 머신 스택, 네이티브 메서드 스택)는
스레드별 데이터 영역이다. 즉 모든 스레드가 공유하는 데이터 영역이 아니다.
각각의 스레드가 자기만의 메모리 영역을 가지고 있고(스레드 프라이빗 메모리)
그 안에 프로그램 카운터 레지스터를 포함하고 있다.
각 스레드마다 독립적인 메모리 영역(자기만의 데이터를 저장하는 영역)을 갖고 있으며,
이를 "스레드 프라이빗 메모리" 라고 한다. 이 영역에는 3가지 주요 구성이 존재한다.
프로그램 카운터 레지스터, 가상 머신 스택, 네이티브 메서드 스택이다.
이 세 가지는 모두 스레드별로 생성되며, 다른 스레드가 공유되지 않는다.
프로그램 카운터 레지스터는 현재 실행중인 스레드의 '바이트코드 줄 번호'를 저장한다.
자바 가상 머신에서의 멀티스레딩은 CPU 코어를 여러 스레드가 교대로 사용하는 방식으로 구현되기 때문에
특정 시각에 각 코어는 한 스레드의 명령어만 실행하게 된다. 따라서 스레드 전환 후 이전에 실행하다 멈춘 지점을
정확하게 복원하려면 스레드 각각에는 고유한 프로그램 카운터가 필요하다.
각 스레드의 카운터는 스레드 프라이빗 메모리의 프로그램 카운터 레지스터라는
메모리 영역에 저장되어 다음 실행할 명령어를 찾을 수 있게 된다.
단, 스레드가 네이티브 메서드를 실행 중일 때는 프로그램 카운터 레지스터에 undefined가 저장된다.
[Thread A]
└ 프로그램 카운터 레지스터
└ JVM 스택
└ 네이티브 메서드 스택
2. 가상 머신 스택
각 스레드별 가상 머신 스택이라는 메모리 영역을 갖으며, 이 영역은 연결된 스레드와 운명을 같이 한다(생성/삭제 시기가 일치한다)
하나의 스레드는 프로그램 실행 도중에 수 많은 메서드를 호출하고, 그 호출 정보들을 (가상 머신) 스택에 차곡차곡 쌓아가며 실행한다.
→ 각 메서드가 호출될 때마다 자바 가상 머신은 스택 프레임을 만들어 지역 변수 테이블, 피연산자 스택, 동적 링크, 메서드 반환 값 등의 정보를 저장한다. 그런 다음 스택 프레임을 가상 머신 스택에 push하고, 메서드가 끝나면 pop하는 일을 반복한다.
가상 머신 스택 메모리 영역에서 발생할 수 있는 2가지 오류
1) StackOverFlowError: 스레드가 요청한 스택 깊이가 가상 머신이 허용하는 깊이보다 클 때 발생하는 오류
가상 머신이 허용하는 깊이란 하나의 스레드에서 사용할 수 있는 스택 메모리의 최대 크기다.
스택 깊이는(호출 가능 메서드 수) 메서드당 필요한 프레임의 크기에 따라 달라지므로
프레임이 작다면 호출 가능 메서드의 수가 수천, 수만 번도 가능하지만,
프레임의 크기가 크다면 수백 번만에 overflow가 발생할 수 있다.
이미 확보한 스레드 전용공간에서(책을 쌓을 수 있는 책상을 배치한 상태에서)
스레드가 진행되면서 메서드를 반복 호출하면 스택 프레임이 쌓이고,
프레임 크기가 너무 크거나 너무 많이 쌓여서 발생하게 되는 것이다.
2) OutOfMemoryError: 스택 용량을 동적으로 확장할 수 있는 자바 가상 머신에서는
스택을 확장하려는 시점에 여유 메모리가 충분하지 않다면 OutOfMemoryError를 던진다.
stack 확장을 시도했지만 메모리가 부족해서 실패한 경우다.
스택을 동적으로 확장할 수 있다는 것은 처음에는 작은 스택을 할당하고,
필요할 때 점점 키울 수 있는 구조를 의미한다.
JVM은 스레드를 생성할 때마다 스레드 전용 스택 공간을 메모리로부터 확보해야 한다.
그런데 OS나 JVM이 메모리를 더 할당해 줄 수 없는 상황에서는 스레드를 더 이상 만들 수 없고,
→ 교실에 책상이 포화상태라 책을 쌓을 수 있는 책상을 더 이상 들여놓지 못하는 경우
이 때 발생하는 것이 OutOfMemoryError다.
모든 스레드가 공유하는 데이터 영역
1. 힙(heap, GC 힙)
자바에서 힙 영역은 자바 애플리케이션이 사용할 수 있는 가장 큰 메모리다.
자바 힙의 유일한 목적은 객체 인스턴스를 저장하는 것이고,
자바 세계의 거의 모든 객체 인스턴스가 힙에 할당된다[저장된다]
'자바 가상 머신 명세' 에는 "모든 객체 인스턴스와 배열은 힙에 할당된다"라고 적혀 있다.
자바 힙은 가비지 컬렉터가 관리하는 메모리 영역이기 때문에 GC 힙이라고도 불린다.
메모리 회수 관점에서 대다수 현대적인 가비지 컬렉터(GC)는 세대별 컬렉션 이론을 기초로 설계됐다.
*** 세대별 컬렉션 이론이란? 객체의 생존 기간(라이프 사이클)에 따라 자바 가상 머신의 런타임 데이터 영역중
모든 스레드가 공유하는 힙 메모리 영역을 세대별로(generation, 구간이라고 이해하면 될 듯) 나누어
세대별로 다른 방식과 주기로 가비지 컬렉션(더 이상 사용되지 않는 객체를 JVM이 자동으로 찾아내서
메모리에서 제거하는 것)을 수행하는 전략이다.
메모리 할당 관점에서 자바 힙은 모든 스레드가 공유한다.
따라서 객체 할당 효율을 높이고자 스레드 로컬 할당 버퍼 여러 개로 나뉜다.
***스레드 로컬 할당 버퍼(Thread-Local Allocation Buffer)란?
JVM 내부에서 객체를 더 빠르게 생성하기 위해 힙 영역을 스레드별로 나눠 쓰는 버퍼다.
→ 자바에서는 객체를 매우 자주 생성한다. new SomeObject()처럼 객체를 만들면
이 객체는 힙 영역에 저장되는데, 모든 스레드가 공용 힙 영역에 동시에 접근해서 동일한 객체를 생성하면
동시성 문제가 발생하고, 동시성 문제는 락을 필요로 하는데 락은 성능을 저하하기 때문에
성능을 높이기 위해 JVM 이 힙의 일부를 스레드 전용 공간으로 나누어
락 없이 객체를 할당할 수 있도록 해주는 기능이 TLAB인 것이다.
*** 자바에서 객체 생성시 여러 스레드가 동시에 힙에 접근해서 생성된 객체를 메모리에 할당하려고 할 때,
힙 포인터(다음 할당 위치)가 충돌하거나 꼬일 수가 있는데 이를 동시성 문제라고 한다.
그래서 TLAB이 등장했고, JVM은 이런 힙 할당의 동시성 문제를 피하기 위해 스레드마다
TLAB(스레드 로컬 할당 버퍼)라는 스레드 전용 힙 조각을 만들어준다.
TLAB 사용시 각 스레드는 자신의 TLAB 안에서 객체를 생성하므로
공유 힙에 접근할 필요가 없고, 락 없이 빠르고 안전하게 할당이 가능해진다.
*** 동시성 문제가 발생하는 지점은 공용 힙 공간에 저장할 인스턴스 공간을 여러 스레드가 동시에 할당할 때다.
자바 힙은 크기를 고정할 수도 있고, 확장할 수도 있게 구현할 수 있다(-Xmx와 -Xms 매개 변수 사용)
'공부기록용' 카테고리의 다른 글
| php 관련 study 기록 (0) | 2025.08.29 |
|---|---|
| php 환경설정, 폴더구조, 기초문법 일부 (3) | 2025.08.26 |
| HashMap(일부 메서드를 직접 구현하며 이해하기, chatgpt 도움 받음, 아직도 잘 모르겠음) (4) | 2025.08.08 |
| IO - Input, Ouput(1) (2) | 2025.07.22 |
| Thread (1) | 2025.07.16 |