BackEnd/Java

[Java] JVM 구조

샤아이인 2022. 5. 19.

 

1. JVM, JDK, JRE

이번에는 JVM, JRE, JDK의 차이점에 대하여 알아보자.

1-1) JVM (Java Virtual Machine)

자바 가상 머신으로 자바 바이트 코드(.class 파일)OS에 특화된 코드로 변환(인터프리터와 JIT 컴파일러)하여 실행하는 역할을 합니다.

 

그럼 자바 바이트 코드는 무엇일까?

우리는 기본적으로 Hello.java 와 같이 java 파일을 만든다. 이를 컴파일한것이 바이트 코드이다.

Hello.java

이를 다음 명령을 통해서 compile 해보자.

javac Hello.java

 

실행 결과를 보면 compile된 class 파일을 확인할 수 있다.

이를 JVM을 통해서 OS가 이해할 수 있는(특정 플랫폼에 종속적인) Native code로 변경하는 것이다.

 

JVM은 표준 스펙이 정해져 있고, 이를 구현하는 여러 밴더에 따라 구현체가 달라진다.

예를 들어 벤더로는 오라클, Azul 등이 있다.

 

요약해보면 JVM은 2가지 기본 기능이 있습니다.

  1. 자바 프로그램이 어느 기기, 어느 운영체제 상에서도 실행될 수 있게 만들어 줍니다. => WORA
  2. 자바 프로그램의 메모리를 효율적으로 관리 & 최적화해 줍니다.

 

다만 JVM은 혼자서 배포되지 않아요, 여러 라이브러리를 추가한 JRE가 최소 배포 단위입니다!

 

1-2) JRE (Java Runtime Environment)

JRE는 자바 클래스 라이브러리(Java class libraries), 자바 가상 머신(JVM) 을 포함하고 있습니다.

 

JRE는 자바 어플리케이션을 실행하는데 필요한 기초적인것들이 포함되어있다.

자바를 개발하는데 필요한것은 제공되지 않는다.

 

1-3) JDK (Java Development Kit)

일반적으로 우리가 자바를 공부하기 위해 설치하는 것이 바로 JDK입니다. (자바 어플리케이션을 실행할수 있는 JRE + 개발에 필요한 툴)

JDK를 설치하면 JRE가 자동으로 설치됩니다. JDK는 JRE를 포함하고 있고, JRE는 JVM을 포함하고 있습니다.

따라서 JDK를 설치하면 JRE, JVM이 자동으로 다 설치됩니다.

 

또한 JDK에는 JRE에는 없는 "자바 컴파일러(javac, java compiler)"를 포함하고 있습니다.

컴파일러란 우리가 작성한 자바 문법을 컴퓨터가 이해할 수 있게 바꿔주는 해석기 인데, 실제로 .java 파일을 만들어서 실행(빌드)하면 컴파일 작업을 거쳐 .class 라는 바이트 코드 파일이 자동으로 생성됩니다.

 

자바 11부터는 JDK만 제공하며 JRE를 따로 제공하지 않는다.

 

2. JVM 구조

JVM의 구조에 대하여 알아보자!

 

2-1) 클래스 로더 시스템

  • .class 에서 바이트코드를 읽고 메모리에 저장한다.
  • 로딩: 클래스 읽어오는 과정
  • 링크: 레퍼런스를 연결하는 과정
  • 초기화: static 값들 초기화 및 변수에 할당

 

2-2) 메모리 영역 (Runtime Data Areas)

우선 Heap, Method 영역은 전체에 공유되는 영역이다.

  • Method Area 에는 클래스 수준의 정보(클래스 이름, 부모 클래스 이름, 메소드, 변수)를 저장한다. 이는 공유 자원이다. 즉 우리가 작성한 코드의 정보는 이곳에 저장된다,
  • Heap Area 에는 객체를 저장, 공유하는 자원이다.

 

나머지 Stack, PC Registers, Native Method Stackthread별로 할당되는 자원이다.

  • stack 영역에는 thread마다 런타임 스택을 만들고, 그 안에 메소드호출을 스택 프레임이라 부르는 블럭으로 쌓는다. thread를 종료하면 런타임 스택도 사라진다.
  • PC(Program Counter) 레지스터: 쓰레드 마다 쓰레드 내 현재 실행할 instruction의 위치를 가리키는 포인터가 생성된다.
  • Native Method 는 메서드에 Native 라는 키워드가 추가되어있고, 그 구현을 Java가 아닌, C++, C# 으로 구현한것을 말한다. 대표적으로 currentThread가 있다.

메서드에 native 키워드가 추가되어있다.

이렇게 구현되어있는 메서드들이 있는 라이브러리를 Native Method Library라고 부른다.

이러한 라이브러리는 항상 JNI(Native Method Interface)를 통해서만 사용할수 있다. 

 

2-3) 실행 엔진 (Excution Engine)

  • 인터프리터 : 컴파일된 바이트 코드를 한줄 한줄 읽어서 머신이 이해할 수 있는 native 코드로 변경해 준다. (한줄씩 실행)
  • JIT 컴파일러 : 인터프리터가 해석하면서 반복되는 코드가 있다면, 기존에 네이티브 코드로 변환해둔 코드로 미리 변경해준다. 그러면 인터프리터가 한줄한줄 읽다가 네이티브 코드로 이미 변경된 부분을 다시 해석하지 않게 되어 빠르다.
  • GC(Garbage Collector): 더이상 참조되지 않는 객체를 모아서 정리한다.

 

3. 클래스 로더

클래스 로더의 전반적인 구조는 다음과 같다.

출처 - 백기선 더 자바 강의

3-1) Loading

클래스 로딩은 동적으로 일어난다. 즉 필요한 순간에 로딩하게 된다.

우선 클래스 로더는 바이트코드인 .class 파일을 읽어 이를 바이너리 데이터로 만들고, 이를 Method Area에 저장해둔다.

 

이때 저장해두는 class 정보로는

1) FQCN (Fully Qualified Class Name): 클래스가 속한 페키지 명을 포함하는 full name

2) 클래스의 메서드와 변수

3) class, interface, enum

 

이후 로딩이 끝나면 해당 Type의 Class 객체를 생성해서 "Heap"에 저장한다.

 

클래스로더의 종류를 알아보자.

  • 부트 스트랩 클래스로더 : JAVA_HOME\lib에 있는 코어 자바 API를 제공한다. 최상위 우선순위를 가진 클래스 로더
  • 플랫폼 클래스로더(익스텐션 클래스 로더): JAVA_HOME\lib\ext 폴더 또는 java.ext.dirs 시스템 변수에 해당하는 위치에 있는 클래스를 읽는다.
  • 애플리케이션 클래스로더 : 애플리케이션 클래스패스(애플리케이션 실행할 때 주는 -classpath 옵션 또는 java.class.path 환경 변수의 값에 해당하는 위치)에서 클래스를 읽는다. 따라서 대부분의 우리가 만든 클래스는 여기서 로딩된다.

애플리케이션 클래스 로더의 부모가 플랫폼 클래스로더이고, 플랫폼 클래스로더 의 부모가 부트 스트랩 클래스로더 이다.

이를 확인해보기 위해 다음과 같이 코드를 작성해보자.

public class ShopApplication {

	public static void main(String[] args) {
		ClassLoader classLoader = Item.class.getClassLoader();
		System.out.println("classLoader = " + classLoader);
		System.out.println("classLoader = " + classLoader.getParent());
		System.out.println("classLoader = " + classLoader.getParent().getParent());
	}

}

출력 결과는 다음과 같다.

다만 Bootstrap ClassLoader는 네이티브 코드로 작성되어 있어서 확인이 불가능 하다.

 

ClassLoader의 코드를 까보면 다음과 같이 되어있다.

static {
    ArchivedClassLoaders archivedClassLoaders = ArchivedClassLoaders.get();
    if (archivedClassLoaders != null) {
        // assert VM.getSavedProperty("jdk.boot.class.path.append") == null
        BOOT_LOADER = (BootClassLoader) archivedClassLoaders.bootLoader();
        setArchivedServicesCatalog(BOOT_LOADER);
        PLATFORM_LOADER = (PlatformClassLoader) archivedClassLoaders.platformLoader();
        setArchivedServicesCatalog(PLATFORM_LOADER);
    } else {
        // -Xbootclasspath/a or -javaagent with Boot-Class-Path attribute
        String append = VM.getSavedProperty("jdk.boot.class.path.append");
        URLClassPath ucp = (append != null && !append.isEmpty())
                ? new URLClassPath(append, true)
                : null;
        BOOT_LOADER = new BootClassLoader(ucp);
        PLATFORM_LOADER = new PlatformClassLoader(BOOT_LOADER);
    }
    // A class path is required when no initial module is specified.
    // In this case the class path defaults to "", meaning the current
    // working directory.  When an initial module is specified, on the
    // contrary, we drop this historic interpretation of the empty
    // string and instead treat it as unspecified.
    String cp = System.getProperty("java.class.path");
    if (cp == null || cp.isEmpty()) {
        String initialModuleName = System.getProperty("jdk.module.main");
        cp = (initialModuleName == null) ? "" : null;
    }
    URLClassPath ucp = new URLClassPath(cp, false);
    if (archivedClassLoaders != null) {
        APP_LOADER = (AppClassLoader) archivedClassLoaders.appLoader();
        setArchivedServicesCatalog(APP_LOADER);
        APP_LOADER.setClassPath(ucp);
    } else {
        APP_LOADER = new AppClassLoader(PLATFORM_LOADER, ucp);
        ArchivedClassLoaders.archive();
    }
}

대략 코드를 살펴보면,

맨 위에있는 BootStrap Loader를 통해 먼저 class 들을 읽고, 여기서 읽지 못하는 class를 다음 PLATFORM Loader가 일고, 여기서도 못읽으면 APP Loader가 읽게 돤다.

 

여기서도 못찾으면 ClasssNotFoundException이 발생하게 된다.

 

3-2) Linking

Verify, Prepare, Reolve(optional) 세 단계로 진행된다.

  • Verify: .class 파일 형식이 유효한지 확인한다.
  • Preparation: 클래스 변수(static 변수)와 기본값에 필요한 메모리를 준비하는 과정이다. 이때 정적 변수를 기본값으로 초기화 한다. 그럼 왜 이때 기본값으로 초기화 할까? 이는 변수에 필요한 메모리 공간을 확보하는 과정이다.
  • Resolve: 심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체한다. 이는 선택적이라 이때 변경될수도 있고, 나중에 사용할때 변경될수도 있다.

심볼릭 레퍼런스는 무엇일까?

Book book = new Book();

위와 같은 코드는 실제 레퍼런스가 아니다. 논리적인 레퍼런스 이다.

이를 실제 Heap에 있는 Book 객체를 가리키도록 하는 과정이다.

 

3-3) Initialization

위 Linking 과정에서 메모리 공간을 확보해 두었다.

이제 초기화 과정에서 먼저 Static 변수의 값을 할당한다. 그 다음으로 static 블럭을 실행한다.

'BackEnd > Java' 카테고리의 다른 글

[Java] Reflection  (0) 2022.05.22
[Java] 바이트코드 조작하기  (0) 2022.05.20
[Java] equals, hashCode 를 같이 구현하는 이유  (0) 2022.05.08
[Java] Java 에서의 Thread, Light Weight Process  (0) 2022.03.30
[Java] Exception 기초  (0) 2022.02.23

댓글