JVM과 메모리 구조
JVM
JVM(Java Virtual Machine)는 자바 가상 머신
- 논리적인 개념, 여러 모듈의 결합체
- Java 앱을 실행하는 주체
- JVM 때문에 다양한 플랫폼 위에서 동작 가능
- 대표적인 역할, 기능
- 클래스 로딩
- GC 등 메모리 관리
- 스레드 관리
- 예외 처리
JVM Architecture
- Class Loaders
- Runtime Data Areas
- Execution Engine
- 메모리 영역에 있는 데이터를 가져와 해당하는 작업 수행
- JNI (Java Native Interface)
- JVM과 네이티브 라이브러리 간 이진 호환성을 위한 인터페이스
- 네이티브 메서드(네이티브 언어 C/C++ 등으로 작성) 호출, 데이
터 전달과 메모리 관리 등 수행
- Native Libraries
- 네이티브 메서드의 구현체를 포함한 플랫폼별 라이브러리
JVM Run-Time Data Areas
- The pc Register
- 스레드 별로 생성되며 실행 중인 명령(오프셋)을 저장하는 영역
- Java Virtual Machine Stacks (Stack Area, Java Stack)
- 스레드 별로 생성되며 메서드 실행 관련 정보를 저장하는 영역 (프레임 저장)
- Heap
- JVM 실행 시 생성되며 모든 객체 인스턴스/배열에 대한 메모리가 할당되는 영역
- Method Area
- JVM 실행 시 생성되며 클래스의 구조나 정보를 저장하는 영역
- Native Method Stacks
- 스레드 별로 생성되며 네이티브 코드 실행에 관련 정보를 저장하는 영역
JVM Run-Time Data Areas - The pc Register
- 스레드 생성 시 생성/할당되며 현재 실행 중인 명령의 주소를 저장하는 영역
- 레지스터는 프로세서 내에서 자료를 보관하는 빠른 기억 장치
- 저장되는 명령의 주소는 Java 바이트 스트림(바이트코드) 안에 오프셋을 의미
- 바이트코드 명령 자체(opcode)는 메서드 영역에 저장
- 각 스레드가 메서드를 실행할 때 실행 메서드에 따라 저장 여부가 결정됨
- Java 메서드 -> 실행 명령의 주소를 저장 네이티브 메서드-> 저장되지 않음
JVM Run-Time Data Areas - Java Virtual Machine Stacks
- 스레드 생성 시 생성/할당되며 프레임(Frame)이 저장되는 영역 (Stack Area, Java Stack)
- JVM의 구현 방식에 따라 크기와 프레임 관리 방법 등이 다를 수 있음
- 로컬 변수 저장과 메서드 호출/반환 등과 같은 작업 시 사용
- 스택은 push/pop을 제외하고 직접 조작되지 않기 때문에 프레임은 힙에 할당될 수 있음
- 허용된 것 보다 더 큰 스택이 필요한 경우 StackOverFlowError 발생
- 스택을 생성하거나 동적으로 크기를 확장할 때 메모리가 부족하면 OutOfMemoryError 발생
Frame
- JVM stack에 생성(push)되는 메서드 관련 정보 저장 단위 (Stack Frame, Activation Record)
메서드가 호출될 때 생성(push)되며 종료되면 소멸(pop)됨
- 메서드의 데이터(매개 변수와 지역 변수 등)와 부분 결과 등을 저장함
동적 연결, 메서드 값 반환, 예외 전달 등에 사용됨
- 각 프레임에는 런타임에 필요한 메서드 참조를 위해 런타임 상수 풀 참조를 포함
구성
- 지역변수 배열 (Local Variables) 매개 변수와 지역 변수 저장
- 오퍼랜드 스택 (Operand Stacks) 실행 중간 연산 결과 등을 임시로 저장
- 프레임 데이터 (Frame Data) 반환 주소 등 기타 데이터 저장
Frame - Local Variables
- 이 지역 변수 배열의 길이는 컴파일 타임에 결정되며 관련 메서드, 클래스 등의 정보와 함께 바이트코드로 제공됨
- 지역 변수는 원시 타입과 reference, returnAddress이 한 곳(slot)에 저장되며 특히 8바이트인 long, double 타입은 연속된 두 곳(slot)에 저장됨
- 지역 변수 배열의 첫번째 인덱스는 0이며 0부터
array.length-1
까지 유효한 인덱스 범위로 간주
- JVM에 의해 메서드 호출 시 매개 변수(또는 Args)는 지역 변수 배열에 담겨 전달
- 인스턴스 메서드 호출 시에 지역 변수 0은 인스턴스 메서드 객체 참조인
this
를 전달하는데 사용 그 이후 1부터 모든 매개변수들이 표현됨
Frame - Operand Stacks
- 일반적인 LIFO 방식의 스택으로 최대 길이는 컴파일 타임에 결정되며 관련된 메서드 코드와 함께 제공되며 최초 생성 시 오퍼랜드 스택은 비어있음
- 문맥에 따라 다를 수 있지만 일반적으로 오퍼랜드 스택은 프레임의 오퍼랜드 스택 지칭
- 지역 변수 배열과 다르게 인덱스가 아닌 push/pop을 할 수 있는 명령어에 의해 액세스 가능
- 저장되는 오퍼랜드의 타입은 JVM에서 제공하는 모든 타입의 값
- JVM은 지역 변수, 필드의 값을 오퍼랜드 스택으로 로딩하는 명령 제공하며
다른 명령들을 통해 오퍼랜드 스택에서 값을 가져와 연산, 결과를 다시 저장함
- 오퍼랜드 스택은 호출할 메서드에 전달할 매개 변수 전달과 결과를 수신할 때도 사용됨
- 소수의 JVM 명령은 타입에 관계 없이 원시 타입의 값으로 런타임 데이터 영역에서 작동함
Frame - Frame Data
- 연관된 메서드들의 심볼릭 레퍼런스와 메서드 반환에 필요한 데이터 저장
- 예외가 발생한 경우 catch 블록 정보를 제공하는 Exception 테이블 참조 포함
JVM 메모리 구조
JVM Run-Time Data Areas - Method Area
- JVM 실행 시 생성되어 모든 스레드에게 공유되며 클래스 별 구조와 정보를 저장하는 영역
- 클래스/인터페이스/인스턴스 초기화에 사용되는 스페셜 메서드
- 런타임 상수 풀, 필드/메서드 데이터
- 생성자/메서드 코드
- Hotspot VM 기준으로 Metaspace(PermGen) 영역에 관리됨 (JVM 구현마다 다름)
- 컴파일된 코드를 저장하는 영역 또는 OS 프로세스의 text 세그먼트와 유사
- 메서드 영역의 크기는 상황에 따라 고정/확장/축소될 수 있음
- 메모리 할당 요청을 충족할 수 없으면 OutOfMemoryError 발생
- 메서드 영역은 논리적으로 힙의 일부, 간단한 구현의 경우 GC/압축/컴팩트를 선택하지 않을 수 있음
- 메서드 영역의 위치, 컴파일된 코드 관리에 대한 정책을 요구하지 않음
JVM Run-Time Data Areas - Heap
- 모든 객체 인스턴스, 배열에 대한 메모리가 할당되는 데이터 영역
- JVM 실행 시 생성되며 모든 JVM 스레드에게 공유되는 영역
- GC가 처리되는 영역
- 특정 스토리지 시스템에 종속적이지 않은 구조
- 계산된 것보다 더 많은 힙 메모리가 필요한 경우 OutOfMemoryError 발생
- 힙의 크기는 상황에 따라 고정/확장/축소될 수 있음
Run-Time Constant Pool
- 클래스/인터페이스가 로딩될 때 메서드 영역(Method Area)에 할당되는 자료구조
- 컴파일 시
.class
파일에 생성되는 일반 상수 풀의 런타임 표현
- 일반 상수 풀의 데이터를 기반으로 생성되며 스태틱 상수와 심볼릭 레퍼런스 등을 포함
- 상수 뿐 아니라 메서드, 필드 참조까지 여러 종류의 상수가 포함됨
- 일반 프로그래밍 언어의 심볼 테이블과 유사하지만 그보다 더 넓은 범위에 데이터를 포함함
- 심볼 테이블은 컴파일러/인터프리터가 프로그램을 분석/처리 시 사용하는 자료구조이며 코드의 식별자/상수/프로시저/함수 등과 관련된 정보를 저장함
- Method Area 영역에서 허용 가능한 메모리를 초과하면 OutOfMemoryError 발생
Constant Pool
- 상수 풀 (Constant Pool)
- Java 바이트코드에 포함되어 있는 모든 상수 값을 저장하는 심볼(룩업) 테이블
.java 파일이 Java 컴파일러에 의해 컴파일 되어 Java 바이트코드로 변환될 때 생성
- 클래스명, 필드명, String/Primitive type 리터럴, 심볼릭 레퍼런스 등이 저장됨
- 컴파일 타임 시점에 알 수 있는 정보들이 저장됨
- 상수 풀에서 런타임 상수 풀에 새로 생성(이동)되는 데이터
- 심볼릭 레퍼런스나 String.intern 등 런타임에 달라질 수 있는 데이터
- 컴파일 타임에 확정되어 런타임에 변경되지 않는 데이터(리터럴 값 등)들을 제외한 데이터
Symbolic Reference
- Java 바이트코드에서 클래스/인터페이스/필드 등 참조하는 다른 요소를 표현 방식
- JVM 구현에 따라 심볼릭 레퍼런스가 Lazy가 아닌 Eager로 확인/연결될 수 있음
- 이때 발생하는 오류는 직간접적으로 참조(사용)하는 지점에서 발생해야 함
- 클래스가 로딩 후 링킹(Resolution)되는 시점에서 심볼릭 레퍼런스가 실제 주소값으로 대체 됨
- 일반적인 예시 (간접적인 표현)
java.lang.String
클래스의 private final byte[] value
필드
Ljava/lang/String;.value:[C
L
-> 참조 타입(클래스/인터페이스)
;
-> 클래스와 필드의 구분자 (일반적으로 생략함)
.value
-> 필드명
:
-> 참조
[
-> 배열 표현 (2차원이라면 [[
)
C
-> char
타입
java.lang.String
클래스의 public char charAt(int index)
메서드
Ljava/lang/String;.charAt:(I)C
(I)
-> 메서드의 파라미터 타입
C
-> 메서드의 반환 타입
JVM Run-Time Data Areas - Native Method Stacks
- 스레드 생성 시 생성/할당되며 네이티브 메서드 데이터가 저장되는 영역
- 네이티브 메서드는 Java 외에 언어로 작성된 메서드 (일반적으로 C/C++)
- 네이티브 로딩 상태나 사용 여부에 따라 JVM이 해당 영역을 제공하지 않을 수 있음
- C와 같은 언어로 구성된 JVM 명령셋을 위한 인터프리터 구현에 사용될 수 있음
- 네이티브 메서드 스택의 크기는 상황에 따라 고정되거나 확장, 축소될 수 있음
- 네이티브 메서드 스택 크기가 고정되어 있다면 생성될 때 크기를 독립적으로 선택할 수 있음
- 허용된 것 보다 더 큰 스택이 필요한 경우 StackOverFlowError 발생
- 동적 확장 시도 중에 사용 가능한 메모리가 부족하면 OutOfMemoryError 발생
JVM Run-Time Data Areas - 생성 시점 정리
- JVM 실행 시
- 스레드 실행 시
- pc register
- Java Virtual Machine Stacks
- Native Method Stacks (필요한 경우)
- 클래스/인터페이스 생성 시
- Run-Time constant Pool (Method Area에 저장)
- 메서드 호출 시
- Frame (Java Virtual Machine Stacks에 저장)
Special Methods
- Instance Initialization Methods (인스턴스 초기화 메서드)
- Constructor, Instance Initializer Blocks (바이트 수준에선 동일)
- 조건
- 클래스 안에 정의
- 이라는 이름을 가진 스페셜 메서드
- 반환 타입은 void
- Class Initialization Methods (클래스 초기화 메서드)
- static Initializer Blocks
- 조건
- 이라는 이름을 가진 스페셜 메서드
- 반환 타입은 void
- 클래스 파일의 버전이 51.0 이상인 경우 해당 인자를 사용하지 않고 ACC_STATIC 플래그가 설정된 메서드
- Signature Polymorphic Methods
- 런타임에 메서드 시그니처가 결정되는 메서드
- 대표적인 예시로 JDK 7부터 도입된
java.lang.invoke.MethodHandle
클래스의 invoke
, invokeExact
메서드
- 조건
java.lang.invoke.MethodHandle
또는 java.lang.invoke.VarHandle
클래스에서 선언
- Object[] 타입의 단일 공식 파라미터
ACC_VARARGS
, ACC_NATIVE
플래그 설정
JVM Run-Time Data Areas
Memory Type
- Heap memory
- JVM 힙 영역, 모든 객체를 저장 메모리 설정 가능(
java -Xms4096M -Xmx6144M com.example.Class
) 부족한 경우 OutOfMemoryError 발생
- Stack memory
- JVM stacks(지역 변수나 메서드 정보 등) 데이터 저장 (GC 영역 아님) JVM이 관리하는 영역이며 JVM에 의해 고정된 크기를 가짐 부족한 경우 StackOverflowError 발생
- Native memory
- 힙 외부에 할당되는 영역으로 오프힙(off-heap) 메모리라고도 표현함 (GC 영역 아님) 외부 영역에 존재하기 때문에 데이터 입출력 시 직렬화 수행이 필요하고 성능은 버퍼, 직렬화 프로세스, 디스크 공간 등 환경에 따라 달라짐 JVM stacks, 내부 자료 구조, 메모리 매핑 파일 등 저장
- Direct memory (Direct Buffer Memory)
- 힙 외부에 할당되지만 JVM 프로세스에 의해 사용되는 네이티브 메모리 영역 (GC 영역 아님)
대표적으로 Java NIO에서 사용되는 영역(JNI 등)
-XX:MaxDirectMemorySize=1024M
처럼 메모리 설정 가능
Stack Memory & Heap Memory
class Person {
int id;
String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
}
public class PersonBuilder {
private static Person buildPerson(int id, String name) {
return new Person(id, name);
}
public static void main(String[] args) {
int id = 23;
String name = "John";
Person person = null;
person = buildPerson(id, name);
}
}
- main 메서드 호출 시 이를 위한 스택 메모리가 할당되며 프레임(main)이 생성
• id 변수의 int 값은 직접 저장됨
• name 참조 변수는 힙의 스트링 풀을 참조
• person 변수 생성 후 null 참조
- main 메서드가 buildPerson 메서드를 호출하면서 프레임(buildPerson)이 생성됨
• 파라미터로 넘어온 id의 값과 name의 참조가 저장됨
- 연이어 Person 클래스의 인스턴스 생성을 위해 생성자가 호출되며 프레임(Person)이 생성됨
• 파라미터로 넘어온 id의 값과 name의 참조 그리고 자신을 가리키는 this
가 저장됨
• 생성되는 Person 객체는 Heap에 저장
- Person 인스턴스가 생성된 후 Person 프레임이 소멸되며 Person 참조 주소를 반환
- 마찬가지로 buildPerson 프레임이 소멸되며 Person 참조 주소를 반환
- main 메서드의 person 변수에 Person 참조 주소가 바인딩되고 main 프레임이 소멸되며 메서드 종료
Stack Memory & Heap Memory
- Stack Memory (스레드 실행과
static memory
할당을 위해 사용)
- 메서드 실행/완료에 따라 메모리 크기가 변경됨
- 스택 내부 지역 변수는 메서드가 실행 동안에만 존재
- 이 영역이 가득 차면 StackOverFlowError 발생
- 힙 메모리 보다 상대적으로 빠름
- 스레드 별로 할당 받기 때문에 스레드 세이프함
- Heap (객체 생성과
dynamic memory
할당을 위해 사용)
- Young Generation, Old Generation 같은 메모리 엑세스 기술이 활용됨
- 이 영역이 가득차면 java.lang.OutOfMemoryError 발생
- 스택 메모리보다 상대적으로 느림
- 스택 메모리와 다르게 해당 영역은 GC에 의해 메모리가 비워짐
- JVM 앱 전체에서 공유되는 영역이기 때문에 스레드 세이프하지 않음 (별도의 동기화 처리 필요)
Stack Memory & Heap Memory
- Applicaiton
- 스택은 스레드에 의해 한 번에 하나씩 부분적으로 사용됨
- 힙은 앱 전체에서 런타임 동안 사용됨
- Size
- 스택 크기는 OS에 의존적이며 보통 힙보다 작음
- 힙 크기는 제한 없음
- Storage
- 스택은 힙에서 생성된 객체의 변수와 참조만 저장
- 힙은 새로 생성된 모든 객체를 저장
- Order
- 스택은 LIFO(Last-in First-out) 기준으로 사용/액세스
- 힙은 Old/Young Generation 등 다소 복잡한 관리 방법을 통해 사용/액세스
- Life
- 스택은 현재 메서드가 실행되는 동안만 존재
- 힙은 앱이 실행되는 동안 존재
- Efficiency
- 스택은 힙에 비해서 할당 속도가 매우 빠름
- 힙은 스택보다 할당 속도가 느림
- Allocation/Deallocation
- 스택은 메서드 호출 시 자동으로 할당되며 반환될 때 해제됨
- 힙은 새 객체가 생성되면 할당되며 더이상 참조(사용)되지 않을 때 GC에 의해 해제됨
Native Memory
- 네이티브 메모리는 OS 레벨에서 직접 관리되는 메모리 영역
JVM 메모리 외에 추가 할당이 가능하며 이 경우 JVM의 최대 메모리 설정보다 더 많은 메모리를 사용하게 됨
- 네이티브 메모리 영역
- Metaspace (Permanent Generation)
로딩된 클래스의 메타데이터를 보관하는 영역 (힙 외부의 별도 영역)
따라서 힙이 아닌 메타스페이스 메모리 설정 필요(-XX:MetaspaceSize
, -XX:MaxMetaspaceSize
)
- Threads
스레드가 사용하는 JVM stack, 일반적으로 약 1MB(-Xss
튜닝 플래그로 설정 가능)
다른 영역과 달리 스레드 수 제한이 없다면 실질적으로 스택에 할당된 메모리는 제한이 없음
작업을 수행하는 스레드 외에 GC처럼 별도의 내부작업을 위한 스레드도 필요
- Code Cache
JIT 컴파일러에 의해 생성된 네이티브 코드를 저장하는 영역(non-heap 영역)
메모리 설정 가능(-XX:InitialCodeCacheSize
, -XX:ReservedCodeCacheSize
)
- GC (Garbage Collection)
GC 알고리즘과 함께 제공되며, 이 작업을 위해 일부 오프-힙 데이터 구조가 필요 (더 많은 네이티브 메모리를 필요로 함)
- Symbols
대표적으로 String 상수 풀(String 인터닝을 통해 비효율적인 메모리 사용을 줄임)이 있고 이를 네이티브 고정된 크기의 해시테이블에 저장
메모리 설정 가능(-XX:StringTableSize
), 런타임 상수 풀 또한 마찬가지로 여기에 해당함
- Native Byte Buffers
개발자가 직접 접근할 수 있는 네이티브 메모리 영역
JNI 및 NIO 등에서 ByteBuffers를 통한 malloc 호출로 사용 가능
출처
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
https://www.geeksforgeeks.org/java-virtual-machine-jvm-stack-area/
https://www.baeldung.com/java-stack-heap
https://www.baeldung.com/java-stack-heap