[JAVA] JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가.

SungBum Park·2021년 3월 7일
0

JAVA

목록 보기
1/9
post-thumbnail

JVM이란 무엇인가


`

JVM(Java Virtual Machine)은 자바 가상 머신으로, 자바로 작성된 프로그램을 실행하기 위해서는 반드시 필요하다. 위 그림과 같이 일반 프로그램은 운영체제 바로 위에서 동작하므로 운영체제가 변경되면 그에 맞게 프로그램도 재컴파일을 해야 한다. 반면에 자바는 JVM 위에서 동작하므로 운영체제가 변경된다고 해도 재컴파일할 필요가 없다. 이를 플랫폼 독립적이라고 한다.

하지만 JVM은 운영체제 위에서 동작하므로 해당 운영체제에 맞는 JVM이 필요하다. 따라서 JVM은 플랫폼 종속적이다.

JVM은 자바 언어 자체를 이해하고 있지는 않다. 자바는 컴파일 결과로 바이트 코드로 구성된 .class 파일을 만들며, JVM은 이 바이트 코드를 해당 OS에 특화된 기계어 코드로 변경하여 실행한다.

현재 JVM은 자바 이외에도 여러 언어에 사용된다. 위에서도 말했듯이 바이트 코드로만 만들 수 있다면 어떤 언어든지 JVM 위에서 동작시킬 수 있다. 현재는 클로저, 그루비, JRuby, Jython, Kotlin, Scala 등을 사용할 수 있다.

자바 컴파일 방법


자바로 작성된 소스 파일(.java)을 컴파일하면 바이트 코드로 변환된 .class 파일이 생성된다. 이 바이트 코드를 JVM 에서 동작시킨다.

실제로 터미널에서 자바 프로그램을 작성하고 컴파일을 수행해보자. (기본적으로 JDK가 설치되어있고, 환경변수가 설정되어 있어야 한다.)

.java 파일 만들기

vi HelloWorld.java
public class HelloWorld {
        public static void main(String[] args) {
                System.out.println("Hello World!");
        }
}

자바 컴파일 하기

javac HelloWorld.java

javac 명령어는 .java 파일을 컴파일하는 명령어로 결과로 .class 파일을 반환한다.

여기서 javap 명령어로 해당 .class 파일의 바이트코드를 실제로 살펴볼 수 있다.

javap -c HelloWorld.class

Compiled from "HelloWorld.java"
public class HelloWorld {
  public HelloWorld();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String Hello World!
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}
  • -c : 코드 역어셈블
  • 여러 옵션을 통해 더 자세한 정보를 볼 수 있다.

자바 실행 방법


java HelloWorld

Hello World!

바이트코드란 무엇인가


바이트코드는 JVM이 이해할 수 있는 코드이며, 각 명령어는 1byte 크기의 opcode와 추가 피연산자로 이루어져있다. 바이트코드는 컴퓨터가 이해할 수 있는 언어가 아니므로 반기계어라고도 한다.

바이너리 코드(Binary Code)

컴퓨터가 이해할 수 있는 언어는 바이너리 코드(이진 코드)이다. 바이너리 코드는 0과 1로 이루어진 코드로 각 1bit 크기를 가진다. 대표적으로 C언어로 구성된 소스 파일(.c)이 컴파일 결과로 오브젝트 파일(.obj)을 반환하는데 이가 바이너리 코드로 이루어져 있다.

JIT 컴파일러


JVM에서 바이트 코드를 실행하기 위해서는 바이트 코드를 컴퓨터가 이해할 수 있는 기계어로 컴파일하는 과정이 필요하다.(javac로 컴파일하는 것이 아니므로 헷갈리면 안된다.) JVM은 이러한 컴파일 과정에서 두 가지 방법을 수행한다.

  • Interpretor(인터프리터): 바이트 코드를 한 줄씩 실행한다.
  • JIT(Just In Time) 컴파일러

인터프리터로만 바이트코드를 실행하기에는 속도가 너무 느리다. 한 줄씩 읽는다는 것은 중복을 전혀 생각하지 않는다는 단점이 있다. 이를 해결한 것이 JIT 컴파일러이다.

JIT 컴파일러는 전체 바이트 코드를 한 번에 읽어 기계어로 변환하여 가지고 있는다. 따라서 인터프리터는 반복된 바이트코드가 있어도 이미 JIT 컴파일러가 변환한 기계어를 재활용할 수 있기 때문에 성능적으로 이점을 얻을 수 있다.

주의할 점은 JIT 컴파일러가 있다고 해서 인터프리터를 사용하지 않는 것은 아니다. JIT 컴파일러는 인터프리터를 보조해주는 역할을 수행한다.

JVM 구성요소


출처: 더 자바, 코드를 조작하는 다양한 방법

JVM은 크게 5가지로 구성된다.

  • 클래스 로더(Class Loader)
  • 메모리(런타임 데이터 영역, Runtime Data Area)
  • 실행 엔진(Execution Engine)
  • JNI(네이티브 메소드 인터페이스)
  • 네이티브 메소드 라이브러리

클래스 로더 시스템

클래스 로더 시스템에서는 .class 파일에서 바이트코드를 읽고 메모리(런타임 데이터 영역)에 저장하는 역할을 한다.

출처: 더 자바, 코드를 조작하는 다양한 방법

클래스 로더 시스템은 크게 3가지 과정을 수행한다.

로딩(loading)

로딩은 클래스 로더가 .class 파일을 읽고 그 내용에 따라 적절한 바이너리 데이터를 만들고 이를 메소드 영역(메모리 내부의 메소드 영역)에 저장하는 과정을 말한다.

이 때 메소드 영역에 저장되는 데이터는 다음과 같다.

  • FQCN
  • 클래스, 인터페이스, enum
  • 메소드와 변수

로딩의 과정은 위 그림과 같이 3가지로 이루어져 있다.

  1. BootStrap ClassLoader : 최상위 클래스 로더이며, JAVA_HOME/lib/rt.jar 위치에 있는 JDK 클래스 파일(코어 자바 API)을 로딩한다. 이는 네이티브 C 언어로 구현되어 있으며, Java 9 버전부터 rt.jar 가 없어지면서 범위가 축소되었다.
  2. Extension ClassLoader : JAVA_HOME/lib/ext 또는 java.ext.dirs 환경 변수로 지정된 위치에 있는 클래스 파일을 로딩한다. Java 9 버전 이후 Platform ClassLoader 로 명칭이 변경되었으며, JCP에 표준화된 모듈 클래스를 볼 수 있다.
  3. Application ClassLoader : classpath 또는 JAR 파일 안에 있는 Manifest 파일의 class-path 속성값으로 지정된 위치에 있는 클래스를 로딩한다.(애플리케이션이 실행할 때 주는 -classpath 옵션 또는 java.class.path 환경 변수의 값에 해당하는 위치) 개발자가 애플리케이션 실행을 위해 직접 작성한 대부분의 클래스가 이 클래스 로더에 의해 로딩된다. Java 9 버전 이후로 System ClassLoader 로 변경되었으며, classpath, modulepath 에 있는 클래스를 로딩한다.

위 과정은 1번 부터 순서대로 동작하며, 3번에서도 찾지 못한 클래스는 ClassNotFoundException이 발생한다.

링크(linking)

링크 과정은 Verify, Prepare, Resolve(optional) 세 단계로 나뉜다.

  • Verify(검증): .class 파일 형식이 유효한지 검사한다.
  • Preparation: 클래스 변수(static 변수)와 기본 값에 필요한 메모리를 준비하는 과정이다.
  • Resolve: 심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체한다.
    • 심볼릭 메모리 레퍼런스: 실제 레퍼런스가 아닌 논리적인 레퍼런스
      • 필드로 static이 아닌 객체를 선언할 때 링크 단계에서 이 객체는 심볼릭 메모리 레퍼런스이다.

초기화

클래스 로더의 마지막 과정으로, static 변수 또는 static block 내부의 값을 기본값으로 할당한다.

메모리(런타임 데이터 영역)

  • 스택 영역: 쓰레드마다 런타임 스택을 만들고, 그 안에서 메소드 호출을 스택 프레임이라는 이름의 블럭으로 쌓는다. 쓰레드가 종료되면 해당 런타임 스택도 사라진다.
  • PC(Program Counter) 레지스터: 쓰레드마다 현재 실행할 스택 프레임을 가리키는 포인터가 생성되며, 이를 저장하는 공간이다.
  • 네이티브 메소드 스택: 네이티브 메소드가 호출될 때 쌓이는 스택 공간이다.(쓰레드 단위)
  • 힙 영역: 객체를 저장하는 공간이며, 모든 쓰레드가 공유하는 공유 자원이다.
    • new로 런타임에 생성되는 객체 또는 static 객체 및 String 관련 자원이 저장되는 공간이다.
    • GC를 통해 메모리 관리가 이루어진다.
  • 메소드 영역: 클래스 수준의 정보(클래스 이름, 부모 클래스 이름, 메소드, 변수 등)를 저장하는 공간이며, 모든 쓰레드가 공유하는 공유 자원이다.

참고 자료: https://javapapers.com/core-java/java-jvm-run-time-data-areas/#Program_Counter_PC_Register

실행 엔진

실행 엔진은 바이트코드를 실행하기 위한 로직을 수행하는 단계이다.

  • 바이트코드 실행: 인터프리터 + JIT 컴파일러
  • 메모리 관리: GC(Garbage Collector)

JNI(Java Native Interface)

JNI는 자바 네이티브 코드로 작성된 함수를 사용할 수 있는 방법을 제공한다. 자바 네이티브 코드는 자바가 아닌 C, C++, 어셈블리어로 작성된 코드를 자바 애플리케이션에서 사용하도록 구현한 코드이며, native 키워드를 사용한다.

Java Native에 대한 자세한 설명과 실제 사용 예제

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

자바 네이티브 코드로 작성된 라이브러리를 말한다.

JDK와 JRE의 차이


출처: 더 자바, 코드를 조작하는 다양한 방법

JRE(Java Runtime Environment)

JRE는 자바 애플리케이션을 실행할 수 있도록 구성된 배포판으로, JVM + 라이브러리로 이루어져있다.

JVM과 핵심 라이브러리 및 자바 런타임 환경에서 사용하는 프로퍼티 세팅이나 리소스 파일을 포함하고 있다. 바이트코드를 실제로 실행하기 위한 실제 코드들의 집합이므로, JRE만 있으면 자바 프로그램을 실행시킬 수 있다.

JDK(Java Development Kit)

JDK는 JRE + 개발 툴로 이루어져있으며, 자바 애플리케이션을 실행시킬 수 있는 것에 더해 자바를 개발하기 위한 여러 도구들을 제공한다.

오라클은 Java 11 버전부터 JDK만을 제공하며, JRE를 따로 제공하지는 않는다. 하지만 Java 9 버전부터 JRE를 직접 모듈화를 할 수 있는 기능인 jlink를 제공하므로 실질적으로 JRE의 필요성이 없어졌다.

자바 유료화

자바 유료화는 실제로 JDK의 유료화를 말한다. 오라클은 Oracle JDK 11 버전부터 상용으로 사용할 때 그에 따른 비용이 발생한다.(두 조건이 모두 충족되야 유료이다.)

JDK가 유료이므로, JVM 위에서 동작하는 코틀린과 같은 모든 언어가 위 버전의 JDK를 상용으로 사용한다면 돈을 지불해야 한다.

자바 챌린저들의 자바 유료화에 대한 생각 정리 글

참고자료


profile
https://parker1609.github.io/ 블로그 이전

0개의 댓글