JVM 관련
JVM 전체 구조
https://coding-factory.tistory.com/828
Class Loader
- 런타임(최초 호출 시점)에 .class(바이트코드) 읽어 class load
Execution Engine
- Interpreter 방식, JIT compile 방식 둘 다 사용
Memory Layout
- PC register
- JVM level에서 현재 실행하고 있는 instruction의 주소 보관.
- CPU level의 PC와 기능은 같지만 추상화 수준이 다름.
- thread 별로 각각 가지고 있음. (CPU register가 thread local 한 것 처럼 당연히)
- Method Area
- Memory layout 상 PermGen, MetaSpace 안에 속하는 일부 영역.
- OS의 text segment와 유사함. (결국 실행 대상 code, instructions를 보관하는 영역이라는 뜻)
- run-time constant pool
- field and method data
- the code for methods and constructors
[!warning] static 변수는 Method Area 안에 들어가는게 아니라(JVM spec에 이런 내용은 없다),
Method Area를 감싸고 있는 PermGen 영역에 들어간다. (이 것도 java 8 부터는 PermGen이 사라지면서 달라졌다.)
(잘못 적어둔 곳이 많아 언급한다)
[!info] Method Area 등은 JVM spec 에서 사용하는 용어이고,
실제 구현체인 Hotspot JVM에서는 이와 1:1 대응되지 않는 다른 용어(PermGen, Metaspace)를 사용 할 수 있다.
그래서 스펙 자체에 뭐가 들어가고 뭐가 안들어가고를 너무 타이트하게 볼 필요는 없다.
JVM Heap 구조와 GC
- Eden -> [S0 <> S1] -> Old 순으로 이동한다. (Old == Tenured)
- Survivor 영역을 S0, S1 2개로 구성하고 GC 마다 살아남은 객체가 S0 <> S1을 왔다 갔다 하도록 만드는 이유는, 메모리 스페이스를 재정렬해서 연속된 메모리 영역을 확보하기 위함.
- https://dzone.com/articles/understanding-the-java-memory-model-and-the-garbag
- GC 동작 방식에 대해서는 d2 참조 https://d2.naver.com/helloworld/1329
JVM PermGen과 MetaSpace
https://www.digitalocean.com/community/tutorials/java-jvm-memory-model-memory-management-in-java (GC 구간 참고)
- Java 7까지는 PermGen(Permanent Generation) 영역이고, Java 8 부터는 해당 영역이 MetaSpace 영역으로 대체되었다. (https://openjdk.org/jeps/122)
- 둘 다 목적은 로드된 class metadata 보관. (class metadata에 대해서는 아래 별도 항목 참조)
- PermGen 영역의 문제점은,
- memory leak : GC가 제대로 되지 않아, MaxPermSize가 다 차면서 OOM 발생 할 수 있다는 점.
- MaxPermSize가 항상 고정이라는 점. (물론 아예 크게 잡으면 되긴 하지만…)
- https://www.baeldung.com/java-permgen-space-error
- PermGen과 MetaSpace의 메모리 고갈 비교
- PermGen이라고 GC가 안되는 것은 아니고, leak 원인은 다양하지만 주로 classloader 관련해서 문제가 있는 듯
- PermGen 영역과 MetaSpace 영역의 차이는,
- PermGen 영역은 Heap의 일부이나, MetaSpace는 Native Memory 영역으로, OS가 관리.
- Java 7에서는 PermGen 영역에 일부 Java Object (static 변수, interned string)가 들어갔지만, Java 8 부터는 기존 PermGen에 들어가던 Java Object는 모두 Old 영역에 들어가고, MetaSpace에는 Meta 정보만 들어감.
- MetaSpace는 기본이 unbounded 크기라 해당 머신의 메모리를 다 쓰지 않는 한 OOM 발생 가능성 낮음.
- MetaspaceSize(임계치)에 도달하면 자동으로 GC 트리거하여 dead classloaders and classes 제거.
- https://www.baeldung.com/java-permgen-metaspace
[!warning] PermGen에서 저장하고 있는 static 변수는 reference 만이다. Object itself가 아니다. (sof)
동적배열 static 변수가 PermGen 고갈을 유발한다고 볼 수는 없다.
Class Loading, Linking, and Initializing
- https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html
- 상기 JVM 구조에서, Class Loader가 클래스를 로드하는 과정은 아래 3가지 sub-step으로 구성된다.
- 이 과정은 thread-safe 하다. (동시에 여러 thread에서 여러번 초기화 되지 않는다)
- 어떻게 접근하느냐에 따라 Loading까지만 일어나고 static 초기화는 아직 일 수도 있지만, 보통은
Cls.class
만 하는 경우가 잘 없으므로 3가지 과정이 연속해서 일어난다고 보면 된다.
1. Loading
Cls.class
접근하는 순간 발생- .class 파일 읽어와 메모리(Method Area)에 로드한다.
2. Linking
- Loading이 끝나면 바로 수행
- class, interface에 대한 verifying, preparing 과정
Verification
- 임의로 bytecode가 변경되었을 수도 있기 때문에, 로딩 된 bytecode가 malform은 아닌지 Verification 하게 된다.
- e.g. final class인데 자식이 있다거나
- 이런 검증들은 compile time에도 하게 되지만, compile 결과물이 변경 되었을 가능성이나, 컴파일러 버전이 너무 낮아 지금은 호환되지 않는 문법을 사용했을 가능성을 배제 할 수 없다.
- 그래서 Class를 실제로 사용하는 시점에 한 번 더 검증하는 것.
Preparation
static
필드를 담을 메모리를 할당하고, 일단 기본 값으로 해둔다.int
는 0으로,String
은 null로, 즉 실제 코드단에서 설정한 값은 무시하고 JVM 상의 기본값으로 초기화해둔다.- 실제 코드단에서 개발자가 초기화 하라고 설정한 값은 3. Initialization 단계에서 세팅하게 된다.
Resolution
- 심볼릭 레퍼런스를 실제 메모리 주소로 변환하는 과정.
javap
로 class 파일 열어보면 아래와 같이 메서드, 필드, 클래스에 대한 레퍼런스가 문자열 인덱스로 나타나있다. 이를 실제 메모리 주소로 변환하는 과정임.
1
2
3
4
5
6
public class Example
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#19 // Example.number:I
...
3. Initialization
- 상수가 아닌 static 멤버에 접근하는 순간(resolve가 필요한 순간) 발생
- static 필드 초기화, static 블럭 수행
[!warning] 즉, static 변수는 예상과 달리 Application 시작 시점에 자동으로 초기화 되는 것이 아니다.
Runtime에 클래스의 static 멤버에 최초로 접근하는 순간, 해당 클래스의 모든 static 멤버들이 같이 초기화 된다.
class load 자체가 런타임에 동적으로 일어나기 때문에, 메모리에 load 조차 되지 않은 클래스의 static member를 초기화 하는 것은 불가능하다.
Execution Engine - JIT Compile
- JVM은 어떤 메서드를 실행 할 때, 최초에는 interpreter로 실행한다.
- interpreter로 실행해도, bytecode를 interpret 하는 것이기 때문에 python, js를 실행하는 것 보다는 빠르다.
- interpreter로 실행하면서 대상 메서드가 몇 번 호출되었는지 profiling 한다.
- 자주 호출되는 메서드를 hotspot으로 보고, JIT compile 해서 native binary로 만들어 실행 속도를 향상시킨다.
- 그래서 이름이 hotspot JVM이다.
- JIT는 Just-In-Time, 말 그대로 사용되는 그 때 컴파일 한다는 의미다.
- JVM의 JIT 컴파일 단위는 메서드다.
Tiered Compilation
- JVM은 2가지 JIT compiler를 가지고 있다.
- C1 compile ( Client Compiler )
- 빠른 컴파일러
- C2 compile ( Server Compiler )
- C1 보다 느리지만 더 높은 수준으로 최적화하는 컴파일러
- JIT compile을 프로파일링 수준에 따라 C1, C2 두 단계에 걸쳐 진행한다. (그래서 Tiered Compilation)
- Tiered Compilation은 기본적으로 활성화 되어 있다.
- C1, C2 실제 컴파일은 2 단계로 구분되지만, 더 세부적인 기준으로 Tier0~4 총 5단계로 구분하고 있다.
- Tier0 - Interpreted Code
- Tier1 - Simple C1 Compiled Code
- Tier2 - Limited C1 Compiled Code
- Tier3 - Full C1 Compiled Code
- Tier4 - C2 Compiled Code
- 보통 Tier 0 → 3 → 4 로 동작하게 된다.
- 메서드가 몇 번 호출되어야 다음 Tier로 넘어가는지는 아래 명령어로 확인 가능하다. (JVM 버전에 따라 기본값이 다르다.)
1 2 3 4 5 6 7 8
❯ java -XX:+PrintFlagsFinal -version | grep CompileThreshold intx CompileThreshold = 10000 {pd product} {default} <-- 이건 Tiered Compilation이 활성화 되어 있으면 의미없는 값이다. double CompileThresholdScaling = 1.000000 {product} {default} uintx IncreaseFirstTierCompileThresholdAt = 50 {product} {default} intx Tier2CompileThreshold = 0 {product} {default} intx Tier3CompileThreshold = 2000 {product} {default} intx Tier4CompileThreshold = 15000 {product} {default}
Compile 결과물은 Code Cache에 저장되고, Deoptimization 되는 경우도 있다