JVM의 메모리 구조 및 할당과정
JVM 구조
JVM은 다섯 가지 컴포넌트로 구성되어 있다.
- 클래스 로더 시스템 : 컴파일 결과로 만들어진 .class 바이트코드 파일을 읽어들여 메모리에 배치.
- 로딩, 링크, 초기화 세 가지 과정을 거친다.
- 메모리
- Runtime Engine: 바이트코드를 읽어들이는 인터프리터가 작동하는 영역.
- 바이트 코드를 기계어로 변환하면서 Line by Line 실행하는 방식.
- 여기 인터프리터가 기계어 코드를 실행할 때, 한 번 변환한 바이트코드를 또 변환하는 대신
실행한 코드를 저장하는 영역이 있다. 그게 Code Cache (JIT Compiler라고도 부른다). 프로그램 실행속도를 향상시키는 용도. - Garbage collection.
- Native Method interface
- Native Method library
참고자료
JVM 메모리 구조
JVM의 메모리 구조는 크게 Heap Memory, Thread Stacks, Meta Space, Code Cache, Shared Library 다섯 개로 나뉜다.
- Heap Memory
- JVM이 객체의 인스턴스나 동적 할당된 데이터를 저장하는 공간.
- Garbage Collection이 일어나는 장소.
Garbage Collection을 위해 메모리를 Young과 Old라는 두 개의 영역으로 논리적으로 분할해 사용한다.
자세한 내용은 Garbage Collection에서 다룰 예정이지만, 간단히 정리하면
-
Young : 인스턴스를 처음 생성했을 때 메모리에 배정되는 영역. 이곳에서 일어나는 Garbage Collection을 Minor GC라고 부른다.
- Eden : 인스턴스를 생성하면 우선 이곳에 배정된다.
- Survivor space : Minor GC로 Garbage를 걸러내고 살아남은 인스턴스가 배정된다.
-
Old : Minor GC를 여러 번 거치고도 살아남은 인스턴스가 배정되는 곳. 이곳에서의 Garbage Collection은 Major GC라고 부른다.
- Thread Stack
메모리에서 흔히 말하는 Stack. 스레드마다 하나씩 배정되는 메모리 영역으로, 메소드의 결과 / 반환값을 저장하거나 지역변수를 저장하는 용도로 쓰인다.
- MetaSpace
애플리케이션의 클래스나 메서드 정보, 또는 static으로 정의된 멤버 변수가 저장된 영역. 이전 버전에서는 PermGem이라는 이름으로도 통용된다.
- Code Cache
Just In time (JIT) 컴파일러가 데이터를 저장하는 영역으로, 자주 접근하는 '컴파일된 코드 블록'이 저장된다. 일반적으로 JVM은 바이트 코드를 기계어로 변환하는 작업을 수행하는데, 이곳에 저장된 코드는 기계어로 이미 변환된 채 캐시되어 있으므로 빠르게 실행할 수 있다.
- Shared Library
애플리케이션에서 사용할 공유 라이브러리가 기계어로 변환된 채 저장된 영역. 해당 OS에서 프로세스당 한 번씩 로드된다.
JVM 메모리 할당 과정 (Stack, Heap)
아래의 자바 코드를 실행하면, 프로그램 실행 과정에서 JVM 메모리에 어떤 변화가 있는지 아래의 슬라이드를 참고해 확인해보자.
class Employee {
String name;
Integer salary;
Integer sales;
Integer bonus;
public Employee(String name, Integer salary, Integer sales) {
this.name = name;
this.salary = salary;
this.sales = sales;
}
}
public class Test {
static int BONUS_PERCENTAGE = 10;
static int getBonusPercentage(int salary) {
int percentage = salary * BONUS_PERCENTAGE / 100;
return percentage;
}
static int findEmployeeBonus(int salary, int noOfSales) {
int bonusPercentage = getBonusPercentage(salary);
int bonus = bonusPercentage * noOfSales;
return bonus;
}
public static void main(String[] args) {
Employee john = new Employee("John", 5000, 5);
john.bonus = findEmployeeBonus(john.salary, john.sales);
System.out.println(john.bonus);
}
}
- 메인함수가 먼저 Thread Stack에 자리잡고, 그 후 첫줄 코드가 실행된다.
Employee john = new Employee("John", 5000, 5);
- Employee라는 새 인스턴스가 Heap에 생성되고, Employee의 생성자에 주어진 인자값으로 인스턴스의 attribute가 초기화된다.
- Thread Stack에 정의된 john이라는 변수가 인스턴스를 참조한다.
john.bonus = findEmployeeBonus(john.salary, john.sales);
- findEmployeeBonus 메소드가 Stack에 올라가고, findEmployeeBonus 메소드 내에서 getBounsPercentage 메소드가 Stack위에 쌓인다.
- getBonusPercentage 메소드에서 연산이 끝나면, findEmployeeBonus 메소드에 반환한 뒤 스택에서 빠져나간다.
- findEmployeeBonus 메소드에셔 연산이 끝나면, Integer 인스턴스가 Heap 내에 생성되고 john의 bonus라는 attribute가 Integer 인스턴스를 참조한다.
System.out.println(john.bonus);
- john 인스턴스의 bonus attribute를 콘솔에 출력한다.
- 메인함수가 void를 리턴하고 프로그램이 종료된다.
확인할 수 있는 특징은
- primitive 데이터 타입 (int)는 stack 메모리에 바로 저장된다.
- Integer, Employee, String과 같은 객체는 Heap에 인스턴스 형태로 저장되며, stack에서 포인터 형태로 참조한다.
- 메소드를 호출하면 stack 위에 쌓이며, 메소드가 연산을 마치면 stack을 빠져나온다.
- 메소드에 정의한 지역변수, 리턴값은 stack에 저장되며, 연산을 마치면 stack과 함께 사라진다.
참고자료