해당 글을 백기선 님의 자바 스터디 1주차 과제를 공부하고 공유하기 위해서 작성되었습니다.

목표

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

학습할 것

연관되어 있는 내용끼리 묶어서 작성하였습니다.

JVM이란 무엇인가

JVM이란?

JVM은 Java virtual machine을 줄인 것으로 자바를 실행하기 위한 가상 컴퓨터이다.

자바로 작성된 애플리케이션은 모두 JVM에서만 실행되기 때문에, 자바 애플리케이션을 실행하기 위해서는 JVM이 반드시 필요하다.

Write once, run anywhere.

일반 애플리케이션은 OS와 바로 맞붙어 있어서 OS에 종속적이다. 다른 OS에서 실행시키기 위해서는 해당 OS에 맞게 변경해야한다.

반면에 Java 애플리케이션은 OS가 아닌 JVM하고만 상호작용을 하기 때문에, OS와 하드웨어에 독립적이다. 단, JVM은 OS에 종속적이기 때문에 해당 OS에서 실행가능한 JVM이 필요하다.

JVM은 바이트 코드를 이해하는 것이지 자바 코드를 이해하는 것이 아니다. 코틀린 또한 코틀린 코드를 바이트 코드로 컴파일해서 JVM 위에서 동작한다.

JVM의 특성

  • 스택 기반의 가상 머신
  • 단일 상속 형태의 객체 지향 프로그래밍을 가상 머신 수준에서 구현
  • 포인터를 지원. 단, C와 같이 주소 값을 임의로 조작이 가능한 포인터 연산은 불가능
  • Garbage collection 수행
  • 플랫폼의 독립성 보장
  • Data Flow Analysis에 기반한 자바 바이트코드 검증기를 통해 문제를 실행 전에 검증하여 실행 시 안전을 보장하고 별도의 부담을 줄여줌

바이트코드란 무엇인가

바이너리 코드

CPU가 이해하기 위한 기계어는 0과 1로 구성된 바이너리 코드(이진 코드)이다. 기계어가 이진 코드로 이루어졌을 뿐 모든 이진 코드가 기계어인 것은 아니다.

바이너리 코드 != 기계어

바이트 코드

0과 1로 이루어진 이진 코드이지만 바이너리 코드와 달리 가상머신이 이해할 수 있는 코드이다. 사람에게 친숙한 고급 언어보다는 덜 추상적이지만 기계어보다는 추상적이다.

고급언어로 작성된 코드를 가상머신이 이해할 수 있도록 컴파일한 것이다. CPU에게 넘어가기 전에 실시간 번역기 또는 JIT(just-in-time) 컴파일러에 의해 바이너리 코드로 변환된다.

정리

Java는 OS와 직접적으로 대화할 수 없다. 오로지 JVM하고만 상호작용을 한다. 자바는 JVM을 거쳐야만 OS와 대화할 수 있다.

바이너리 코드와 바이트 코드 둘 다 0과 1로 이루어져 있다. 바이너리 코드는 CPU가 이해할 수 있는 언어, 바이트 코드는 가상 머신이 이해할 수 있는 언어이다.

그 중에 JVM을 위한 바이트 코드를 “자바 바이트코드”라고 한다.

JVM 구성 요소

JVM은 크게 네 가지 구성요소로 볼 수 있다.

jvm 구성 요소

Class Loader(클래스 로더)

자바 클래스로더는 자바 클래스를 JVM으로 동적 로드하는 JRE(자바 런타임 환경)의 일부이다. 클래스 파일을 로드하는데 사용되는 하위 시스템이다.
Runtime 시 동적으로 클래스를 로드하며, jar 파일 내에 저장된 클래스들을 JVM 위에 탑재하고 사용하지 않는 클래스들은 메모리에서 삭제한다.
변환된 바이트 코드 파일(.class)을 JVM이 운영체제로부터 할당 받은 메모리 영역인 Runtime Data Area로 “적재”하는 역할을 한다.

Execution Engine(실행 엔진)

클래스 로더가 JVM 내의 런타임 데이터 영역에 바이트 코드를 배치시키면 실행엔진에 의해 실행된다. 자바 바이트 코드를 JVM 내부에서 컴퓨터가 실행할 수 있는 형태인 바이너리 코드로 변경한다.
변경하는 방식은 두가지가 있는데, 인터프리터 방식과 JIT 방식이 있다.

  • 인터프리터 방식
    기본 바이트 코드를 실행하는 방법은 인터프리터 방식이 기본이다. 자바 바이트 코드를 명령어 단위로 읽어서 실행하기 때문에 느리다.
  • JIT(just-in-time)
    실행 시점에 인터프리터 방식으로 기계어 코드를 생성하면서 그 코드를 캐싱한다. 전체 컴파일 후 캐싱하고 이후 변경된 부분만 컴파일하고 나머지는 캐시에서 가져다 쓴다.

GC(Garbage Collector)

GC를 수행하는 모듈이다. 더이상 사용되지 않는 오브젝트, 메모리를 정리하는 역할이다.

Runtime Data Area

프로그램을 수행하기 위해 OS에서 할당받은 메모리 공간이다.

  • Method Area
    Class Loader에 의해 적재된 클래스 파일(메타데이터, 상수 런타임 풀, 메서드 등)을 저장한다. 다른 스레드에서도 활용 가능한 공유자원이다.

  • Native Method Stack
    일반적인 메소드를 실행하는 경우 JVM Language Stack에 적재되지만, 네이티브 메소드 스택은 네이티브 라이브러리에 따라 네이티브 코드 명령(C언어와 같이 네이티브 방식으로 작성된 메소드)을 보관한다.

  • PC Register
    Thread가 시작될 때 생성되는 공간으로 Thread마다 하나씩 존재한다. Thread가 어떤 부분을 어떤 명령으로 실행해야 할 지에 대한 기록을 하는 공간이다.
    현재 실행하고 있는 부분(Java 가상 시스템 명령)의 주소를 가지고 있다.

  • Stack
    프로그램 실행과정에서 임시로 할당되었다가 메소드를 빠져나가면 바로 소멸되는 특성의 데이터를 저장하기 위한 영역이다. 각 스레드에는 자체 JVM 스택이 있고, 스레드가 생성될 때 동시에 생성된다.
    각종 형태의 변수나 임시 데이터, 스레드나 메소드의 정보를 저장하고 호출된 메서드의 매개변수, 지역변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장한다.

  • Heap
    new 연산자로 생성된 객체와 배열을 저장하는 메모리 공간이다. 모든 개체, 관련 인스턴스 변수 및 배열은 힙에 저장된다. Method Area에서 클래스 정보를 복사하여 Heap 영역에서 메모리를 할당하여 사용한다.
    이 메모리는 여러 스레드에 걸쳐 공유된다. 또한, GC의 주 대상이 된다.

JIT 컴파일러란 무엇이며 어떻게 동작하는지

JVM에서 OS로 넘어가기 전에 바이트 코드를 바이너리 코드로 변경해야 한다. 처음에는 인터프리터 방식을 사용하였는데 속도가 느리다보니 이를 해결하기 위해 나온 것이 JIT이다.
JIT는 Just In Time의 약자로 JIT 컴파일러는 프로그램이 실행되는 시점에서 컴파일을 시행하는데, 캐시를 보관하여 사용하기 때문에 한 번 컴파일된 코드는 빠른 수행이 가능하다.

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

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

정리

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

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

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

JVM 내부에서는 자바 컴파일러가 자바 프로그램 코드를 바이트 코드로 변환시킨 후 실제 바이트 코드가 실행하는 시점에서 JIT 컴파일러를 통해 기계어로 변환한다.

컴파일 하는 방법

컴파일이란?

컴파일러는 특정 프로그래밍 언어로 쓰여 있는 문서를 다른 프로그래밍 언어로 옮기는 프로그램을 말한다. 기존 문서를 소스 코드 혹은 원시 코드라고 부르고, 출력된 문서를 목적 코드라고 부른다.
원시 코드에서 목적 코드로 옮기는 과정을 컴파일이라고 한다.

자바 컴파일 과정

  1. 소스 파일 생성한다. (Hello.java)
  2. 자바 컴파일러(javac.exe)를 사용하여 컴파일한다. $ javac Hello.java
  3. 컴파일이 정상적으로 완료되면 클래스 파일 생성된다. (Hello.class)

실행하는 방법

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

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

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

JDK와 JRE의 차이

JDK란?

JDK는 Java Development Kit으로 자바 프로그래밍 시 필요한 컴파일러 등을 포함한다. JDK는 JRE를 포함하며, 개발을 위해 필요한 도구(java, javac 등)를 포함한다.

JRE란?

JRE는 Java Runtime Enviroment로 컴파일된 자바 프로그램을 실행시킬 수 있는 자바 환경을 말한다. JRE는 JVM의 실행환경을 구현했다고 볼 수 있으며, JVM이 자바 프로그램을 동작시킬 때 필요한 라이브러리 파일들과 기타 파일들을 가지고 있다.

JRE JDK 차이


Reference