2.1 들어가며
메모리 관리 측면에서 C, C++ 개발자 vs 자바 개발자
- C, C++ : 객체 각각의 소유권뿐 아니라 객체의 탄생부터 죽음까지 관리할 책임, 자유도 높은만큼 관리 포인트 많음
- Java : 가상 머신이 자동 메모리 관리 제공, 통제권을 위임했기 때문에 가상 머신의 메모리 관리 방식을 이해하지 못하면 문제 상황을 해결하기 어려움
2.2 런타임 데이터 영역
JVM은 메모리를 몇 개의 데이터 영역으로 나눠서 관리한다.
런타임 데이터 영역 구분
- 모든 스레드가 공유 : 메서드 영역(런타임 상수 풀), 힙
- 스레드별 데이터 영역(스레드 프라이빗) : 가상 머신 스택, 네이티브 메서드 스택, 프로그램 카운터 레지스터
2.2.1 프로그램 카운터 레지스터
'바이트 코드 줄번호 표시기' : 현재 실행중인 스레드가 실행할 명령의 주소를 저장하는 작은 메모리 영역
PC 레지스터가 필요한 이유
- JVM 멀티스레딩 : CPU 코어를 여러 스레드가 교대로 사용하기 때문에 특정 시각에 각 코어는 한 스레드의 명령만 실행
- 스레드 실행, 중단이 반복되기 때문에 스레드 전환 후 이전에 실행하다 멈춘 지점을 복원하기 위해서 스레드 각각 pc가 필요
- 각 스레드의 카운터는 서로 영향을 주지 않는 독립된 영역에 저장
PC 레지스터에 기록되는 주소
- 스레드가 자바 메서드를 실행중이면 바이트 코드 명령어 주소
- 네이티브 메서드를 실행중이면 undefined
2.2.2 자바 가상 머신 스택
스레드 프라이빗 영역으로 연결된 스레드와 생성/ 삭제가 함께 된다.
JVM이 스택을 사용하는 과정
- 메서드 호출
- JVM이 스택 프레임(데이터 구조) 생성
- 지역 변수 테이블, 피 연산자 스택, 동적 링크, 메서드 반환값 등 저장
- 스택 프레임을 가상 머신 스택에 push, pop을 반복하며 메서드를 실행, 종료
스택의 중요성
- 자바 메모리 영역을 heap, stack으로 구분하는 것은 C, C++ 메모리 구조에서 기인한 것
- 자바 메모리 영역은 훨씬 복잡하지만 그럼에도 heap, stack을 가장 신경 써야 한다.
지역 변수 테이블
- '스택'은 보통 JVM 스택을 가리킴. 그 중에서도 '지역 변수 테이블'을 가리키는 경우가 많음
- JVM이 컴파일 타임에 알 수 있는 기본 데이터 타입, 객체 참조, 반환 주소 타입 등 저장
- 지역 변수 테이블을 구성하는데 필요한 데이터 공간(변수의 슬롯 개수)은 컴파일 과정에서 할당됨
stack에서 발생할 수 있는 오류
- StackOverflowError
- OutOfMemoryError
2.2.3 Native Method Stack
가상 머신 스택과 비슷한 역할
가상 머신 스택과 비교
- 가상 머신 스택 : 자바 메서드 실행
- Native Method Stack : Native Method 실행
2.2.4 Java Heap
- 거의 모든 객체 인스턴스가 저장되기 때문에 모든 스레드가 공유하는 가장 큰 메모리 영역
- JVM 구동시 생성
- GC가 관리하는 영역이라 GC Heap 이라고도 한다.
- Java Heap은 메모리 회수와 할당을 더 빠르게 하기 위해 스레드 로컬 할당 버퍼 여러 개로 나눈다.
2.2.5 메서드 영역
- 모든 스레드가 공유
- 가상 머신이 읽어들인 타입 정보, 상수, 정적 변수, JIT 컴파일러가 컴파일한 코드 캐시 등을 저장
- Java heap과 구분하기 위해 non-heap으로 부르기도 함
- 회수할 대상이 상수 풀과 타입이 대부분이라 회수 효과가 상대적으로 작다.
2.2.6 런타임 상수 풀
- 메서드 영역의 일부
- 클래스 버전, 필드 메서드, 인터페이스 등 클래스 파일에 포함된 설명 정보, 컴파일 타임에 생성된 다양한 리터럴과 심벌 참조가 저장
- 가상 머신이 클래스를 로드할 때 해당 정보를 저장
2.2.7 다이렉트 메모리
- 가상 머신 런타임에 속하지 않음
- 네이티브 메서드를 통해 heap이 아닌 메모리에 직접 할당
- OutOfMemoryError 원인이 될 수 있다.
2.3 핫스팟 가상 머신에서의 객체 들여다보기
- 가장 보편적인 가상 머신인 핫스팟과 자바 힙을 예로 설명
2.3.1 객체 생성
가상 머신 수준에서 객체가 생성되는 과정
- 자바 가상 머신이 new에 해당하는 바이트 코드를 읽음
- 이 명령의 매개 변수가 상수 풀 안의 클래스를 가리키는 심벌 참조인지 확인
- 이 심벌 참조가 뜻하는 클래스가 로딩, 해석, 초기화되었는지 확인
- 로딩이 완료된 클래스라면 새 객체를 담을 메모리를 할당
2.3.2 객체의 메모리 레이아웃
핫스팟 JVM에서는 객체를 세 부분으로 나눠 힙에 저장
객체 헤더
- 마크워드 : 객체 자체의 런타임 데이터(해시코드, GC세대 나이, 락 정보 등)
- 클래스 워드: 객체의 클래스 관련 메타데이터를 가리키는 클래스 포인터
- 배열인 경우 배열 길이
인스턴스 데이터
- 객체가 실제로 담고 있는 정보 : 필드 관련 내용, 부모 클래스 유무, 부모 클래스에서 정의한 필드 등
정렬 패딩
- 존재하지 않는 경우도 있고 자리 확보를 위한 역할
- 객체 시작 주소는 반드시 8바이트의 정수배여야 하기 때문에
2.3.3 객체에 접근하기
- 대다수 객체는 다른 객체 여러 개를 참조하여 만들어진다.
- 스택에 참조 데이터를 통해 힙에 들어 있는 객체들에 접근해 이를 조작한다.
- JVM마다 객체에 접근하는 방식이 다르며, 주로 핸들이나 다이렉트 포인터를 사용
핸들 방식
- 자바 힙에 핸들 저장용 풀이 별도로 존재
- 참조에는 객체의 핸들 주소가 저장되고 핸들에는 다시 해당 객체의 인스턴스 데이터, 타입 데이터, 구조 등의 정확한 주소 정보가 담김
- 장점: 안정적인 핸들의 주소가 저장된다. GC 등 객체의 위치가 바뀌는 상황에서도 참조 자체는 손댈 필요 없음
다이렉트 포인터 방식
- 참조에 객체의 실제 메모리 주소가 저장
- 객체 접근이 빠르지만, 객체 이동 시 모든 참조를 수정해야 한다.
2.4 실전: OutOfMemoryError 예외
2.4.1 자바 힙 오버플로
자바 힙은 객체 인스턴스를 저장하는 공간이라, 객체를 계속 생성하고 접근 경로가 살아 있다면 언젠가는 힙의 최대 용량을 넘게 된다.
실제 자바 애플리케이션에서 OutOfMemoryError가 가장 많이 발생하는 영역이 자바 힙이다.
해결 방법
1. 힙 덤프 스냅샷 분석
2. 메모리 누수 확인 : 오버플로 일으킨 객체가 불필요한 객체인 경우 메모리 누수
3. 메모리 누수 해결 : 누수된 객체부터 GC루트까지의 참조 사슬 확인으로 코드 위치 파악
4. 오버플로 해결
- 가상 머신 메모리 확인 및 추가 할당 : JVM의 힙 매개 변수 설정(-Xmx와 -Xms)과 컴퓨터의 가용 메모리 비교
- 코드 최적화 : 객체 수명 주기 검토, 비효율적인 데이터 구조 등
'Java' 카테고리의 다른 글
[JVM 밑바닥까지 파헤치기] 3장 가비지 컬렉터와 메모리 할당 전략 (2) | 2024.12.16 |
---|---|
정적 멤버와 static, singleton 패턴 (0) | 2018.12.09 |