이 글에서는 java가 실행되는 환경인 Java Virtual Machine에 대해 알아볼 것이다. Java 8 이후에 변경된 점을 포함하기 때문에 기존 PermGen 영역에 위치하던 Method Area를 Metaspace라고 표기했으며, static과 constant pool이 heap으로 옮겨졌다는 것을 반영한다.
JDK, JRE, JVM
Java의 구동에는 여러가지가 필요하다.
- Java Development Kit - Java 개발 도구
- Java Runtime Environment - Java용 실행 환경
- Java Virtual Machine - Java 실행 프로그램
Java는 여러가지 OS에서 사용 시 java platform indepence를 보장받기 위해 virtual machine을 사용하며 이를 JVM이라 한다.
JVM의 구조
JVM은 다음과 같은 구성으로 이루어진다.
- Class Loader
- Execution Engine
- Interpreter
- JIT Compiler
- Garbage Collector
- Runtime Data Area
- Metaspace ( == Method Area == Class Area)
- Heap (object + static + constant pool)
- Stack Area
- PC Registers
- Native Method Stacks
- Java Native Interface, Native Method Library
Java의 실행 과정
어떤 코드가 작성되었을 때 실행 과정은 다음과 같다.
1. 작성한 .java 파일을 java compiler가 .class 파일(byte code)로 변경한다.
2. JVM의 class loader가 .class 파일을 JVM에 로딩한다.
3. Execution engine이 로딩된 .class 파일을 실행한다.
이 때 JVM은 byte code를 OS에 알맞는 binary code로 변환하고 넘겨 준다.
Class Loader
Class Loader는 class를 JVM에 dynamic load해 준다.
앞에서 Java Compiler가 .java 파일을 .class 파일로 compile해서 넘겨준다고 했다. 어떤 class가 처음으로 호출되었을 때 Class Loader는 해당 class 파일을 load하고 link해서 initialize한 후 Runtime Data Area에 올려준다. 즉 한 번에 모든 class를 memory에 올리는 것이 아니라 해당 class가 참조되는 순간에 dynamic하게 올려준다.
Execution Engine
Execution Engine은 Runtime Data Area에 올라간 class byte code를 실행한다. Interpreter와 JIT(Just-In-Time) 방식을 혼합한다.
interpreter는 byte code에 명시된 명령어를 한 줄 한 줄씩 native code로 바꾸어 실행하는 방식이고, JIT는 byte code 전체를 compile해서 native code로 만들고 바로 실행하는 것이다. interpreter는 중복되는 byte code도 native code로 바꾸는 compile을 하게 되는데, 이는 비효율적이기 때문에 JIT을 사용하는 것이 효과적이다. 따라서 처음에는 interpreter 방식으로 실행하다가 적당한 시점에 JIT 방식으로 실행한다.
Native Code란 CPU가 직접적으로 읽을 수 있는 형태의 코드를 말한다.(binary code 등)
Garbage Collector
Garbage collector는 heap에서 사용하지 않는 object들을 해제해 memory leak를 막는다. java 8부터는 runtime constant pool과 static도 모두 GC의 대상이 된다.
Java는 C나 C++과 다르게 개발자가 직접 memory address에 접근할 수 없다. 따라서 memory에 할당되었으나 사용하지 않는 object를 해제하는 기능이 필요하다. 이처럼 GC는 사용하지 않는 object들을 해제한다.
Runtime Data Area
Runtime Data Area는 java applicatoin 수행 시 사용되는 memory 공간이며 아래의 5가지 종류가 있다.
- Program Counter Register : 실행 중인 JVM instruction의 주소값을 저장한다.
- Stack : method 호출 시 해당 method의 parameter, local variable, 리턴값이 저장되는 영역이다.
- method별로 frame을 만들고, 그 안에 parameter, local variable, return value 등이 저장된다.
- method가 호출될 때 할당되며 method가 끝날 때 소멸하며 stack이라는 이름에 맞게 LIFO이다.
- Native Method Stack : native code를 위한 memory이며 native method의 parameter, local variable 등이 올라간다.
- Heap : static, constant pool, new로 동적 할당된 object들이 올라가는 영역이며, 참조 객체들은 이 영역을 참조한다. Heap은 garbage collector가 관리하기에 더 이상 참조당하지 않는 object / static / constant pool의 내용은 garbage collector가 삭제한다.
- contant pool은 class file에서 사용되는 숫자 값, string 값, 등등의 literal 상수 값이 저장된다.
- 변경 이유는 기존 PermGen이라는 공간에서 static과 runtime constant pool을 할당했는데, 이 경우 많은 static object을 선언하거나 runtime constant pool이 부족해지는 Out Of Memory 현상이 많이 일어나서 수정했다고 한다.
- 참고 : JEP 122
- Metaspace : Class Loader가 로드한 class의 metadata(class field, method, type, constructor 등의 class 정보)가 올라가는 공간이다. class에 대한 코드 정보가 올라가기 때문에 Class Area라고 부르기도 하고, method에 대한 정보가 올라가기 때문에 Method Area라고 부르기도 한다.
- Method Area는 JVM이 시작될 때 생성되어 프로그램이 종료될 때 까지 유지되며 새로운 class가 load될 경우 그 내용도 추가된다.
Heap과 Method Area는 모든 thread가 공유하고 PC Register, Stack, Native Method Stack은 각 thread가 개별적으로 가지고 있다.
* 각 변수들의 로딩 위치
class variable(static variable) : heap
local variable, parameter : stack
JNI, JNL
각각 java native interface, java native library이다. JNI는 타 언어로 만들어진 프로그램과의 상호작용에 사용하는 interface와 JNL은 그 때 사용하는 library이다.
예시: main() method 실행 시 일어나는 일
다음과 같은 method를 실행했다고 하자.
public class App {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
0. App.java 파일을 compiler가 .class 파일로 변환한다.
1. JRE가 static void main() method를 찾는다.
2. JVM이 실행된다.
- Class Loader가 App.class 파일을 JVM에 로딩한다.
- 추가로 App.class에서 import하는 java.lang package를 method area에 올린다.
3. main method가 stack에 올라간다.
- 이 때, main의 리턴 타입과 parameter 등이 frame으로 묶여 stack에 올라간다.
- Hello World!가 출력된다.
- 이후에는 main method가 실행 종료되었기 때문에 stack에서 해제된다.
4. main method가 끝났기 때문에 JRE는 JVM을 종료시키고 JRE도 종료된다.
Garbage Collector
garbage collector의 동작 원리에 대해 좀 더 살펴볼 것이다.
Heap의 좀 더 상세한 구조
heap은 위 그림과 같이 eden 영역, survivor 영역, old 영역으로 나뉜다. 일반적으로 object는 빨리 GC의 대상이 되고, 살아남는 object는 더 오래 살아있을 가능성이 높기 때문이다.
- eden space : 새롭게 생성된 object들이 위치해 있는 공간
- survivor 0 / survivor 1 space: eden space의 object 중 garbage collection에서 살아남은 object가 이동하는 공간
- old space : survivor 0/1 space에서 계속 살아남아 있는 object가 이동하는 공간
Java 8에서 업데이트 된 내용에 따르면 static 변수들과 runtime constant pool도 heap에 속한다. 그러나 이것들의 gc에 관한 내용은 찾지 못했기 때문에 발견 시 추후 올리겠다. 추측하건데 static 변수의 경우에는 해당 class가 metaspace에서 내려가야만 GC가 이루어 질 것 같다. 그렇지만 이 경우는 잘 없기 때문에 기존에 static을 사용하는 것과 마찬가지로 메모리 낭비를 주의해 static을 사용해야 할 것이다.
GC는 heap에 존재하는 object가 garbage인지 여부를 판단하기 위해 reachability를 사용한다. reachable은 [해당 object가 참조되는가?]로 판단한다. reachable object는 살리고, unreachable object는 죽이는 식이다.
Minor GC
Minor GC는 young generation에 대해 수행하는 GC이다. young generation space가 old generation space보다 더 작기 때문에 GC에 더 적은 시간이 걸리며 상대적으로 더 많이 수행된다.
수행 과정은 아래와 같다.
- eden space가 가득 차면 GC가 발생한다.
- eden space에서 GC에서 살아남은 object는 survivor space 중 하나로 이동한다.
- survivor space에서 살아남은 space는 다른 survivor space로 이동한다. 원래의 survivor space는 비게 된다.
- 이 과정을 반복하면 survivor space에 계속 사용하는 object만 쌓일 것이다. survivor space object 중 일정 이상 살아남은 object는 old generation으로 이동한다.
Major GC
Major GC는 old generation에 대해 수행하는 GC이며 old generation이 가득 차게 되면 수행한다.
실행은 크게 mark-sweep-compact 3단계이다. 사용하지 않는 object를 찾는 mark단계 - 해당 object를 지우는 sweep 단계 - memory fragmentation을 막기 위한 compact로 구성된다.
이 때 GC를 실행하는 thread를 제외한 나머지 thread는 모두 작업을 멈추며 이를 stop-the-world라고 한다.
major GC는 Serial GC, Parallel GC, Parallel Old GC, CMS GC, G1 GC 등이 있다.
'Development > Java' 카테고리의 다른 글
[Java] Primitive Wrapper Class (0) | 2023.03.04 |
---|---|
[Java] Generic (0) | 2023.03.02 |
[Java] Exception Handling (0) | 2023.03.01 |
[Java] abstract class vs interface (0) | 2023.02.27 |
[Java] Polymorphism - static polymorphism, dynamic polymorphism, casting (0) | 2023.02.23 |