JVM 의 Stack Area, Heap Area 중심으로
Kotlin / JVM 메모리 사용방식
Run-Time Data Area 는 크게 Method Area, Heap Area, Stack Area, PC registers, Native Method Stack 이 존재합니다.
여기서 PC Register, Native Method Stack 영역은 Low Level 의 Operation 을 위한 공간입니다.
이 영역은 인스턴스 생성을 위한 객체 구조, 생성자, 필드 등이 저장됩니다.
JVM 에서 하나만 생성되는 이 영역은 JVM 의 모든 Thread 가 Method Area를 공유하게 됩니다. 여기서 특정 객체나 메모리에 대한 요청이 오면 실제 물리 메모리로 변환해서 전달해주는 역할을 합니다.
RunTime 에 생성되고, 종료까지 유지되는 공통 영역입니다.
Method Area 에 저장되는 목록
Type Information
- Type 이름 : Package Name + Class Name
- Type 종류 : Class, Enum ,Interface, data Class, sealed Class 를 구분하기 위한 정보
- Type 제어자 : (접근제어자 public, private, default, abstract, final .. )
- 관련된 Interface 정보
Runtime Constant Pool
말 그대로 Type 의 상수 정보를 따로 저장하는 Pool 입니다. 상수는 Index 를 통해 접근이 가능합니다.
실제로 이는 직렬화 하기 위해 타입을 체크할 때도 필요합니다.
이 영역에서는 Type, Field, Method 등 메모리에 접근하기 위한 reference 정보를 가지고 있습니다.
다음은 String 내부의 serialVersionUID 값입니다.
모든 클래스는 직렬화 하기 위해 이 값들이 JVM 에서 default 로 선언해줍니다. 이 값으로 클래스를 비교를 합니다. 이 부분은 직렬화 포스팅에서 구체적으로 확인해보세요!
Field Information
- Type 명, 접근 제어자 를 저장
Method Information
- Method 명, 반환 타입, parameter 수와 순서, 타입 정보들을 가지고 있습니다.
흔히 객체가 저장되는 공간이라고 많이 알고 있죠. 문자열에 대한 String Pool, 인스턴스, 배열 등이 저장되고 JVM 내에서 하나만 생성됩니다. 이 데이터는 Stack 의 reference 에 의해 참조되고 Thread 공유 데이터입니다.
Heap영역은 Stack과는 달리 조금 속도가 느립니다. 또한 공유 자원이기 때문에 동시성 문제(Thread Safe 하지 않은 문제)가 발생할 수 있는데 synchronized 블록을 사용하거나 Lock을 이용하는 방법등 이런 방법들로 해결하는 것이 좋습니다.
그럼 이 영역에서 GC 가 어떻게 동작하는지 알아볼까여?
여기서 Eden이 Full될 때마다 From 의 Lived 객체는 To Space로 이동하는데 이는 논리적으로 그렇고,
Minor GC 가 발생할 때마다 Survivor Space 의 포인터를 유지하고 있다가 From Space에서 To Space로 변경시켜주는 것이다. 이 후 Eden 이 Full ehlaus To Space에 있는 Lived 객체는 Old Generation 으로 복사됩니다.
정리하면?
1. if(Eden Full) -> GC -> Lived 객체 (Eden -> From)
2. if(Eden Full) Lived 객체 ( From -> To ) Space 로 이동
3. if(Eden Full) Lived 객체 ( From -> Old Generation ) Space 로 이동
여기서 보다 시피 주소만 바꿔주는 작업이기 때문에 매우 빠르고 효율적입니다. JVM Thread 가 멈추기 않게 동작하는 이유중 하나입니다.
왜 이렇게 가만히 두질 않고 자꾸 옮겨주는 걸까요? 이는 최대한 객체의 생명 주기를 짧게 가져가겠다는 JVM 의 의도입니다.
Major 는 모든 Thread 실행을 잠시 멈추고 살아있는 객체를 Check 해둡니다.
사용하지 않는 객체는 정리합니다. 이 작업을 Mark and Sweep 이라고 하는데 대체로 Minor GC 보다 10배 이상 많은 시간을 소요하게 됩니다.
이는 Old Generation 으로 가기 전에 Young Generation에서 제거되게끔 하고 오래된 객체의 경우 Old Generation에 상주시켜 Minor GC만으로 관리할 수있게끔 유도하는 것이 중요한데
Young Generation은 전체의 1/2 보다 작게
Survivor Space 는 전체의 1/8 크기면 적절하다고 한다.
위까지 Method Area, Heap Area는 쓰레드간 공유하는 공통 영역이고 아래부터 나타나는 PC Register, Stack Area, Native Method Stack 이 세가지는 각각의 쓰레드마다 별도로 생성하는 공간입니다.
그럼 3가지를 소개해볼까요?
기술 면접으로 나오는 질문중 하나가 JVM 메모리 영역이죠.
Stack Area에서는 메소드가 호출될 때 할당되는 영역입니다. 메소드 내부의 지역 변수,
여기서는 메모리를 호출할 때마다 Function Frame를 추가(push)합니다. 메소드가 반환하면 Frame 은 Stack으로 부터 제거(pop)됩니다.
내부 구성 영역은 다음과 같습니다.
Local Variable, Operand Stack 그리고 Constant Pool Reference 로 구성이 되어 있습니다
Heap 영역에 생성된 객체의 reference 와 일치하지 않는 즉. 참조되지 않는 값들은 GC가 따로 처리합니다.
Java를 사용하면서 Java로 작성되지 않은 라이브러리나 API가 조재합니다. 이런 다른 프로그래밍 언어로 작성된 메소드를 Native Method 라고 합니다. 흔히 C Stacks 라고 불리는데 이 영역은 Native Method를 호출하거나 생성, 실행될 경우 Stack 영역에 쌓입니다.
현재 진행중인 명령어의 reference 를 저장하고 사용하는 관리를 담당하는 주체입니다. Native Method 라면 undefined 라고 작성되고, 그렇지 않은 Java 명령어면 명령의 주소 값을 저장하게 됩니다.
reference