JVM 구동 방식

JVM 메모리 구조

Method Area
메서드 영역은 보통 정적 영역이라 불린다. 프로그램 실행 중 클래스나 인터페이스를 사용하면, JVM은 클래스 로더를 이용해 클래스와 인터페이스의 메타데이터를 메서드 영역에 저장한다.
- JVM이 동작해 클래스가 로딩될 때 생성
- 클래스 로더에 의해 Load된 모든 Type의 메타 정보를 저장하는 메모리 공간
- Type은 클래스와 인터페이스를 통칭한다.
- Type 메타데이터
- 런타임 상수 풀, 멤버 변수(필드), 클래스 변수(static 변수), 상수(final), 생성자(constructor)와 메서드(method)
- 어느 곳에서나 접근 가능
- 프로그램 시작부터 종료까지 메모리에 남아있어, 프로그램 종료까지 어디서든 사용 가능하다.
- static 데이터를 무분별하게 많이 사용하는 경우 부족 현상이 나타날 수 있다.
- JIT(Just In Time) 컴파일러가 번역한 기계어 코드를 캐싱하기 위한 메모리 공간으로 활용
- 인터프리팅 기능의 양이 작아저 기능이 향상됨.
- Java 8부터는 PermGen이 아니라 Metaspace에 속함
- Metaspace는 JVM 힙이 아니라 네이티브 메모리에서 관리하며 크기가 동적으로 달라질 수 있음.
Runtime constant pool
- 클래스 버전, 필드, 메서드, 인터페이스 등 클래스 파일에 포함된 정보 및 각종 리터럴, 심벌 참조가 저장되는 영역
- 상수(클래스 자체에 대한 메타데이터)들이 저장되는 공간.
- 심볼(특정 클래스가 어떤 클래스를 어떻게 참조해 어떤 메서드를 콜하는지)를 저장하는 정보
- 해석: 그냥 문자열로 적혀있는게 심볼참조를, 실제 메서드 영역의 자바 바이트 코드 위치의 메모리 주소를 찾아서 그걸 콜하도록 바인딩하는 과정
- 클래스 로더가 클래스를 로드할 때 상기 정보들을 저장
- 동적으로 운영되며 런타임에 새로운 상수가 추가될 수 있음
- 코드 수준에서 상수를 기술, 컴파일 타임에 로딩된다고 기술.
코드 자체는 짧더라도 상수 풀에는 상당량의 데이터가 들어온다.
따라서 클래스 양이 늘어나면, 이 상수풀의 양도 폭발적으로 늘어날 수 있다.
Method 영역 데이터 할당 과정

Stack area
스택은 각 스레드가 실행 중인 메서드 호출 및 반환에 관여하고 지역 변수와 결과들을 저장해, 메서드 호출과 실행에 대한 전반적인 관리를 수행한다.

- 자바 스택은 스레드별로 1개가 존재하며, 스택 프레임(Stack Frame)이라는 구조체를 저장하는 스택이다.
- LIFO(Last In First Out) 구조로 관리된다.
- 스택 프레임은 메서드가 호출될 때마다 생성된다.
- 메서드 실행이 끝나면 스택 프레임은 pop되어 스택에서 제거된다.
스택 프레임(Stack Frame)
구성요소
- 지역 변수 배열 (Local Variable Array)
- 메서드 내에서 사용되는 모든 지역 변수들이 저장된다.
- 0부터 시작하는 인덱스를 가진 배열 형태로 관리된다.
- 다음 데이터들이 포함된다.
- 메서드 파라미터
- 지역 변수
- 인스턴스 메서드의 경우 this 참조 (index 0에 위치)
- 피연산자 스택 (Operand Stack)
- 메서드 실행 중 실제 작업이 이루어지는 공간
- 다음의 상황에서 사용된다.
- 연산을 위한 피연산자 보관
- 메서드 호출 시 전달할 파라미터 저장
- 메서드의 반환값 임시 저장
- 상수 풀 참조 (Runtime Constant Pool Reference)
- 해당 메서드가 속한 클래스의 상수 풀 참조
- 다음의 정보들을 포함한다.
- 리터럴 상수
- 타입 참조
- 필드 참조
- 메서드 참조
- 실행 환경 정보
- 메서드의 실행과 관련된 부가적인 정보들이 저장된다:
- 메서드 호출자 정보
- 예외 처리 정보
- 디버깅 관련 정보
스택의 push와 pop
스택이 프레임을 push하고 pop하는 과정은 아래와 같다.
public class Example {
public static void main(String[] args) {
int result = methodA();
System.out.println(result);
}
public static int methodA() {
int value = methodB();
resurn value + 10;
}
public static int methodB() {
reuturn 5;
}
}
스택의 push 과정

main()
호출
- 스레드가 시작되면
main()
이 호출된다. main()
의 프레임이 스택에 push된다.
- 이 프레임에는
mina()
의 파라미터, 지역변수, 연산결과를 위한 피연산자 스택 정보들이 포함된다.
main()
에서 methodA()
호출
methodA()
의 프레임이 main()
의 프레임 위 스택에 push된다.
methodA()
의 프레임도 역시 파라미터, 징겨변수, 피연산자 스택이 포함된다.
methodA()
에서 methodB()
호출
- 위와 마찬가지로
methodB()
의 프레임이 스택에 push된다.
스택의 pop 과정

반대 순서대로 (methodB
프레임 -> methodA
프레임 -> main
프레임) 스택에서 pop된다.
반환되는 과정에서 반환된 값이 있다면, 이전 프레임의 피연산자 스택에 push된다.
예를들어, methodB()
에서 반환된 5는 methodA()
의 프레임의 피연산자 스택에 push되어 사용되어 진다.
스택의 예외처리
StackOverflowError
- 스레드의 계산이 허용된 JVM 스택 크기보다 큰 스택을 필요로 하는 경우, JVM은 StackOverflowError를 던진다.
OutOfMemoryError
- JVM 스택이 동적으로 확장될 수 있는데 확장 시도 시 충분한 메모리를 확보할 수 없거나, 새 스레드를 위한 초기 JVM 스택을 생성할 때 충분한 메모리를 확보할 수 없는 경우, JVM은
OutOfMemoryError
를 던진다.
Stack 영역 데이터 할당 과정

Heap Area
- 모든 JVM 스레드간에 공유되는 공간.
- 동기화 이슈가 수반된다.
- stack은 스레드 갯수마다 각각 생성되지만, heap은 몇 개의 스레드가 존재하든지 상관없이 단 하나의 heap 영역만 존재한다.
- 멀티스레딩 환경에서 스레드 간 공유되므로 자원을 효율적으로 사용할 수 있다는 장점이 있지만, 데이터의 일관성과 스레드의 안정성을 보장하기 위한 추가적 처리가 필요하다.
- 참조형(Reference Type) 데이터 타입을 갖는 객체(인스턴스, Instance)와 배열(Array)이 저장되는 공간
- 단, Heap 영역에 있는 오브젝트들을 가리키는 레퍼런스 변수는 stack에 적재
- JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역
- Heap 영역은 Stack 영역과 다르게 보관되는 메모리가 호출이 끝나더라도 삭제되지 않고 유지된다.
- 그러다 어떤 참조 변수도 Heap 영역에 있는 인스턴스를 참조하지 않게 된다면, GC(가비지 컬렉터)에 의해 메모리에서 청소된다.
Heap 영역의 세부 구조

- Young Generation
- Eden Space
- 새로 생성된 객체가 배치되는 공간. 대부분의 객체가 여기에 생성되고, 일정 시간이 지나지 않고 사용되지 않는다면 소멸된다.
- Survivor Space (S0 and S1)
- Eden 영역에서 살아남은 객체들이 이동하는 곳.
- 객체들은 S0과 S1 사이에서 이동하며, 여러 번의 가비지 컬레션(GC) 사이클을 거치면서 살아남은 객체는 Old Generation으로 이동한다.
- Old Generation
- 장시간 동안 사용되는 객체들이 이동하는 영역.
- Young Generation에서 살아남은 객체들이 이곳으로 이동하며, 가비지 컬렉션이 발생하는 빈도가 상대적으로 낮다.
- Java8버전 이후의 Heap 영역의 구조는 Young Generation, Old Generation만 포함한다.
- Java8 버전 이전: 클래스의 메타데이터가 Heap영역에 저장(이 공간을 PermGen 영역이라 함) 되었다.
- Java8 버전 이후: Metaspace라는 공간이 운영체제가 관리하는 네이티브 메모리 영역으로 분리되면서 메모리 관리 전력이 진화함
- 이전(Java8이전)에는, JVM 시작 시 설정된 고정된 크기에 많은 클래스 로드와 많은 정적 데이터를 사용하는 경우 PermGen 영역이 고갈되면서
java.lang.OutOfMemoryError:PermGen space
오류로 이어지곤 했다.
- Metaspace라는 네이티브 메모리 영역을 사용함으로써 JVM 메모리 요구사항에 따라 필요한만큼 동적으로 메모리를 할당받을 수 있게 되었다.
Heap 영역 데이터 할당 과정
-
생성자 new Counter()
를 호출한다.
- 생성자를 호출하면 heap 영역에 Counter 클래스 인스턴스 변수들이 저장되게 되고, stack 영역의 지역변수 sub에 주소값으로 연결되게 된다.

-
twice(sub)
메소드를 실행한다.
- 새로운 메서드를 실행하는 것이니, stack 영역에 새로운 스택 프레임이 생기게 된다.
- 그리고 아규먼트로 클래스를 전달했기 때문에
twice()
의 매개변수 c
는 주소값으로 같은 힙 영역을 가리키게 된다.

-
객체의 plus()
메소드를 실행한다.
- 객체 Counter에 정의된
plus()
메소드를 호출하게 되는데, 이 역시 메소드이므로 스택 영역에 새로운 스택 프레임으로 생성되게 된다.
- 다만 여기서
this
라는 암묵적인 변수가 자동 생성되게 되는데, 이 this
변수는 자동으로 힙 영역에 있는 Counter 객체를 기리키게 된다.
- 따라서
plus()
메소드 안의 코드 state += n
이 동작하면서 힙 영역에 있는 인스턴스 변수 state
가 값이 변하게 된다.

-
객체의 state
인스턴스 변수를 가져오는 get()
메소드를 호출한다.
- 모두 실행되어 볼일이 끝난
plus()
스택 프레임은 제거된다.
- 마찬가지로
sub
객체변수의 메소드 get()
을 호출하면 스택 영역에 새로운 스택 프레임이 생기고, this
변수가 힙 영역의 객체를 가리키게 된다. 그리고 힙 영역의 변수를 반환하게 된다.
- 마지막으로 할일을 마친
get()
스택 프레임은 스택 영역에서 제거되고, main 스택 프레임에 result2
지역변수가 추가된다.


📌 주의사항
- 이 예제에서 강조되는 부분은 호출되는 메서드가 파라미터로 객체값을 전달받아 객체의 상태를 변경하게 되면, 메서드 종료(스택 제거) 이후에도 힙 영역에 있는 객체의 상태는 쭉 유지된다는 점이다.
-
마지막 코드가 실행되면 main 스택 프레임은 스택 영역에서 제거된다.
- 스택 영역은 메서드의 끝을 알리는 닫는 중괄호
}
를 만나면 자동으로 메모리에서 제거된다.
- 그러나 힙 영역에서는 여전히 객체 데이터가 메모리에 상주하게 된다.

-
가비지 컬렉터(GC)가 힙 영역을 청소한다.
- 가비지 컬렉터는 힙 영역에 참조되지 않고 남아버린 고아 객체들을 식별해 힙 영역을 청소해주는 역할을 한다.
- 추가로 코드 실행이 모두 끝나면 Method(Static) 영역도 비워지게 된다.

PC(Program Counter) Registers

JVM은 멀티스레드 환경을 지원하며, 각 스레드는 독립적인 PC Register를 가진다. PC Register는 다음과 같은 핵심 특징을 가진다.
- 명령어 위치 추적
- 현재 실행 중인 JVM 명령어의 주소를 저장한다.
- 명령어 실행이 완료되면 다음 명령어의 주소를 자동 갱신한다.
- 스레드별 할당
- 각 스레드는 자신만의 PC Register를 보유한다.
- 스레드는 독립적인 호출 스택을 가지며 한 번에 하나의 메서드만 실행한다.
- 실행 흐름 관리
- PC Register는 Java Byte Code레벨에서 실행 흐름을 괸리한다.:
- 현재 실행 중인 작업의 위치
- 다음 실행할 작업의 위치
- 전체적인 실행 흐름의 제어
Native method stack

자바 외의 언어로 작성된 네이티브 코드를 위한 스택.
- JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택. 언어에 맞게 C 스택이나 C++ 스택이 생성된다.
- 자바 메서드가 호출될 때에는 스택 프레임이 스택에 추가되지만, native 메서드가 호출될 때는 새로운 스택 프레임이 추가되는 것이 아니라, 동적으로 native 메서드를 연결한다.
Reference