Chapter 2. Java Memory Area & Memory Overflow
2장. 자바 메모리 영역과 메모리 오버플로
2-2. 런타임 데이터 영역


프로그램 카운터
JVM이 현재 실행 중인 명령의 바이트코드 주소를 저장하는 작은 메모리 공간. JVM은 어떤 명령을 실행해야 하는지 추적할 수 있음
역할
JVM의 바이트코드 인터프리터는 프로그램 카운터에 저장된 주소를 읽고 해당 바이트 코드 명령을 실행 ( .java -> javac compiler -> .class -> 인터프리터/JIT compiler )
실행 종료 후, 프로그램 카운터의 값을 갱신하여 다음 명령어로 이동. (동적 실행 구조)
흐름 제어(조건문, 반복문, 예외 처리 등)를 수행할 때도 활용
멀티스레딩과 프로그램 카운터
JVM은 멀티스레딩을 지원하며, 각 스레드는 독립적인 실행 흐름을 가짐
스레든느 번갈아가며 CPU를 점유하고, 각 스레드는 자신만의 프로그램 카운터를 별도로 유지.
특정 스레드가 실행을 멈췄다가 다시 실행될 때, 프로그램 카운터의 값을 참조하여 중단된 위치에서 실행을 재개.
네이티브 메서드 실행 시
네이티브 메서드를 실행할 때는 프로그램 카운터를 유지할 필요X
네이티브 메서드 실행중인 스레드의 프로그램 카운터 값은 Undefined.
자바 가상 머신 스택 (JVM Stack)
JVM 스택은 각 스레드가 실행하는 메서드 호출 정보를 저장하는 공간이고, 스레드마다 독립적으로 생성됨. 해당 공간은 JVM이 스레드의 실행 상태를 관리하는데 사용되고, 스택 프레임 단위로 구성됨.
스택 프레임의 구성 요소
지역 변수 테이블(Local Variable Table) - 메서드의 지역 변수들을 저장
연산자 스택(Operand Stack) - 연산 과정에서 사용할 피연산자와 결과값을 임시 저장
동적 링크(Dynamic Linking) - 메서드 실행 중 참조해야 할 메서드나 클래스 정보를 저장
메서드 반환 주소(Return Address) - 메서드가 종료된 후 실행이 재개될 위치를 저장
스택 프레임 동작 방식
메서드가 호출될 때 새로운 스택 프레임이 생성되어 JVM 스택에 push 된다.
메서드 실행이 종료되면 해당 스택 프레임은 pop 되어 제거 된다.
JVM 스택과 메모리 오류
StackOverflowError: 너무 깊은 재귀 호출 / 무한 루프
OutOfMemoryError: JVM이 스택 확장을 시도할 때 메모리가 부족하면 발생
네이티브 메서드 스택 (Native Method Stack)
자바 코드가 아닌 네이티브 코드를 실행할 때 사용하는 스택. 스레드 별로 별도로 생성되며, JVM 명세에는 "네이티브 메서드 스택의 구조나 구현 방식에 대한 규정을 두지 않는다"
역할
JNI를 통해 호출된 네이티브 메서드가 사용하는 실행 환경 제공
네이티브 코드 실행을 위한 지역 변수 저장 및 연산 수행
네이티브 코드 실행 중 JVM과의 인터페이스 역할 수행
메모리 오류
네이티브 메서드 스택 크기가 초과되면 StackOverflowError
확장시 메모리가 부족하면 OutOfMemoryError
자바 힙 (Heap)
JVM에서 가장 큰 메모리 영역으로, 모든 객체 인스턴스가 저장되는 공간이며, JVM 시작과 함께 생성되고 모든 스레드가 공유함.
역할
객체 인스턴스 및 배열 저장
GC에 의해 관리됨
메모리 회수 및 재사용을 통해 효율적 관리
힙 영역 세분화 (Generatinal Garbage Collection)
Young(Eden, Survivor)
Old
Permanent Generation(JDK7까지): 클래스 메타데이터 저장(JDK8 이후 메타스페이스로 변경됨)
메모리 오류
OutOfMemoryError: 힙 메모리가 부족하여 새로운 객체를 할당 할 수 없을 때 발생
메서드 영역 (jdk 7힙 -> jdk 8 메타스페이스)
메서드 영역은 모든 스레드가 공유하는 메모리 공간. 클래스 관련 정보와 JIT 컴파일된 코드 캐시를 저장
저장되는 정보
로드된 클래스 정보 (클래스, 인터페이스, 필드, 메서드 정보)
런타임 상수 풀
정적 변수
JIT 컴파일된 코드
변화된 JDK 8 이후
OS가 네이티브 메모리 할당.
런타임 상수 풀 ( Runtime constant pool: 메서드 영역에 포함)
메서드 영역의 일부로, 클래스 로딩 시 생성되는 상수 및 심볼 정보를 저장하는 공간
포함되는 정보
클래스, 메서드, 필드의 심볼 정보
String Poll (interned 문자열)
상수 값 저장
메모리 오류
상수 풀 공간이 부족하면 OutOfMemoryError 발생
다이렉트 메모리 (Direct Memory: JVM 관리 영역X)
JVM의 힙 영역이 아니라 네이티브 메모리를 직접 할당하는 방식으로 사용
사용 목적
DirectByteBuffer를 활용한 고속 I/O 처리
데이터 복사를 최소화하여 성능 향상
메모리 할당 과정
ByteBuffer.allocateDirect()를 호출 -> OS에서 직접 네이티브 메모리를 할당
JVM 힙이 아닌 네이티브 메모리에 저장
JVM 내부적으로 DirectByteBuffer 객체를 생성하여 참조
사용 후 명시적으로 해제해야함
장점
빠른 성능
힙 메모리의 객체를 사용할 경우, 네이티브 코드로 데이터를 복사하는 과정이 필요.
다이렉트 메모리는 OS의 네이티브 메모리를 직접 사용하므로 즉시 접근 가능(I/O 최적화)
GC 영향없음
GC로 인한 성능 저하 없음
대용량 데이터 처리 최적화
네트워크 통신, 파일 입출력, 데이터베이스 연동 등 대용량 데이터를 처리할 때 효율적.
제한 사항
-Xmx
등의 설정으로 JVM의 힙 크기를 조절할 수 있지만, 다이렉트 메모리는 JVM의 관리 대상이 아니므로 메모리 누수 방지를 위해 개발자가 명시적으로 해제해야함.운영 체제의 물리 메모리 한계를 초과할 경우
OutOfMemoryError
발생 가능
// DirectByteBuffer를 이용한 파일 I/O 성능 최적화
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class DirectByteBufferFileIO {
public static void main(String[] args) throws Exception {
// 파일 채널 생성
FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt");
FileChannel inputChannel = fis.getChannel();
FileChannel outputChannel = fos.getChannel();
// DirectByteBuffer 할당 (네이티브 메모리 사용)
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (inputChannel.read(buffer) != -1) {
buffer.flip();
outputChannel.write(buffer);
buffer.clear();
}
inputChannel.close();
outputChannel.close();
fis.close();
fos.close();
}
}
JVM 힙을 거치지 않고 다이렉트 메모리를 사용하여 파일 데이터를 빠르게 복사
네이티브 메모리를 활용하므로 I/O 성능이 향상됨
2-3. 핫스팟 가상 머신에서의 객체 들여다보기
new 키워드를 사용하여 객체를 생성하지만, 내부에서는 추가 과정들이 존재.
1. 객체 생성 과정
클래스 로드 확인
해당 객체의 클래스가 이미 메모리에 로드되었는지 확인
로드되지 않았다면 클래스 로딩 -> 해석 -> 초기화 단계를 거침
힙 메모리 할당
객체의 크기를 결정하고 적절한 힙 공간을 찾아 메모리를 할당
메모리 할당 방식에는 포인터 증가(Bump-the-pointer)와 Free List 방식이 있음
객체 헤더 설정
객체의 기본 정보를 설정 (해시값, GC 연령 정보 등)
필드 초기화
객체의 모든 필드를 기본값으로 설정
생성자 호출
init 메서드를 실행하여 객체를 최종적으로 초기화
힙 메모리 할당 방식
객체용 메모리 공간 할당은 자바 힙에서 특정 크기의 메모리 블록을 잘라 주는 일. 객체에 필요한 메모리 크기는 클래스를 로딩하고 나면 완벽히 알 수 있음.
포인터 증가 방식 (Bump-the-pointer)
자바 힙이 연속적인 메모리 블록으로 관리될 때 사용
사용중인 메모리와 여유 메모리 사이의 경계를 포인터로 추적
새로운 객체를 생성하면 포인터를 해당 크기만큼 이동하여 메모리를 할당
GC가 한 번 실행될 때마다 메모리를 재정렬 해야함(Compact 필요)
단편화되지 않을 시에 매우 빠른 할당 속도.
Free List 방식
조각난 메모리 공간이 존재할 떄 사용(Compact 불필요)
사용 가능한 빈 메모리 블록 목록을 유지하고 거기에서 적절한 크기의 공간을 찾아 객체를 할당
메모리 관리가 유연하지만, 할당 속도가 상대적으로 느림
멀티스레드 환경에서의 객체 생성 문제
여유 메모리의 시작 포인터 위치를 수정하는 작업도 thread-safe 하지 않기 떄문에 race condition 발생 가능.
해결 방법은 두 가지 방법이 있다.
CAS
객체를 할당하는 과정에서 CAS 연산을 활용하여 포인터 갱신을 원자적으로 처리
포인터 값을 비교 후 안전하게 변경
TLAB (Thread-local allocation buffer)
각 스레드가 자신만의 작은 힙 영역을 별도로 할당하여 객체를 생성
스레드 간 경쟁 없이 독립적인 공간에서 객체를 빠르게 생성 가능
TLAB이 가득 차면 새로우 ㄴ힙 영역을 요청하여 사용
JVM 옵션으로 -XX;+UserTLAB을 설정하면 활성화 가능
JVM은 TLAB을 기본적으로 활성하여 사용
**TLAB 최적화 관련 JVM 옵션

생성자 호출과 객체의 완전한 초기화
객체는 new
키워드와 함께 생성되지만, 실제로 객체가 초기화되는 것은 생성자(<init>
메서드)가 실행된 이후이다.
new
키워드 → 힙 메모리 할당invokespecial
→ 생성자 호출 (초기화 수행)init
메서드 실행 → 완전한 객체 생성 완료
2. 객체의 메모리 레이아웃

핫스팟 가상 머신은 객체를 세 부분으로 나누어 힙에 저장한다.
객체 헤더 - 객체의 메타데이터를 포함
인스턴스 데이터 - 실제 객체 필드 값들이 저장됨
정렬 패딩 - 메모리 정렬을 위해 추가되는 공간
객체 헤더 (Object Header)
객체 헤더는 JVM이 객체를 관리하는 데 필요한 정보를 저장하는 영역
마크 워드 (Mark Word)
객체의 해시 코드 (hashCode)
GC 연령 (Age)
락 플래그 (Lock Flag, 동기화 정보)
스레드 경합 상태 (Thread Contention State)
평활화 스레드 타임스탬프 (Bias Locking Timestamp)

인스턴스 데이터
객체의 실제 데이터(필드 값들)가 저장되는 공간.
객체의 모든 필드(primitive + reference types) 저장
부모 클래스의 필드도 함께 저장
저장 순서는 JVM의 필드 할당 전략에 따라 달라질 수 있음
JVM 옵션
-XX:FieldsAllocationStyle
을 통해 필드 정렬 방식 변경 가능
핫스팟 JVM은 기본적으로 같은 크기의 필드를 묶어 정렬(padding을 최소화)하는 전략을 사용한다.
-XX:CompactFields=true
옵션을 설정하면 작은 필드를 빈 공간 없이 압축하여 저장
정렬 패딩
정렬 패딩은 메모리 정렬(alignment)을 맞추기 위해 추가되는 공간이다.
JVM은 8바이트 단위로 메모리를 정렬하는 경우가 많다.
인스턴스 데이터 크기가 8바이트 단위로 정렬되지 않는 경우, 패딩을 추가하여 정렬을 맞춘다.
CPU가 메모리에 빠르게 접근하도록 최적화하기 위한 기술.
3. 객체에 접근하기
자바 객체는 **참조(reference)**를 통해 접근한다. 참조 방식은 JVM 구현에 따라 다르지만, 핫스팟 JVM은 두 가지 방식을 제공한다.

핸들(Handle) 방식
핸들 방식에서는 힙(heap) 외부에 별도의 핸들 풀(handle pool)을 유지
핸들은 객체의 실제 주소가 아닌 핸들 풀의 주소를 저장하며, 핸들 풀은 객체의 실제 데이터 위치를 가리키는 포인터를 가진다.
장점
객체가 이동할 경우 핸들만 업데이트하면 되므로 참조를 갱신할 필요 없음
GC가 객체를 이동할 때도 핸들 주소는 그대로 유지됨
단점
객체에 접근할 때 항상 한 단계 추가적인 간접 참조가 필요
성능이 다이렉트 포인터 방식보다 약간 느릴 수 있음

다이렉트 포인터 (Direct Pointer) 방식. (핫스팟 JVM의 기본값)
다이렉트 포인터 방식에서는 객체 참조가 직접 힙 메모리의 객체 주소를 가리킨다.
장점
객체에 빠르게 접근 가능 (한 단계 간접 참조가 없음)
참조를 직접 가리키므로 CPU 캐시 최적화 가능
단점
GC가 객체를 이동하면 참조를 업데이트해야 함
메모리 주소가 직접 바뀌므로 GC의 복잡도가 증가
핸들과 다이렉트 포인터의 차이
핸들 방식 구조
객체 참조 → 핸들 풀 → 객체 실제 메모리
객체 이동 시 핸들만 업데이트하면 됨
다이렉트 포인터 방식 구조
객체 참조 → 객체 실제 메모리
객체 이동 시 참조 주소를 직접 변경해야 함

Last updated