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

8 minute read

백기선님, 자바스터디 주제에 따라 학습한 내용입니다.

목표

자바 소스 파일(.java)을 JVM으로 실행하는 과정 이해하기.


JVM이란 무엇인가

JVM (Java Virtual Machine)

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

  • 바이트 코드를 실행하는 표준(JVM 자체는 표준)이자 구현체(특정 밴더가 구현한 JVM)다.

  • JVM은 OS가 ByteCode를 이해할 수 있도록 해석해 주는 역할을 함

    • 따라서 JVM은 c언어 같은 네이티브 언어에 비해 속도가 느렸지만 JIT(Just In Time)컴파일러 구현을 통해이점을 극복
  • 특정 플랫폼에 종속적

    JAVA가 특정 OS에 독립적 (즉, 어떤 운영체제에서든 잘 녹아들 수 있는)인 이유가, JVM에 있다. 자바는 어찌됬든 JVM과 서로 대화를 하는건데, JVM은 자바 언어를 특정 OS에 맞게 변경을 해줘야 하기 때문에, 특정 플랫폼(특정 OS)에 종속적이게 된다. 그리고 이를 기계어로 변환해 주는게 JVM의 InterPreter와 JIT 이고, JVM을 통해 자바가 특정 OS나 플랫폼에 독립적일 수 있다는 거다.

  • Oracle 이라는 회사가 JVM의 스펙과 표준을 정의하면, 공급업체들이 JVM을 실제 그래픽카드처럼 실물을 만들어서 배포하게 된다. 이 공급업체를 Vendor 라고 한다.

    img

    [사진 출처] : https://catch-me-java.tistory.com/11?category=438116

JVM의 특성

  • 스택 기반의 가상 머신

    대다수의 명령어가 스택 선두에서 피연산자를 택하고 결과는 다시 스택에 넣는다. 스택 상의 피연산자 타입을 구분하고 명령어에 스택 상의 피연산자 타입을 기술

  • 단일 상속 형태의 객체 지향 프로그래밍을 가상 머신 수준에서 구현

  • 포인터를 지원. 단, C와 같이 주소 값을 임의로 조작이 가능한 포인터 연산은 불가능

  • Garbage collection 수행

  • 플랫폼의 독립성 보장

  • Data Flow Analysis에 기반한 자바 바이트코드 검증기를 통해 문제를 실행 전에 검증하여 실행 시 안전을 보장하고 별도의 부담을 줄여줌

JDK와 JRE의 차이

JRE (Java Runtime Environment): JVM + 라이브러리

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

JDK (Java Development Kit): JRE + 개발 툴

  • JRE + 개발에 필요한 툴(java, javac 등) = JRE의 슈퍼셋

    Java 응용프로그램 개발, 디버깅 및 모니터링을 위한 개발 도구와 함께 JRE가 가진 모든 것을 포함하고 있다

  • 그리고 컴파일러 (javaC) 와 자바 어플리케이션 런처, Appletiviewer 등을 포함하고 있다.

    • 컴파일러 : 자바 코드를 바이트 코드(JVM이 읽을 수 있는 언어)로 변경해준다.
    • 자바 어플리케이션 런처 : JRE를 실행시키는데 필요한 클래스나 메인 메서드를 로딩한다.

JDK, JRE, JVM의 차이점

JRE = Java 애플리케이션을 실행할 JVM + 라이브러리.

JDK = JRE + Java Application을 개발하기 위한 도구.

  • JDK
    • 코드를 쓰는 Java 애플리케이션 개발자라면 기계에 JDK를 설치해야 한다.
    • 터미널에서 javac 로 java파일을 컴파일 하는건 jdk에 포함
  • JRE
    • Java에 내장된 응용 프로그램만 실행하려면 컴퓨터에 JRE만 설치하면 된다.
    • 컴파일된 코드(.class)를 읽는건 jre 의 java 명령어
JDK JRE JVM
jdk는 개발자 키트로, 개발에 필요한 어플리케이션 소프트웨어이다. 소프트웨어 번들로, jre는 자바 클래스와 라이브러리, 필수적인 컴포넌트들을 제공한다. jvm은 바이트코드를 실행하고, 실행에 필요한 환경을 제공한다.
jdk는 플랫폼에 독립적이다. jre는 플랫폼에 독립적이다. jvm은 플랫폼에 종속적이다.

JDK vs JRE vs JVMimg

JRE JDK 차이

자바

  • 프로그래밍 언어

  • JDK에 들어있는 자바 컴파일러(javac)를 사용하여 바이트코드(.class 파일)로 컴파일 할 수 있다.

  • 자바 유료화? 오라클에서 만든 Oracle JDK 11 버전부터 상용으로 사용할 때 유료

JVM 언어

  • JVM 기반으로 동작하는 프로그래밍 언어
  • 클로저, 그루비, JRuby, Jython, Kotlin, Scala, …

  • 이 언어들은 무조건 실행하기 전에 컴파일 과정에서, 바이트 코드로 번역되는 과정을 가진다.

컴파일 하는 방법

컴파일이란?

컴퓨터 (OS)가 알아 들을 수 있게 하기 위해 기계어를 번역하는 과정

자바 컴파일 과정

(비교) C언어 컴파일 실행 과정

img

img

자바의 컴파일 과정은 총 4가지에서 5가지의 과정이 있다.

img

  1. 어휘 분석

    public, class 와 같은 키워드, “Hello World”같은 리터럴, 그리고 연산자인 + 와 같은 오퍼레이터들을 수집한다. 이런 키워드, 연산자, 리터럴을 ‘어휘소’ 라고 한다.

    어휘소를 모두 수집해서 토큰 스트림 이라는 하나의 스트림으로 만드는 과정

  2. 구문 분석 (Syntax Analysis)

    토큰 스트림을 통해서, 문법에 맞는지 확인하는 과정

    구문 분석을 하는데 문법이 틀렸다거나 하는 상황에 컴파일 과정에서 Syntax error 가 발생한다.

  3. 의미 분석

    의미가 맞는지를 검사 - 타입 검사 , 자동 타입 변환 etc…

    ex) String 에 integer값을 입력

  4. 중간 코드(바이트 코드)

    JVM 이 읽을 수 있는 언어. JVM에서는 이 중간 코드를 바이트코드라고 한다.

    JVM은 이 바이트코드를 읽어 들여서 컴퓨터 이해할 수 있는 언어로 변환한다.

바이트코드란 무엇인가

[JDK] 자바 컴파일 과정을 통해 중간 코드(바이트 코드)가 만들어 진다.

중간 코드란 JVM 이 읽을 수 있는 언어로 JVM에서는 이 중간 코드를 바이트코드라고 한다.

[JRE]를 사용해서 바이트코드를 JAVA명령어로 실행

[JVM - InterPreter/ JIT]은 이 바이트코드를 읽어 들여서 기계어(컴퓨터 이해할 수 있는 언어)로 변환한다.

바이트 코드로 할 수 있는 일?

  • 프로그램 분석
    • 코드에서 버그 찾는 툴
    • 코드 복잡도 계산
  • 클래스 파일 생성
    • 프록시
    • 특정 API 호출 접근 제한
    • 스칼라 같은 언어의 컴파일러 만들기

실행하는 방법

class 파일을 자바 인터프리터(java.exe)로 실행한다. $ java Hello (실행 시에는 확장자를 붙이지 않는다.)

내부적인 진행순서는 다음과 같다.

  1. 프로그램의 실행에 필요한 클래스(*.class파일)을 로드한다.
  2. 클래스파일을 검사한다.(파일형식, 악성코드 체크)
  3. 지정된 클래스(Hello)에서 main(String[] args)을 호출한다.

img

JDK를 통해서 바이트 코드를 생성하고 JRE에게 던지게 된다. JRE를 사용해서 바이트코드를 JAVA명령어로 실행하게 되는데, JVM이 ‘실행’단계를 걸친다.

JVM 구성 요소

JVM 의 구성요소는 크게 3가지로 구성 되어있다.

  1. 클래스 로더 시스템 (Class Loader)
  2. 메모리 (Jvm Memory)
  3. 실행 엔진 (Execution Engine)

image

image-20210731205636105

사진 출처

Class Loader

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

Runtime Data Areas (메모리)

  • Heap 과 Method는 모든 쓰레드가 공유 나머지는 쓰레드 마다 생성
  • JVM이 프로그램을 수행하기 위해 OS로 부터 별도로 할당받은 메모리 공간

PC Register

  • CPU가 명령을 수행하는 동한 필요한 정보를 저장
  • PC레지스터는 현재 실행중인 메서드가 네이티브가 아니면, 현재 실행중인 JVM 명령어 위치에 저장되고, 네이티브이면 PC레지스터에 저장되는 값은 정의되지 않는다. CPU의 Register와는 다르게, 연산을 위해 필요한 피연산자를 임시로 저장하기 위한 용도로 사용한다.

JVM Stack

  • Thread가 시작될 때 생성되며 Method와 Method 정보 저장

  • 쓰레드마다 런타임 스택을 만들고, 그 안에 메소드 호출을 스택프레임이라 부르는 블럭으로 쌓는다.

    메서드 호출이 정상 완료되거나 예외가 던져지면 스택프레임은 스택에서 빠지면서 소멸된다.

    쓰레드 종료하면 런타임 스택도 사라진다.

  • Java 이외의 언어로 작성된 native 코드를 위한 Stack(JNI)

Method Area

  • 모든 쓰레드가 공유하는 메모리 영역

  • 클래스 수준의 정보 (클래스명, 부모 클래스명 인터페이스, 메소드, 필드, Static 변수등의 바이트 코드 등을 저장

Heap

  • 런타임시 동적으로 할당하여 사용하는 영역 class를 통해 instance를 생성하면 Heap에 저장됨

  • Heap의 경우 명시적으로 만든 class와 암묵적인 static 클래스(.class 파일의 class)가 담긴다.
  • 또한 암묵적인 static 클래스의 경우 클래스 로딩 시 class 타입의 인스턴스를 만들어 힙에 저장한다. 이는 Reflection에 등장한다.

Execution Engine

  • Load된 Class의 ByteCode를 실행하는 Runtime Module
  • Class Loader를 통해 JVM 내의 Runtime Data Areas에 배치된 바이트 코드는 Execution Engine에 의해 실행(바이트 코드를 명령어 단위로 읽어서 실행)
  • 인터프리터: 바이크 코드를 한줄 씩 실행.
  • JIT 컴파일러: 인터프리터 효율을 높이기 위해, 인터프리터가 반복되는 코드를 발견하면 JIT 컴파일러로 반복되는 코드를 모두 네이티브 코드로 바꿔둔다. 그 다음부터 인터프리터는 네이티브 코드로 컴파일된 코드를 바로 사용한다.
  • GC(Garbage Collector): 더이상 참조되지 않는 객체를 모아서 정리한다.

JNI(Java Native Interface)

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

클래스 로더 시스템

image

  • 클래스를 읽어오는 시스템 구조로 로딩, 링크, 초기화 순으로 진행된다.
  • 로딩
    • 클래스 로더가 .class 파일을 읽고 그 내용에 따라 적절한 바이너리 데이터를 만들고 “메소드” 영역에 저장
      • 클래스가 없을 경우, 이 시점에서 “Class Not Found Exception”이 발생
    • 이때 메소드 영역에 저장하는 데이터
      • FQCN(Fully Qualified Class Name)
      • 클래스, 인터페이스, ENUM 인지를 저장
      • 메서드와 변수
    • 로딩이 끝나면 해당 클래스 타입의 Class 객체를 생성하여 “힙” 영역에 저장
  • 링크
    • Verify, Prepare, Resolve(Optional) 세 단계로 나눠져 있다.
    • Verify(검증): .class 파일 형식이 유효한지 체크한다.
    • Preparation: 클래스 변수(static 변수)의 기본값에 따라 필요한 메모리
    • Resolve: 심볼릭 메모리 레퍼런스를 메서드 영역에 있는 실제 레퍼런스로 교체한다.
  • 초기화
    • Static 변수의 값을 할당한다.(static 블럭이 있다면 이때 실행된다.)
  • 클래스 로더는 계층 구조로 이뤄져 있으면 기본적으로 세가지 클래스 로더가 제공된다.
    • 부트 스트랩: JAVA_HOME/lib에 있는 코어 자바 API를 제공한다. 최상위 우선순위를 가진 클래스 로더
    • 플랫폼: JAVA_HOME/lib/ext 폴더 또는 java.ext.dirs 시스템 변수에 해당하는 위치에 있는 클래스를 읽는다.
    • 애플리케이션: 앱 ClassPath(앱을 실행 할 때 주는 -classpath 옵션 또는 java.class.path 환경변수의 값에 해당하는 위치)에서 클래스를 읽는다.

JIT(Just In Time) 컴파일러란

자바는 인터프리터를 하는 과정 전에, 바이트 코드로 컴파일 과정을 한번 거친다는 특징이 있어서 성능에 대한 이슈가 있었다.

img

[출처] : 자바 vs python

자바는 바이트코드로 한번 컴파일 하는 과정과, 바이트코드를 인터프리터 하는 방식 2가지를 진행하기 때문인데, 가뜩이나 인터프리터 방식은 소스코드를 런타임시에 한줄 한줄 읽어 들여야 하는 방식 때문에, 컴파일 방식보다 느린건 만연한 사실이다.

(1) 컴파일 방식 : 소스코드를 한꺼번에 컴퓨터가 읽을 수 있는 native machine (기계)어로 변환

(2) 인터프리터 방식 : 소스코드를 빌드 시에 아무것도 하지 않다가, 런타임시에 한줄 한줄 읽어가며 변환

둘 다 장단점이 있는 방식이지만, 여기서 중요한건 자바는 컴파일과 인터프리터 방식을 모두 사용한 다는 것이다.

JIT컴파일러는 이미 한번 읽어서 기계어로 변경한 소스코드는 저장소에 저장을 하고, 다시 번역하지 않는다.

img

반복되는 코드를 모두 컴파일러로 컴파일 시켜서, 인터프리터가 안읽어도 된다고 생각되는 부분은 컴파일된 코드를 바로 사용할 수 있도록 인터프리터의 역학을 보조한다.

img

JIT는 Just In Time의 약자로 JIT 컴파일러는 프로그램이 실행되는 시점에서 컴파일을 시행하는데, 캐시를 보관하여 사용하기 때문에 한 번 컴파일된 코드는 빠른 수행이 가능하다.

인터프리터 방식으로 실행되다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고 이후에는 네이티브 코드로 직접 실행하는 방식이다.

JIT 컴파일러는 실행 엔진(Execution Engine) 내부에서 동작한다.

정리

JVM 구성 요소는 다음과 같다.

  1. 클래스 로더 컴파일러가 내부에 만든 .class(바이트 코드)를 런타임 데이터 공간에 “적재”한다.
  2. 런타임 데이터 공간 OS로부터 메모리를 할당받은 공간으로 스택, 힙, 메소드, 네이티브 메소드, PC 레지스터가 있다.
  3. GC 메모리를 정리하는 역할이다.
  4. 실행 엔진 인터프리터 방식 또는 JIT 컴파일러를 이용하여 데이터 영역에 배치된 바이트 코드를 실행한다.

JIT 컴파일러는 바이트 코드를 바이너리 코드로 변환하는 속도가 느린 인터프리터 방식을 보완하기 위해 나온 것이다. 인터프리터 방식으로 기계어 코드를 생성하면서 그 코드를 캐싱하여, 같은 함수가 여러 번 불릴 때 매번 기계어 코드를 생성하는 것을 방지한다.

JVM 내부에서는 자바 컴파일러가 자바 프로그램 코드를 바이트 코드로 변환시킨 후 실제 바이트 코드가 실행하는 시점에서 JIT 컴파일러를 통해 바이트 코드를 여러가지 다양한 테크닉을 사영하여 JVM 해석 엔진 없이 바로 수행되는 기계어 코드를 만들어 낸다. 그럼으로서 바이트코드가 가지는 장점과 기계어가 가지는 장점을 결합할 수 있다.

참고

  • JIT 컴파일러: https://aboullaite.me/understanding-jit-compiler-just-in-time-compiler/
  • JDK, JRE 그리고 JVM: https://howtodoinjava.com/java/basics/jdk-jre-jvm/
  • https://en.wikipedia.org/wiki/List_of_JVM_languages

JDK, JRE, JVM과 관련된 인터뷰 질문

  1. JVM 아키텍처란?

  2. Java에는 클래스 로더의 종류가 몇 가지인가?

    3개의 클래스 로더가 있다. 부트스트랩, 확장 및 애플리케이션 클래스 로더.

  3. Java에서 클래스 로더는 어떻게 작동하나?

    클래스 로더는 미리 정의된 위치에서 jar 파일 및 클래스를 검사한다. 그들은 경로에 있는 모든 클래스 파일을 스캔하고 필요한 클래스를 찾는다. 발견된 경우 클래스 파일을 로드, 링크 및 초기화한다.

  4. JRE와 JVM의 차이점?

    JVM은 Java 애플리케이션을 실행하는 런타임 환경의 규격이다. Hotspot JVM은 그러한 규격의 구현이다. 클래스 파일을 로딩하고 인터프리터와 JIT 컴파일러를 사용하여 바이트코드를 기계 코드로 변환하여 실행한다.

  5. interpreter와 JIT 컴파일러의 차이점?

    interpreter는 바이트코드를 선별로 해석하여 순차적으로 실행한다. 실적이 저조하다. JIT 컴파일러는 블록의 코드를 분석하여 이 프로세스에 최적화를 추가한 후 보다 최적화된 머신 코드를 준비한다.