본문 바로가기

Develop/Java

JVM 동작 방식과 구조 정리

728x90

Java

  • 프로그래밍 언어
  • JDK에 들어있는 자바 컴파일러(javac)를 사용하여 바이트코드(.class)로 컴파일 할 수 있습니다.
  • 자바 유료화의 케이스는 오라클에서 만든 Oracle JDK 11 버전부터 상용으로 사용할 때만 유료입니다.
  • 강조된 오라클, Oracle JDK 11 이상 상용 중 하나도 만족이 안되면 유료가 아닙니다.
  • 자바가 왜 유료가 아닌지에 대한 커뮤니티 정리 글 https://medium.com/@javachampions/java-is-still-free-c02aef8c9e04
  • javap -c *.class 를 통해 바이트 코드 확인이 가능합니다.

JVM (Java Virtual Machine)

  • 자바 가상 머신으로서 바이트 코드(.class)를 OS에 특화된 코드로 변환(인터프리터와 JIT 컴파일러)하여 실행합니다.
  • 바이트 코드를 실행하는 표준이다. JVM 자체는 표준이며 특정 밴더 사가 구현한 구현체입니다.
  • JVM 스팩 https://docs.oracle.com/javase/specs/jvms/se11/html/
  • JVM 밴더 : 오라클, 아마존 등등
  • JVM은 특정 플랫폼(Windows, Linux, ...) 에 종속적입니다.

JRE (Java Runtime Environment)

  • JRE는 JVM + Library 입니다.
  • 자바 애플리케이션을 실행할 수 있도록 구성된 배포판입니다.
  • JVM과 핵심 라이브러리자바 런타임 환경에서 사용하는 프로퍼티 세팅이나 리소스 파일을 가지고 있습니다.
  • 개발 관련 도구는 포함하지 않습니다. (JDK에서 제공합니다.)

JDK (Java Development Kit)

  • JRE + 개발에 필요한 툴 (javac, jar, jconsole, …)
  • 소스 코드를 작성할 때 사용하는 자바 언어는 플랫폼에 독립적입니다.
  • 오라클은 자바 11부터는 JDK만 제공하며, 바이트 코드(.class)로 컴파일 할 수 있습니다.
  • Write Once Run Anywhere !

JDK의 구성요소

 


JVM 언어

  • JVM 기반으로 동작하는 프로그래밍 언어
  • Closure, Groovy, JRuby, Jython, Kotlin, Scala, …
  • 위의 언어로 작성한 바이트 코드런타임 환경을 제공하는 라이브러리들을 포함시키는 jar를 사용하여 실행 가능
  • 코틀린으로 kt 파일을 빌드하면 class 파일의 바이트 코드는 만들 수 있지만 이 파일만으로는 실행이 불가능합니다. 런타임 환경을 제공 해야해서 jar 파일안에 런타임 환경을 패키징해야 합니다. kotlinc *.kt -include-runtime -d *.jar

참고문서

JIT Compiler https://aboullaite.me/understanding-jit-compiler-just-in-time-compiler/
JDK, JRE 그리고 JVM https://howtodoinjava.com/java/basics/jdk-jre-jvm/
JVM 언어 리스트 https://en.wikipedia.org/wiki/List_of_JVM_languages

JVM 구조

 

JVM 구조

  • 5가지 컴포넌트로 구성되어 있다고 볼 수 있지만 네이티브 메소드 인터페이스네이티브 메소드 라이브러리는 하나로 묶어서 봐야하기 때문에 크게 4덩어리로 볼 수 있습니다.
  • Intellij로 class 파일을 열어보면 순수 bytecode가 아니라 decompile된 결과를 보여줍니다. (java 파일이랑 거의 비슷함..)

1. 클래스 로더 시스템

  • java 파일의 바이트 코드(.class)를 읽어 들여서 메모리에 적재하고 배치하는 일을 합니다.
  • 클래스 로더 시스템이 하는 일은 크게 3가지 (로딩, 링크, 초기화)
  • 로딩클래스 파일에서 바이트 코드를 읽어오는 과정입니다.
  • 링크도 구체적으로 3개로 나눌 수 있지만 간략하게 레퍼런스를 연결하는 과정입니다.
  • 초기화는 클래스에 있는 static 값들을 초기화 및 변수에 할당합니다.

2. 메모리

  • 메소드 영역에서는 클래스 수준의 정보 (클래스 이름, 풀패키지 이름, 부모 클래스 이름, 메소드, 변수)를 저장한다. 메소드 영역은 공유 자원이라 다른 영역에서도 참조할 수 있습니다.
  • 힙 영역에는 실제 인스턴스들 저장함. 객체를 저장, 공유하는 자원입니다.

나머지 스택, PC, 네이티브 메소드 스택 등의 블럭들은 쓰레드에 국한되며, 어떤 쓰레드인지에 따라 그 쓰레드에서만 공유함. 힙이나 메소드 처럼 다른 모든 영역에 공유하지 않습니다.

 

2 - 1. 스택

  • 쓰레드 마다 런타임 스택을 만들고, 그 안에 스택 프레임을 쌓음. 여기서 스택 프레임은 메소드 콜입니다.

2 - 2. PC (Program Counter)

  • PC Registers는 스택 안에 메서드들을 쌓았을때 현재 어느 위치를 실행하고 있는지를 가리키는 포인터이다. 이는 쓰레드마다 생성됩니다.

2 - 3. 네이티브 메소드 스택

  • 네이티브 메소드 호출할 때 사용하는 별도의 메소드 스택입니다.
  • 여기서 네이티브 메소드라 함은 Thread.currentThread(); 를 예시로 들 수 있는데 public static native Thread currentThread(); 형식으로 선언되어 있으며 C, C++, 어셈블리 등으로 작성된 함수를 자바에서 사용할 수 있습니다. 이러한 함수가 호출되면 그 쓰레드에 네이티브 메소드 스택이 생겨서 스택 프레임에 메소드 콜이 쌓이게 됩니다.

자세한 내용은 https://javapapers.com/core-java/java-jvm-run-time-data-areas/#Program_Counter_PC_Register 를 참고하면 됩니다.

 

이러한 내용들은 주로 Java Application을 Profiling 할때 사용됩니다.


3. 실행 엔진

3 - 1. 인터프리터

  • 바이트 코드를 정말 줄 단위로 한 줄씩 네이티브 코드로 컴파일하며 실행합니다.
  • 근데 여기서 같은 역할을 하는 바이트코드를 똑같이 네이티브 코드로 컴파일 하는 것은 매우 비효율적이기 때문에 반복되는 바이트 코드가 발생하면 JIT 컴파일러한테 보냅니다.

3 - 2. JIT 컴파일러

  • 인터프리터가 한 줄씩 컴파일 하여 실행하는 것의 효율을 높이기 위하여 JIT 컴파일러는 반복되는 코드를 미리 모두 찾아둬서 네이티브 코드로 컴파일 해둡니다. 인터프리터가 반복되는 라인에 도달했을 때, 그것을 인터프리팅하는 것이 아니라 JIT 컴파일러를 통해 만들어진 네이티브 코드를 바로 사용합니다. 이러한 방법으로 프로그램 실행 속도를 좀 더 향상 시킵니다.

3 - 3. GC (Garbage Collector)

  • 인터프리터와 JIT 컴파일러는 어떻게 동작하는지 이해하면 그만이지만 GC는 이해도 해야하고 경우에 따라 우리가 옵션 조정을 통해 커스터 마이징 해야할 수도 있습니다. Profiling 할때도 우리가 사용하는 GC가 어떤 GC 인지 또는 애플리케이션을 실행하기 전에 우리가 사용해야할 GC를 선택해야 하는 경우도 있습니다.
  • 가장 크게 GC를 2가지로 나누면 Throw-put 위주의 GC가 있고 Stop-the-world를 줄이는 GC가 있습니다. 그 중에 적절한 것을 사용하면 되며 우리는 서버 운영 중에 많은 객체를 사용하며 Response Time이 굉장히 중요할 경우 Stop-the-world GC를 사용하여 GC를 사용할 때 발생하는 pause 현상을 최소화할 수 있는 GC를 사용하는 것이 더 좋을 것 입니다.

4. JNI(Java Native Interface)

  • 자바 애플리케이션에서 C, C++, 어셈블리로 작성된 함수를 사용할 수 있는 방법 제공합니다.
  • Native 키워드를 사용한 메소드를 호출합니다.

JNI에 대해 더 자세한 내용은 https://medium.com/@bschlining/a-simple-java-native-interface-jni-example-in-java-andscala-68fdafe76f5f


5. 네이티브 메소드 라이브러리

  • C, C++로 작성 된 라이브러리

참고 https://www.geeksforgeeks.org/jvm-works-jvm-architecture/ https://dzone.com/articles/jvm-architecture-explained http://blog.jamesdbloom.com/JVMInternals.html


세부적인 클래스 로더

  • 로딩 → 링크 → 초기화 순으로 진행됩니다.

1. 로딩 (Bootstrap → PlatForm → Application)

  • 클래스 로더가 .class 파일을 읽고 그 내용에 따라 적절한 바이너리 데이터를 만들고 메소드 영역에 저장합니다.
  • 메소드 영역에 저장하는 데이터 종류는
    • FQCN (Fully Qualified Class Name) 패키지 이름, 클래스 이름, 클래스 로더까지 포함인데 보통은 패키지 이름, 클래스 이름까지만 얘기합니다.
    • 클래스 | 인터페이스 | 이늄 종류를 저장합니다.
    • 메소드와 변수 저장
    로딩이 끝나면 해당 클래스 타입의 Class 객체를 생성하여 영역에 저장됨 (Class<클래스 이름> 이러한 객체가 만들어져서 힙 영역에 저장됨)
  • 클래스 로더는 계층 구조로 이루어져 있으며 기본적으로 세 가지 클래스 로더가 제공됩니다.
    • 부트 스트랩 클래스 로더는 JAVA_HOME\lib에 있는 코어 자바 API를 제공한다. (네이티브 코드)
    • 플랫폼 클래스 로더는 JAVA_HOME\lib\ext폴더 또는 java.ext.dirs 시스템 변수에 해당하는 위치에 있는 클래스를 읽습니다.
    • 애플리케이션 클래스 로더는 우리가 작성하는 클래스 파일과 애플리케이션을 실행할 때 주는 -classpath 옵션 또는 java.class.path 환경 변수의 값에 해당하는 위치에서 클래스를 읽습니다.
    public class App {
    	public static void main(String[] args) {
    		ClassLoader classLoader = App.class.getClassLoader();
    		System.out.println(classLoader); // -> jdk.internal.loader.ClassLoader$AppClassLoader@512ddf17
    		System.out.println(classLoader.getParent()); // -> jdk.internal.loader.ClassLoaders$PlatformClassLoader@6bdf28bb
    		System.out.println(classLoader.getParent().getParent()); // -> null
    	}
    }
    
    null로 나오는 부트 스트랩 클래스 로더 부분은 native code로 구현이 되어 있어서 VM마다 다르고 자바 코드에서 참조해서 출력할 수가 없음

2. 링크 (Verify → Prepare → Resolve)

  • Verify는 .class 파일 형식이 유효한지 체크합니다 (바이트 코드 문법 검증).
  • Prepare는 메모리를 준비하는 과정. 클래스에 있는 static 변수와 기본 값에 필요한 메모리를 준비하는 과정
  • Resolve는 심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체합니다. (이 과정은 Optional입니다. 이때 교체 과정이 벌어질 수도 있고, 나중에 실제 해당 레퍼런스를 사용할 때 발생할 수도 있습니다.)이 코드 Snippet을 읽었다 하더라도 링크는 Book book = new Book(); 은 실제 레퍼런스를 가르키고 있진 않고 논리적인 레퍼런스인데 실제 힙에 들어있는 Book 객체 인스턴스를 가르키도록 하는 과정을 의미합니다. 이 교체 과정이 앞서 설명한 대로 이때 발생할 수도 있고 해당 레퍼런스를 사용할 때 발생할 수도 있습니다.
  • public class App { Book book = new Book(); public static void main(String[] args) { } }

3. 초기화

  • 링크 과정에서 생성된 Static 변수의 값을 할당합니다. (static 블럭이 있다면 이때 실행됩니다.)