동적 메모리 할당과 가비지 컬렉션 기술 측면에서 자바와 C++ 사이에는 높은 벽이 가로막고 있다.
벽 바깥쪽 사람들은 안으로 들어오고 싶어하고, 안쪽 사람들은 나가고 싶어한다.
(2부: 자동 메모리 관리, 서문, p.57)
p.58 ~ p.92
- C/C++ 은 메모리 관리 측면에서 전권을 가진 황제, 잡다한 막노동 까지 직접 하는 일꾼
- 자바 개발자는 가상 머신이 제공하는 자동 메모리 관리 메커니즘 덕분에 메모리 누수, 오버플로 문제를 겪지 않는다.
- 골치 아픈 메모리 관리를 가상 머신이 해줌
- but, 문제가 한번 터지면 가상 머신의 메모리 관리 방식을 이해하지 못하는 한 해결하기가 어렵다.
- JVM 은 자바 프로그램 실행하는 동안 필요한 메모리를 몇 개의 데이터 영역으로 나눠 관리함
- 긱 영역은 별도의 목적, 생성/삭제 시점 있음
- 어떤 영역은 가상 머신 프로세스 시작과 동시에 만들어지며, 어떤 영역은 스레드 시작/종료 맞춰 생성됨
- 그림 2-1: 자바 가상 머신 런타임 데이터 영역 PlantUML

- 현재 실행 중인 스레드의 바이트코드 줄 번호 표시기
- 예외 처리, 스레드 복원 같은 모든 기본 기능이 이 표시기를 활용해 이루어짐
- 스레드 전환 후 이전 지점을 복원하려면 각 스레드에 고유한 프로그램 카운터 필요
- 각 스레드 카운터는 서로 영향 주지 않는 독립된 영역에 저장 (Thread Private Memory)
- 스레드가 자바 메서드 실행 중일 때: 실행 중인 바이트코드 명령어의 주소를 프로그램 카운터에 저장
- 스레드가 네이티브 메서드를 실행 중일 때 프로그램 카운터 값은
Undefined
네이티브 메서드는 자바 바이트코드가 아니라 C/C++로 구현된 외부 코드이고,
JVM은 그 실행 위치를 추적할 수 없기 때문에 프로그램 카운터를 사용할 수 없어 Undefined로 처리함.
- JVM 명세에서
OutOfMemoryError 조건이 명시되지 않은 유일한 영역
- PC처럼 스레드 프라이빗함
- 연결된 스레드와 생성/삭제 시기 일치함
- 각 메서드가 호출될 때마다 스택 프레임을 만들어 여러 정보를 저장한다.
- 그리고 스택 프레임을 가상 머신 스택에 push하고, 메서드 종료되면 pop함
- 자바 메모리 영역을 heap, stack 으로 구분하는 사람이 많음.
- 전통적인 C/C++ 메모리 구조에서 기인한 것으로 자바 언어를 설명하기엔 무리가 있음 (자바 메모리 구분은 훨씬 복잡)
- '스택'이라 하면 자바 가상 머신 스택, 특히 지역 변수 테이블을 가리킬 때가 많음
- 지역 변수 테이블
- JVM이 컴파일 타임에 알 수 있는 다양한 정보를 저장함 (데이터 타입, 객체 참조, 반환 주소 타입)
- 일반적으로 테이블의 한 슬롯은 32 bit 임 (double 처림 64 bit 인 경우 두 슬롯을 차지함)
- 테이블을 구성하는데 필요한 공간은 컴파일 과정에서 할당됨
- 자바 메서드는 스택 프레임에서 지역 변수용으로 할당받아야할 공간의 크기(슬롯의 갯수)가 이미 완벽하게 결정되어 있다.
- 변수 슬롯을 구현하는데 사용한느 메모리 실제 크기는 가상 머신 구현 방식에 따라 32 bit, 64 bit, 그 이상이 될 수도 있다.
- 2가지 오류가 발생 가능함
StackOverflowError: 스레드가 요청한 스택 깊이가 가상 머신이 허용하는 깊이보다 클 경우
OutOfMemoryError: 스택 용량 동적으로 확장 가능한 JVM에서 확장하려는 시점에 메모리가 충분하지 않을 경우
- 클래식 VM과 달리 핫스팟 가상 머신은 스택 용량을 동적으로 늘리지 못함 -> 최근 JVM 에서는 발생 X
- 자바 가상 머신과 비슷한 역할을 함
- 차이점은 가상 머신 스택은 자바 메서드(바이트코드)를 실행할 때 사용하고, 네이티브 메서드 스택은 네이티비 메서드 스택을 실행할 때 사용한다는 점
- "자바 가상 머신 명세" 에서는 네이티브 메서드 스택을 어떻게 표현해야하는지 명세하지 않음.
- 네이티브 메서드 스택과 가상 머신 스택을 합쳐 놓은 가상 머신도 있음 (핫스팟?)
- 2가지 오류가 발생 가능
StackOverflowError: 네이티브 메서드 스택 허용 깊이 초과할 경우
OutOfMemoeryError: 스택 확장에 실패할 경우
- 자바 애플리케이션이 사용할 수 있는 가장 큰 메모리
- 유일한 목적은 객체 인스턴스를 저장하는 것이고 '거의' 모든 객체 인스턴스를 이 영역에 할당
- 구현 관점에서 자바 언어가 계쏙 발전하면서 모든 자바 객체 인스턴스가 힙에 할당된다는 설명은 절대적 진리라고 볼수 없게 됨
- 자바 힙을 설명할 때 신세대, 구세대, 영구 세대, 에덴 공간, 생존자 공간 등으로 나뉨
- 자바 가상 머신 명세에는 세부 영역 구분에 대한 자세한 이야기가 없으므로 반드시 이형태로 메모리를 구성해야하는 것은 아님
- 메모리 할당 관점에서 자바 힙은 모든 스레드가 공유함
- 자바 힙을 다시 작게 구분하는 목적은 오직 메모리 회수와 할당을 더 빠르게 하기 위함임
- 자바 가상 머신 명세에 따르면 자바 힙은 물리적으로는 떨어진 메모리에 위치해도 상관 없으나 논리적으로는 연속되어야함
- 하지만 대다수 가상 머신은 큰 객체(주로 배열 객체)의 경우 물리적으로도 연속된 메모리 사용하도록 구성함
- 새로운 인스턴스에 할당해 줄 힙 공간이 부족할 경우
OutOfMemoryError 발생 할 수 있음
- 메서드 영역도 모든 스레드가 공유함
- 가성 머신이 읽어 들인 타입 정보, 상수, 정적 변수, JIT 컴파일러가 컴파일한 코드 캐시 등을 저장
- 자바 가상 머신 명세에서는 메서드 영역도 논리적으로는 힙의 한 부분으로 명시하지만, 자바 힙과 구분하기 위해 '논힙(non-heap) 이라 부르기도 함
- 많은 사람들이 메서드 영역을 영구 세대라 부르며 두 개념을 혼동함
- 메서드 영역을 영구 세대에 구현했을 뿐임 (GC 등 작업량 감소)
- 돌이켜보면 좋은 결정은 아니었음. 이 설계 때문에 메모리 오버플로 가능성이 높아짐
- JDK 6 시절 핫스팟 개발팀은 영구 세대를 포기하고 메서드 영역을 네이티브 메모리에 구현할 계획을 세움
- 자바 가상 머신 명세에서는 메서드 영역에 제약을 거의 두지 않음.
- 이 영역에서는 GC 할 일이 거의 없다. 대부분 상수 풀과 타입이라서 회수 효과가 상대적으로 적음
- 메서드 영역이 꽉 차서 필요한 만큼 메모리 확보하지 못하면
OutOfMemoryError 발생
- 런타임 상수 풀은 메서드 영역의 일부
- 클래스 버전, 필드, 메서드, 인터페이스 등 클래스 파일에 포함된 설명 정보 + 컴파일 타임에 생성된 다양한 리터럴과 심벌 참조 저장
- 자바 가상 머신은 클래스 파일의 각 영역별로 엄격한 규칙을 정해 놓음
- 클래스 파일을 로드해 실행하려면 각 바이트에는 명세가 요구하는 데이터가 들어있어야함
- 다만 자바 가상 머신 명세가 요구사항을 상세하게 정의하지 않아서 가상 머신 제공자가 입맛에 맞게 구현 가능
- 클래스 파일의 상수 풀과 비교해 런타임 상수 풀의 중요한 특징 중 하나는 동적이라는 점임
- 상수가 꼭 컴파일 타임에 생성되어야한다는 규칙이 없음
- 상수 풀의 공간이 부족하면
OutOfMemoryError 가 발생할 수 있음
- 가상 머신 런타임에 속하지 않으며, 자바 가상 머신 명세에 정의된 영역도 아님
- 하지만 자주 쓰이는 메모리이며
OutOfMemoryError의 원인이 될 수도 있음
- JDK 1.4 에서 NIO 도임 됨. NIO 는 힙이 아닌 메모리를 직접 할당 할 수 있으며
DirectByteButffer 객체를 통해 작업을 수행할 수 있음
- 따라서 자바 힙과 네이티브 힙 사이에서 데이터를 복사해 주고 받지 않아도 되서 일부 케이스에서 성능을 크게 개선함
- 물리 메모리 직접할 당하기 때문에 자바 힙 크기 제약과는 무관함
- 하지만 서버 관리자들이
-Xmx 등의 매개 변수 설정할 때 가상 머신 메모리 크기만 고려할 뿐, 다이렉트 메모리 크기는 간과하는 경우가 제법 있음
- 물리 메모리의 한계를 넘어서서 동적 확장을 시도할 때
OutOfMemoryError 발생할 수 음
|
구분 | 힙 메모리 (Heap) | 다이렉트 메모리 (Direct Memory)
메모리 제어 | JVM이 알아서(GC) | 프로그래머가 직접 제어해야 함
목적 | 안전, 편의성 | 고성능, 제어성
위험성 | 낮음 (메모리 누수 가능성 낮음) | 높음 (누수, 시스템 다운 가능)
비유 | 보호장비 입고 달리기 | 보호장비 벗고 스포츠카 몰기
- 가장 보편적인 가상 머신 핫스팟과 보편적인 메모리 영역인 자바 힙을 예로
- 핫스팟이 관리하는 자바 힙에서의 객체 생성, 레이아웃, 접근 방법 등 전체 과정 상세 내용
- 언어 수준에서는 닷누히
new 키워드를 쓰면 끝
new 에 해당하는 바이트 코드를 만나면, 이 명령의 매개 변수가 상수 풀 안의 클래스를 카리키는 심벌 참조인지 확인
- 이 심벌 참조가 뜻하는 클래스가 로딩, 해석, 초기화 되었는지 확인
- 준비가 되지 않은 클래스라면 로딩부터 해야함
- 로딩이 완료된 객체라면 새 객체를 담을 메모리 할당
- 객체에 필요한 메모리를 클래스 로딩하고 나면 완벽하게 알수 있음
- 자바 힙이 완벽히 규칙적이라고 가정하면, 사용 중 메모리는 모두 한쪽에, 여유 메모리는 반대편에 자리하며, 포인터가 두 영역의 경게인 가운데 지점을 가르키게 됨
- 이 상태에서 메모리 할당하면 포인터를 생성되는 객체 크기 많큼 이동 시킴: 포인터 밀치기 (bump the pointer)
- 실제로는 규칙적이지 않음
- 사용 중 메모리와 여유 메모리 뒤섞여 있어 포인터 밀치기 어려움
- 가상 머신은 가용 메모리 블록을 별도로 관리, 신규 객체를 담을 충분한 공간을 찾아 할당 후 목록 갱신
- 이 할당 방식을 여유 목록(free list)라 함
- 사용하는 GC가 컴팩트(모으기)를 할 수 있느냐에 따라 달렸음
- 멀티 스레딩 환경에서는 여유 메모리 시작 포인터 수정하는 일도 스레드 세이프하지 않기 때문에 문제가 생길 수 있음
- 해법1: 메모리 할당을 동기화 함 (갱신을 원자적으로 수행)
- 스레드 각각이 힙 내에 작은 크기의 전용 메모리 미리 할당
- 각 스레드는 로컬 버퍼에서 메모리 할당 받아 쓰다가 부족해지면 동기화를 해서 새로운 버퍼를 할당 받는 식
- 가상 머신이 스레드 로컬 버퍼를 사용할지는
-XX:+/-UseTLAB 매개 변수로 설정
- 해법2: ??
책에서는 아직 안나온거 같긴한데 해법2는 "CAS로 전역 포인터를 원자적으로 수정" 인 것 같음
- CAS 연산으로 경합 없이 경쟁적으로 메모리 할당 포인터를 밀어내는 방법
- 메모리 할당이 끝났으면 가상 머신은 할당 받은 공간을 0으로 초기화함
- 왜?: 자바 코드에서 객체의 인스턴스 필드를 초기화하지 않고도 사용할 수 있는 이유 (??)
자바 언어 규칙에 의해 모든 인스턴스 필드는 명시적 초기화 없이 "기본값(default value)"을 가져야 한다.
- 숫자 타입(int, float 등): 0
- boolean: false
- 참조 타입(Object 등): null
- 다음 단계로 각 각체에 필요한 설정 해줌
- 어느 클래스의 인스턴스 인지, 클래스 메타 정보는 어떻게 찾는지, 객체의 해시코드는 무엇인지 등
- 각 객체 헤더에 저장됨. 뒤에서 자세히 설명
- 자바 가상 머신 관점에서는 여기서 객체가 다 만들어진 셈임
- 하지만 자바 프로그램 관점에서는
<init>() 메서드가 실행되지 않았음
- 일반적으로
new 에 이어 <init>() 까지 실행되어야 개발자의 의도대로 사용가능한 진짜 객체 완성
wip