JVM JRE JDK JAVA

jdk

  • JDK
    • Java Development Kit 으로, JRE와 개발에 필요한 tool들을 묶은 것
    • 오라클 java 11 버전부터는 JDK만 제공
      • 기존에는 JRE와 JDK를 별도로 제공했음
      • 자바에 모듈시스템이 들어와 JRE를 만들어서 사용할 수 있음 (TODO :jlint)
  • JRE
    • Java Runtime Environment 로 jvm + Library 형태
    • 자바 어플리케이션을 실행할 수 있도록 구성되어 있다. (배포판)
      • jvm 핵심 라이브러리, 자바 런타임 환경에서 사용하는 프로퍼티 세팅이나 리소스 파일을 가지고 있다. (rt.jar, charset.jar ..)
      • 📌 개발 관련 도구는 포함하고 있지 않음 → jre로 자바 파일 컴파일 불가능 (javac 없음)
  • JVM
    • Java Virtual Machine, 자바를 실행하기 위한 가상 기계

    • 자바로 작성된 어플리케이션은 모두 JVM 위에서 실행되기 때문에 자바 어플리케이션이 실행되기 위해서는 반드시 JVM (가상 컴퓨터)이 필요하다.
    • 바이트코드를 실행하는 표준 (JVM 자체는 표준)
      • 특정 밴더(오라클, 아마존 ..)가 구현한 구현체
        • ex. Oracle-jdk 안의 jvm
    • 특정 플랫폼에 종속적
      • 네이티브코드로 바꿔서 실행해야 하는데 네이티브코드가 os 에 맞춰서 실행해야 하기 때문에
    • JVM 언어
      • JVM 기반으로 동작하는 프로그래밍 언어
      • 자바가 아닌 다른 언어로 작성했더라도 JVM 기반으로 동작 가능
        • 자바를 위해 JVM이 만들어졌지만, 사실상 자바 언어와 직접적인 연관관계가 있는게 아니라, 클래스파일(.class) 만 있으면 JVM에서 실행해주는것이기 때문에 자바와 의존성이 깊지 않다.
        • 다른 프로그래밍 언어를 사용하더라도 컴파일 했을 때 클래스파일이 만들어진다거나 자바 파일이 만들어지면 JVM을 활용할 수 있다.
        • 클로저, 그루비, JRuby, Jython, Kotlin, Scala …
          • ex.
            fun main(args : Array<String>) {
                print("Hello, Kotlin")
            }
          
            kotlinc Hello.kt -include-runtime -d hello.jar
          
            java -jar hello.jar
          

          jar 파일로 컴파일하는 이유는 런타임에 필요한 파일들을 같이 패키징 해주기 때문

    • JVM은 JVM만 제공되지 않음, 최소한의 단위 JRE 형태로 제공됨
  • JAVA
    • 프로그래밍 언어
    • 자바로 작성된 소스코드를 JDK에 들어있는 자바 컴파일러(javac)로 컴파일 하여 바이트코드 (.class)파일을 생성하는 것
    • 오라클에서 만든 Oracle JDK 11 버전부터 상용으로 사용할 때 유료

JVM 구조

클래스 로더 시스템

  • .class 에서 자바 바이트코드를 읽어서 메모리의 적절한 공간에 배치하는 시스템
  • 로딩 → 링크 → 초기화 순으로 진행
    1. 로딩: 클래스 로더가 .class 파일을 읽고 바이너리 데이터를 만들고 메소드 영역에 다음 데이터를 저장한다.

       - FQCN (Fully Qualified Class Name)
           - 패키지, 풀패키지 경로, 클래스 이름, (클래스 로더)
       - 클래스인지, 인터페이스인지, enum인지 같은 정보
       - 메소드와 변수
      
      • 어떤 클래스 로더를 사용했는지 알고싶다면?

        클래스로더는 계층형 구조를 갖고있다. 어떤 클래스로더를 사용했는지 알고싶다면 다음과 같이 하면 된다.

          ClassLoader classLoader = App.class.getClassLoader();
          System.out.println(classLoader);
          System.out.println(classLoader.getParent());
          System.out.println(classLoader.getParent().getParent());
        
          결과
          jdk.internal.loader.ClassLoaders$AppClassLoader@125ddf16
          jdk.internal.loader.ClassLoaders$PlatformClassLoader@6bdf26bb
          null    // 부모가 있는데 네이티브 코드로 구현되어 있어 null로 출력된다.(Bootstrap)
        

        최상위 부모가 읽어오고 못읽어올 경우 그 다음 부모가 읽어오고 제일 마지막 클래스 로더가 읽지 못하면 ClassNotFoundException 발생하는 것

        • 부트스트랩 클래스로더 : 최상위 우선순위를 가진 클래스로더로 JAVA_HOME/lib 에 있는 코어 자바 API를 제공
        • 플랫폼 클래스로더: JAVA_HOME/lib/ext 폴더 또는 java.ext.dirs 시스템 변수에 해당하는 위치에 있는 클래스를 읽음.
        • 애플리케이션 클래스로더: 애플리케이션 실행할 때 주는 -classpath 옵션 또는 java.class.path 환경 변수의 값에 해당하는 위치에서 클래스를 읽음.
    2. 로딩이 끝나면 해당 클래스 타입의 Class 객체를 생성하여 Heap 영역에 저장 (ex. Class<Test>)

    3. 링크 (Verify → Prepare → Resolve(Optional))

      • Verify : class 파일 형식이 유효한지 체크
      • Prepare : 클래스 변수 (static 변수) 와 기본값에 필요한 메모리를 준비하는 과정.
      • Resolve : 심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체하는 과정인데, 이 과정은 optional이다. 이때 발생할 수도 있고 실제 해당 레퍼런스를 사용할 때 발생할 수도 있다.
    4. 초기화

    static 변수의 값을 할당하고 만약 static 블럭이 있다면 이때 실행된다.

💡 로딩 : 클래스를 읽어오는 과정
💡 링크 : 레퍼런스를 연결하는 과정
💡 초기화 : static 값들 초기화 및 변수에 할당

메모리

  • 메소드 영역
    • 클래스 수준의 정보 (클래스 이름, 부모 클래스 이름, 메소드, 변수)를 저장한다.
    • 이 영역은 공유 자원으로 다른 영역에서도 참조 가능하다.
    • 예를들어, 다음과 같은 것들이 메소드영역에 저장된다.

        public class Test {
            static String name;
          
            static {
                name = "yesol";
            }
          
            public static void main(String[] args) {
                System.out.println(name);
            }
        }
      
  • 힙 영역
    • 객체가 저장된다 (인스턴스)
    • 공유자원이다.
    • 예.

        Test test = new Test();
      
  • 스택, PC, 네이티브 메소드 스택 영역
    • 힙이나 메소드 영역처럼 모든 영역이 공유하는 자원이 아니고, 스레드에 국한되어 어떤 쓰레드인지에 따라 ㅏ이포그 스레드에서만 공유하는 자원이다.
    • 스택 (Stack) : 쓰레드 마다 런타임 스택 생성, 메소드 호출(Method Call)을 스택 안에 스택 프레임이라 부르는 블럭으로 쌓는다. 쓰레드를 종료하면 런타임 스택도 사라진다.

      이러한 Method Call 들이 스택 안에 쌓여 있는 것이다.

    • PC (Program Counter registers) : 스레드마다 스택이 생성되는데, pc register도 같이 생긴다. pc 레지스터는 스레드 내 현재 실행할 스텍 프레임을 가리키는 포인터로, 스택과 마찬가지로 해당 스택에 국한된다.
    • 네이티브 메소드 스택 : 스레드 마다 생성, 네이티브 메소드를 사용할 때 생기는 별도의 스택이다.
  • 실행 엔진 (Execution Engine)

  • 인터프리터 (interpreter) : 바이트코드를 한 줄 씩 바이트코드로 변환하여 실행
  • JIT 컴파일러 (Just In Time) : 인터프리터는 똑같은 코드가 나와도 한 줄 씩 변환하는데 이는 비효율적, 그래서 인터프리터가 반복되는 코드를 발견하면 JIT 컴파일러로 반복되는 코드를 모두 네이티브 코드로 컴파일 → 그 다음부터는 JIT 컴파일러가 네이티브로 컴파일 해 놓은 코드를 바로 사용한다.
  • GC (Garbage Collector) : 더 이상 참조되지 않는 객체를 정리한다.

자바 프로그램 작성 및 실행 과정

자바 코드를 작성 후 실행하려면 다음 과정을 거친다.

  1. Hello.java 작성

     class Hello {
         public static void main(String[] args) {
             System.out.println("Hello, Java");
         }
     }
    
  2. 컴파일

    컴파일이란 사람이 이해하는 언어(원시코드)를 컴퓨터가 이해할 수 있는 언어(목적코드)로 바꾸어 주는 과정으로, 자바 컴파일러는 jvm을 위한 바이트코드를 생성한다.

     javac Hello.java
    

    자바 컴파일러를 이용하여 컴파일 하는 방법으로, 위 명령어를 실행하면 Hello.class 클래스 파일이 생성된다.

    • 생성과정

      1. IDE 사용시 Ctrl + s, 즉 저장시 ssd 메모리에 java 파일이 저장된다.
      2. 컴파일러를 RAM에 올린다 (SSD 보다 빠르기 때문에)
      3. 컴파일러가 java 파일을 읽어 레지스터에 전달한다.
      4. 레지스터가 java 파일을 한줄 씩 cpu에게 전달하면서 해석 과정을 거쳐 클래스 파일이 생성된다. 생성된 클래스 파일을 컴파일러에게 다시 전달한다.
      5. 전달 받은 클래스 파일을 다시 SSD 메모리에 저장한다.
    • 클래스 파일 확인 방법

        javap -c Hello.java
      

      → 자바 클래스 파일 안에 들어있는 것이 바이트코드이다.

  3. 실행 (런타임)

java Hello
Hello, Java.

마지막으로 자바 인터프리터 (java.exe)로 실행하면 위에서 작성한 예제를 실행한 결과를 볼 수 있다.

바이트코드

특정 하드웨어가 아닌 가상 컴퓨터에서 돌아가는 실행 프로그램을 위한 이진 표현법

자바 바이트코드 (Java Bytecode)

.java 파일은 사람이 이해할 수 있는 언어로 작성했기 때문에 컴퓨터는 이해할 수 없으므로 번역이 필요하다.

컴퓨터가 이해할 수 있는형태로 번역하는 것은 jvm이 담당

그러면 우리는 jvm이 이해할 수 있는 형태로 번역을 해서 전달해야 한다 이 형태가 자바 바이트코드이다.

  • 자바 바이트코드란 자바 가상 머신이 이해할 수 있는 언어로 변환된 자바 소스코드
  • 자바 컴파일러에 의해 변환되는 코드의 명령어 크기가 1바이트라 자바 바이트코드라고 불림
  • 자바 바이트코드는 jvm만 설치되어 있으면 어떤 운영체제에서도 실행 가능

and Methods in the JVM

JVM은 인스턴스와 클래스 객체를 초기화하는데 독특한 메소드를 사용한다.

  • <init>
  • <clinit>

Instance Initialization Methods ()

간단하게 객체를 만들고 인스턴스화를 해보자.

Object obj = new Object();

위 코드를 컴파일 하고, 바이트 코드를 보면

0: new           #2          // class java/lang/Object
3: dup
4: invokespecial #1          // Method java/lang/Object."<init>":()V
7: astore_1

객체를 초기화하기 위해서 JVM은 <init> 이라는 특별한 메소드를 호출하는 것을 볼 수 있다.

이 메소드는 인스턴스를 초기화 하는 메소드인데,

if and only if : (무조건 다음 조건이 모두 충족되어야 <init> 메소드가 호출됨)

  • it is defined in a class
  • its name is
  • it returns void

각 클래스는 인스턴스 초기화 메소드를 갖고있지 않거나 여러개 갖고있을 수 있다. 이 메소드는 java와 kotlin같은 jvm 기반의 프로그래밍 언어에서 생성자에 해당된다.

자바 컴파일러가 생성자를 어떻게 <init> 하는지 더 잘 이해하기 위해 다른 예제를 보면,

public class Person {

	private String firstName = "Foo"; 	// <init>
	private String lastName = "Bar";	// <init>

	// <init>
	{
		System.out.println("Initializing ...");
	}

	// <init>
	public Person(String firstName, String lastName) {
		this.firstName = firstName;
		this.lastName = lastName;
	}

	// <init>
	public Person() {
	}
}

이 코드의 바이트 코드를 보면,

public Person(java.lang.String, java.lang.String);
  Code:
     0: aload_0
     1: invokespecial #1                  // Method java/lang/Object."<init>":()V
     4: aload_0
     5: ldc           #2                  // String Foo
     7: putfield      #3                  // Field firstName:Ljava/lang/String;
    10: aload_0
    11: ldc           #4                  // String Bar
    13: putfield      #5                  // Field lastName:Ljava/lang/String;
    16: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
    19: ldc           #7                  // String Initializing ...
    21: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    24: aload_0
    25: aload_1
    26: putfield      #3                  // Field firstName:Ljava/lang/String;
    29: aload_0
    30: aload_2
    31: putfield      #5                  // Field lastName:Ljava/lang/String;
    34: return

자바코드에서는 생성자(Person())와 initializer block({ } )이 분리되어 있지만, 바이트코드상에서는 같은 인스턴스 초기화 메소드에 있는 것을 볼 수 있다.

  • index 0 ~ 13 : 첫번째, firstNamelastName 필드를 초기화했다.
  • index 16~21 : 그다음 초기화 블럭 안에 코딩된 println 함수를 실행
  • 마지막으로 위에서 초기화한 필드에 생성자 파라미터에 있는 값으로 업데이트(할당) 했다.

만약 Person 객체를 만든다면,

Person person = new Person("younghee", "yesol");
	Code:
	 0: new           #2      // class Person
	 3: dup
	 4: ldc           #3      // String younghee
	 6: ldc           #4      // String yesol
	 8: invokespecial #5      // Method Person."<init>":(Ljava/lang/String;Ljava/lang/String;)V
	11: astore_1

이 때 jvm이 java 생성자와 상응하는 signature(파라미터로 넘긴 값) 를 가진 <init> 메소드를 호출한다.

💡 JVM 세계에서는 생성자와 다른 인스턴스 초기화자가 메소드로 동일하다.

Class Initialization Methods ()

자바에서 클래스초기화 블럭(static initializer blocks) 은 클래스 레벨의 것들을 초기화 할 때 사용한다.

```java
public class Person {

    private static final Logger LOGGER = LoggerFactory.getLogger(Person.class);

    static {
        System.out.println("Static Initializing...");
    }

    // ...
}
```
static {};
	Code:
	   0: getstatic     #2    // Field java/lang/System.out:Ljava/io/PrintStream;
	   3: ldc           #3    // String Static Initializing...
	   5: invokevirtual #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
	   8: return

바이트코드를 보면 컴파일러는 static block을 클래스 초기화 메소드 (class initialization method)로 번역하는 것을 볼 수 있다.

💡 메소드는 static 필드와 static 초기화블럭 일때만 호출된다.

JVM은 해당 클래스를 처음 사용할 때 <clinit> 메소드를 호출한다. 그러므로, <clinit> 은 런타임에 호출이 일어난다. 그래서 우리는 바이트코드에서 invocation 이 일어나는것을 볼 수 없다.

정리하자면, <init>

  • 인스턴스가 만들어 지기 전
    • 기본 생성자 인스턴스초기화자 <init> 으로 선언만 바이트코드에서 볼 수 있고,
  • 인스턴스가 만들어 지면 (new Person("이름", "이름");)
    • 넘겨진 파라미터를 가진 <init> 메소드를 호출하는 것을 볼 수 있다.

<clinit> 은 해당 클래스를 처음 사용할 때 런타임에 호출이 일어나기때문에 바이트코드에서 호출되는 부분은 볼 수 없다.


Reference

the-java-code-manipulation