핫스팟 가상 머신에서의 객체
객체 생성
자바는 객체 지향 프로그래밍 언어로 자바 프로그램이 동작하는 동안 수시로 객체가 만들어짐
언어 수준에서의 객체 생성은 보통 new 키워드를 쓰면 끝나지만 가상 머신 수준에서의 객체 생성 과정
- new 명령에 해당하는 바이트코드를 만나면 이 명령의 매개 변수가 상수 풀 안의 클래스를 가리키는 심벌 참조인지 확인
- 심벌 참조가 뜻하는 클래스가 로딩, 해석(reslove), 초기화(initialize) 되었는지 확인(준비되지 않은 클래스라면 로딩부터 해야 함)
- 로딩이 완료된 클래스라면 새 객체를 담을 메모리 할당
- 객체에 필요한 메모리 크기는 클래스를 로딩하고 나면 완벽하게 알 수 있음
- 객체용 메모리 공간 할당은 자바 힙에서 특정 크기의 메모리 블록을 잘라 주는 일이라 할 수 있음
- 메모리 할당 방식
- 포인터 밀치기(bump the pointer)
- 자바 힙이 완전히 규칙적이라고 가정하면 사용 중인 메모리는 한쪽에 여유 메모리는 반대편에 자리함
- 시리얼과 파뉴처럼 컴팩트(compact: 모으기)가 가능한 컬렉터를 사용하는 시스템
- 포인터가 두 영역의 경계인 가운데 지점을 가리키며 새로운 메모리를 할당하면 포인터를 여유 공간 쪽으로 객체 크기만큼 이동 시킴
- 자바 힙이 완전히 규칙적이라고 가정하면 사용 중인 메모리는 한쪽에 여유 메모리는 반대편에 자리함
- 여유 목록(free list)
- 자바 힙은 규칙적이지 않음(사용 중인 메모리와 여유 메모리가 뒤섞여 있음)
- 이론상의 CMS처럼 스윕(sweep: 쓸기) 알고리즘을 적용한 컬렉터를 쓰는 시스템
- 가용 메모리 블록들을 따로 관리하며 객체 인스턴스를 담기에 충분한 공간을 찾아 할당한 후 목록 갱신
- 자바 힙은 규칙적이지 않음(사용 중인 메모리와 여유 메모리가 뒤섞여 있음)
- 포인터 밀치기(bump the pointer)
- 멀티스레딩 환경
- 여러 스레드가 동시에 객체를 생성하려 하므로 문제 발생
- 메모리 할당을 동기화
- 스레드마다 다른 메모리 공간 할당
- 스레드 각각이 자바 힙 내에 작은 크기의 전용 메모리를 미리 할당받아 놓음(스레드 로컬 할당버퍼 TLAB, -XX:+/-UseTLAB 매개 변수로 설정)
- 로컬 버퍼에서 메모리를 할당받아 사용하다가 버퍼가 부족해지면 동기화를 해 새로운 버퍼를 할당받는 방식
-
더보기자바 코드에서 객체의 인스턴스 필드를 초기화하지 않고도 사용할 수 있는 이유
- 메모리 할당이 끝나면 가상 머신은 할당받은 공간을 0으로 초기화
- 모든 필드가 자연스럽게 각 데이터 타입에 해당하는 0 값을 담고 있게 됨
- 스레드 로컬 할당 버퍼를 사용한다면 초기화는 TLAB 할당 시 미리 수행
- 객체에 필요한 설정을 함
- 어느 클래스의 인스턴스인지, 클래스의 메타 정보는 어떻게 찾는지, 이 객체의 해시코드는 무엇인지(사실 해시코드는 Object::hashCode() 메서드가 처음 호출될 때 계산), GC 세대 나이는 얼마인지 등의 정보가 여기 속함
- 이런 정보가 각 객체의 헤더에 저장됨
- 가상 머신 관점에서 새로운 객체가 다 만들어졌지만 자바 프로그램 관점에서는 이제 시작
- 생성자가 아직 실행되지 않았고, 모든 필드는 기본값인 0인 상태
- 객체로서 구실 하기 위한 여러 자원과 상태 정보 역시 개발자의 의도대로 구성되지 않음
- 일반적으로 new 명령어에 이어서 init() 메서드까지 실행되어 객체를 개발자의 의도대로 초기화해야 비로소 사용 가능한 진짜 객체가 완성됨
-
더보기자바 컴파일러는 자바의 new 키워드를 바이트 코드 명령어인 new와 invokespecial로 변환
- new: 자바 가상 머신 관점에서의 객체 생성 (메모리 할당)
- invokespecial: 자바 프로그램 관점에서의 객체 생성 (생성자 실행)
자바 코드에서 new가 아닌 다른 방식으로 객체를 생성한 경우라면 invokespecial 이 연이어 나오지 않을 수 있음
객체의 메모리 레이아웃
핫스팟 가상 머신은 객체를 객체 헤더, 인스턴스 데이터, 길이 맞추기용 정렬 패딩으로 나눠 힙에 저장
객체 헤더
- 객체 자체의 런타임 데이터
- 크기: 32비트 가상머신에서는 32비트, 64비트 가상머신에서는 64비트
- 구성
- 마크 워드(mark word)
- 해시 코드, GC 세대 나이, 락 상태 플래그, 스레드가 점유하고 있는 락들, 편향된 스레드의 아이디, 편향된 시각의 타임스탬프 등
- 데이터 구조: 작은 공간에 많은 정보를 담고 객체 상태에 따라 공간을 재활용할 수 있게 하기 위해 동적으로 의미가 달라짐
- ex. 32비트 핫스팟 가상머신
- 25비트: 객체 해시코드
- 4비트: 객체의 세대 나이
- 1비트: 0으로 고정
- 2비트: 락 플래그 저장(경량 락, 중량 락, GC 마크, 편향 가능 등의 정보 표현)
- ex. 32비트 핫스팟 가상머신
- 클래스 워드(klass word)
- 마크 워드 다음에 오는 데이터
- 객체의 클래스 관련 메타데이터를 가리키는 클래스 포인터 저장
- 이 포인터를 통해 자바 가상 머신은 특정 객체가 어느 클래스의 인스턴스인지 런타임에 알 수 있음
- 모든 가상 머신 구현이 클래스 포인터를 객체 헤더에 저장하진 않음(객체의 메타데이터 정보를 반드시 객체 자체에서 찾아야 하는 것은 아님)
- 자바 배열의 경우 배열 길이도 객체 헤더에 저장
- 클래스 워드 다음에 저장됨
- 객체 헤더의 메타데이터로부터 자바 객체의 크기를 얻지만 객체 헤더에 저장되는 객체 타입은 배열에 담긴 원소 타입이므로 배열 길이(원소 개수)까지 알아야 배열 객체가 차지하는 메모리 크기를 제대로 계산할 수 있음
- 마크 워드(mark word)
인스턴스 데이터
- 객체가 실제로 담고 있는 정보
- 프로그램 코드에서 정의한 다양한 타입의 필드 관련 내용, 부모 클래스 유무, 부모 클래스에서 정의한 모든 필드
- 저장 순서는 가상 머신의 할당 전략 매개 변수와 자바 소스 코드에서 필드를 정의한 순서에 따라 달라짐
- 핫스팟 가상 머신은 기본적으로 long, double > int > short, char > byte, boolean > 일반 객체 포인터 순으로 할당됨
- 필드 길이가 같다면 부모 클래스에서 정의된 필드가 자식 클래스의 필드보다 앞에 배치됨
- +XX:CompactFields 매개변수를 true 로 설정하면 하위 클래스의 필드 중 길이가 짧은 것들은 상위 클래스의 변수 사이사이에 끼워 넣어져서 공간이 조금이나마 절약됨
정렬 패딩
- 존재하지 않을 수도 있으며 특별한 의미 없이 자리를 확보하는 역할
- 핫스팟 가상 머신의 자동 메모리 관리 시스템에서 객체의 시작 주소는 반드시 8바이트의 정수배여야 하므로 객체 헤더는 정확히 8바이트의 정수배가 되도록 설계되어 있음
- 인스턴스 데이터가 조건을 충족하지 못하는 경우에만 패딩으로 채움
객체에 접근
스택에 있는 참조 데이터를 통해 힙에 들어있는 객체들에 접근해 이를 조작
힙에서 객체의 정확한 위치를 알아내 접근하는 구체적인 방법을 규정하지 않아 가상 머신에서 구현하기 나름임
주로 핸들이나 다이렉트 포인터를 사용해 구현
- 핸들
- 자바 힙에 핸들 저장용 풀이 별도로 존재
- 참조에는 객체의 핸들 주소가 저장, 핸들에는 해당 객체의 인스턴스 데이터, 타입 데이터, 구조 등의 정확한 주소 정보가 담김
- 장점
- 참조에 안정적인 핸들의 주소가 저장됨
- 가비지 컬렉션 과정에서 객체가 이동하는 일이 흔한데 핸들을 이용하면 객체의 위치가 바뀌는 상황에서도 참조 자체는 손댈 필요가 없으며 핸들 내의 인스턴스 데이터 포인터만 변경하면 됨
- 다이렉트 포인터
- 자바 힙에 위치한 객체에서 인스턴스 데이터뿐 아니라 타입 데이터에 접근하는 길도 제공
- 스택의 참조에는 객체의 실제 주소가 바로 저장
- 장점
- 핸들을 경유하는 오버헤드가 없기 때문에 빠름
- 자바는 다른 객체에 접근하는 일이 많기 때문에 실행 시간에 영향을 크게 줄 수 있음
OutOfMemoryError
프로그램 카운터 외에도 가상 머신 메모리의 여러 런타임 영역에서 발생할 수 있음
- 각 런타임 영역에 저장되는 내용 검증
- 실제 메모리 오버플로가 일어나는 과정 경험
OpenJDK 17의 핫스팟 가상 머신에서의 예제
자바 힙 오버플로
실제 자바 애플리케이션에서 메모리 오버플로가 가장 많이 발생하는 영역
해결 방법
- 메모리 이미지 분석 도구로 힙 덤프 스냅숏 분석
- 오버플로를 일으킨 객체가 꼭 필요한 객체인지 확인
- 필요 없는 객체에 의한 메모리 누수라면,
메모리 누수라면 누수된 객체의 타입 정보와 GC 루트까지의 참조 사슬 정보를 보면 메모리 누수를 일으키는 코드의 정확한 위치를 찾을 수 있음 - 오버 플로라면,
자바 가상 머신의 힙 매개 변수 설정과 컴퓨터의 가용 메모리를 비교하여 가상 머신에 메모리를 더 많이 할당할 수 있는지 확인
프로그램이 런타임에 소비하는 메모리를 최소로 낮춤
- 코드에서 수명 주기가 너무 길거나 상태를 너무 오래 유지하는 객체는 없는지 확인
- 공간 낭비가 심한 데이터 구조를 쓰고 있는지 확인
- 필요 없는 객체에 의한 메모리 누수라면,
가상 머신 스택과 네이티브 메서드 스택 오버 플로
핫스팟 가상 머신은 가상 머신 스택과 네이티브 메서드 스택을 구분하지 않음
스택을 동적으로 확장할 수 있는 여지를 줬지만 핫스팟 가상 머신은 확장을 지원하지 않음
- OutOfMemoryError: 스레드를 생성할 때 메모리 부족할 때 발생 (스레드 실행 중에는 발생하지 않음)
- StackOverflowError: 스택 용량이 부족하여 새로운 스택 프레임을 담을 수 없을 때 발생
단일 스레드로 제한하지 않고 스레드를 계속 만들어내면 핫스팟에서도 메모리 오버플로를 일으킬 수 있지만 스택 공간이 충분한지와는 관련 없고 운영 체제 자체의 메모리 사용 상태에 영향을 받음 (스레드 별 스택을 크게 잡을수록 메모리 오버플로를 쉽게 일으킬 수 있음)
- 운영 체제가 각 프로세스에 할당하는 메모리 크기 제한적
- 다이렉터 메모리와 자바 가상 머신 프로세스가 자체적으로 소비하는 메모리를 제외한 나머지가 가상 머신 스택과 네이티브 메서드 스택에 할당
- 각 스레드에 스택 메모리를 많이 할당하면 생성할 수 있는 스레드 수가 작아짐
- 새로운 스레드를 생성하려 할 때 메모리가 고가될 가능성이 커짐
- 많은 스레드를 만들어 메모리 오버플로가 일어나는 경우,
프로그램에서 사용하는 스레드 수도 줄일 수 없고 64비트 가상 머신도 사용할 수 없을 때 최대 힙 크기와 스택 용량을 줄이면 스레드를 더 많이 만들 수 있음
메서드 영역과 런타임 상수 풀 오버플로
런타임 상수 풀은 메서드 영역에 속함
핫스팟은 영구 세대를 점진적으로 없애기 시작하여 메타 스페이스로 완전히 대체
- JDK 7 이전
- 메서드 영역 제한을 통해 영구 세대의 크기를 조절하며 테스트
- 런타임 상수 풀이 오버플로 되면 OutOfMemoryError: PermGen space 에러 문구로 메서드 영역의 한 부분임을 알 수 있음
- String::intern()
- 처음 만나는 문자열 인스턴스를 영구 세대의 문자열 상수 풀에 복사
- 영구 세대에 저장한 문자열 인스턴스의 참조 반환
- StringBuilder로 생성한 문자열 객체는 자바 힙에 존재하므로 같은 참조가 될 수 없음
- JDK 7
- 영구 세대에 젖아했던 문자열 상수 풀을 자바 힙으로 옮겼기 때문에 메서드 영역 제한 옵션이 의미 없어져 예외를 던지지 않고 무한 루프를 돌며 멈추지 않음
- 최대 힙 크기를 제한하면 오버플로가 객체 할당 시에 일어나는가 아닌가에 따라 다른 결과가 나옴
- String::intern()
- 문자열 상수 풀 위치가 자바 힙이므로 첫 번째 인스턴스의 참조로 바꿔줌
- StringBuilder 가 생성한 문자열 인스턴스와 같음
- JDK 8
- 메타스페이스 이용
- 기본 설정으로 실행할 경우 일반적인 동적 생성 시나리오로는 메서드 영역에서 오버플로를 일으키기 어려움
- 메타스페이스 보호용 매개 변수 제공
- -XX:MaxMetaspaceSize: 메타스페이스 최대 크기. 기본 -1 (제한 없음, 네이티브 메모리 크기가 허용하는 만큼)
- -XX:MetaspaceSize: 메타스페이스 초기 크기(바이트 단위). 이 크기가 가득 차면 가비지 컬렉터가 클래스 언로딩 시도 후 크기 조정. MaxMetaspaceSize를 설정했다면 그 값을 초과할 수 없음
- -XX:MinMetaspaceFreeRatio: 가비지 컬렉션 후 가장 작은 메타스페이스 여유 공간의 비율. 공간이 부족해 발생하는 가비지 컬렉션의 빈도를 줄일 수 있음
네이티브 다이렉트 메모리 오버플로
용량은 -XX:MaxDirectMemorySize로 설정. 따로 설정하지 않았으면 자바 힙의 최댓값과 동일
다이렉트 메모리에서 발생한 메모리 오버플로는 힙 덤프 파일에서 이상한 점을 찾을 수 없음
메모리 오버플로로 생성된 덤프 파일이 매우 작고 프로그램에서 DirectMemory를 직접 혹은 간접적으로(보통 NIO를 통해) 사용했다면 다이렉트 메모리에서 원인을 찾는데 집중해야 함
'📙 > Study' 카테고리의 다른 글
[JVM 파헤치기] 02. 자바 메모리 영역과 메모리 오버플로(1) (0) | 2025.03.23 |
---|---|
[JVM 파헤치기] 01.자바 기술 시스템 소개(2) (0) | 2025.03.18 |
[JVM 파헤치기] 01. 자바 기술 시스템 소개(1) (1) | 2025.03.17 |
[만들면서 배우는 클린 아키텍처] 12. 아키텍처 스타일 결정하기 (1) | 2024.10.20 |
[만들면서 배우는 클린 아키텍처] 11. 의식적으로 지름길 사용하기 (1) | 2024.10.19 |