Post

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

JVM PermGen과 MetaSpace

https://www.digitalocean.com/community/tutorials/java-jvm-memory-model-memory-management-in-java (GC 구간 참고)

[!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 컴파일 단위는 메서드다.

interpreter → C1 → C2

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 되는 경우도 있다

참고

This post is licensed under CC BY 4.0 by the author.