목표

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


sun cloud


지난 글(Java 실행 과정 - Yong’s devlog)에서 개발자가 작성한 소스 코드를 컴파일러가 바이트 코드로 변환하고, JVM이 변환된 바이트 코드를 해석하고 실행된다고 알아본 적 있습니다. 하지만 JVM과 바이트코드, JDK, JRE 등의 내용에 대해 다루지는 않아서 이번 글을 통해 알아보도록 하겠습니다.

참고로 글의 주제는 백기선님께서 유튜브로 진행하셨던 자바 기초 스터디(Issues · whiteship/live-study · GitHub)에서 다룬 내용을 기준으로 하고 있습니다.


JVM이란?

JVM이란 Java Virtual Machine 의 약자로 자바 코드를 실행하는 스택 기반의 가상 머신을 뜻합니다. 자바가 가진 Write once, run anywhere라는 슬로건이 바로 JVM의 가장 큰 목적을 나타내는 것이 아닐까 싶은데, 이에 대한 설명을 돕기 위해 JVM의 설명에 앞서 자바에 대해 간단히 알아보겠습니다.

자바 언어는 1991년 제임스 고슬링(James Gosling)에 의해 만들어졌습니다. 초기에는 가전제품에 탑재될 소프트웨어를 만들려고 했는데, C++로는 목적을 이루기에 부족함을 느껴 새로운 언어를 개발하기에 이르렀다고 합니다. 특히나, C나 C++와 같은 언어는 컴파일러가 운영체제에 종속적이었기 때문에 한 운영체제에 맞게 개발된 프로그램을 다른 종류의 운영체제에 적용하기 위해 많은 노력이 필요했습니다.

이러한 점을 보완하기 위해 자바에서는 운영체제에 독립적이도록 자바 프로그램과 운영체제 사이에 JVM이 중개자 역할을 하게 됩니다. JVM으로 인해 운영체제에 관계 없이 플랫폼에 독립적인 개발이 가능해진 것 입니다.(이번 글에서는 자바 언어 자체의 특징 - 객체지향언어, 메모리 관리 등 - 에 대해서는 다루지 않고 JVM에 집중하도록 하겠습니다.)

image

(출처: The Java Programming Environment - Saloni Goyal)

그리고 JVM은 자바 코드 실행과, 중개자 역할 이 외에 메모리 관리(Garbage Collection)를 수행합니다. 이 부분에 대해서는 컴파일 과정과 JVM의 구성 요소 등을 살펴본 후에 자세히 살펴보도록 하겠습니다.



컴파일 방법

JVM이 자바 코드를 실행하기 위해 우선 필요한 작업이 컴파일입니다. 자바 컴파일러(javac.exe)가 컴파일을 수행하여 .java파일을 .class파일(바이트코드)로 변환해주게 됩니다. 그리고 변환된 바이트코드를 JVM이 읽어들여 실행하게 되는 방식입니다.


그렇다면 컴파일은 어떻게 이뤄질까요?

컴파일은 보통 빌드가 되는 과정에서 이뤄지고, IDE(eclipse, IntelliJ 등)는 자동 빌드 기능도 제공을 해주기에 개발자가 직접 컴파일 할 일이 드물 것이라 생각됩니다.

컴파일은 컴파일러(javac.exe)가 수행한다고 말씀드렸는데, 사실 IDE를 사용할 때에도 내부적으로 컴파일러를 실행시켜서 컴파일 과정을 거치게 됩니다.


우선 IDE를 사용한 컴파일 결과를 보도록 하겠습니다.

1.프로그램 작성

image

image


2.프로그램 빌드 후 디렉토리 확인(out/productions/compileTest/ 아래 위치)

image


IntelliJ를 사용해서 빌드 후 확인해본 결과 디렉토리가 새롭게 생기며 .class 파일이 생성된 것을 확인할 수 있었습니다.



이번에는 같은 소스코드 파일을 바탕화면에 옮긴 후 명령 프롬프트(cmd)로 컴파일 해보겠습니다.

javac 명령어로 소스코드를 컴파일 할 수 있습니다.

image

위와 같이 정상적으로 컴파일이 되는 것을 확인할 수 있습니다.



실행 방법

컴파일된 바이트코드를 실행하는 방법은 간단합니다. java 명령어를 사용합니다. 단, 여기서 확장자는 입력하지 않고 파일명만 입력하도록 합니다.

image



바이트코드란?

컴파일러를 사용해서 소스코드를 바이트코드로 변환하는 과정을 살펴봤습니다.

그렇다면 바이트코드는 무엇일까요?

글의 서두에서 JVM이 바이트 코드를 읽어들여 번역하고 실행한다고 말씀드린 바 있습니다. 이처럼 JVM이 이해할 수 있는 언어로 번역된 코드로, 위키백과의 정의를 찾아보면 다음과 같습니다.

바이트코드(Bytecode, portable code, p-code)는 특정 하드웨어가 아닌 가상 컴퓨터에서 돌아가는 실행 프로그램을 위한 이진 표현법이다.

특정 하드웨어가 아닌, 가상 컴퓨터에 돌아가도록 하기 때문에 이식성이 뛰어난 것이고, 자바에서는 바이트코드를 JVM의 JIT 컴파일러가 기계 코드로 번역해서 실행하게 됩니다.



JIT Compiler?

JIT(Just-In-Time) 컴파일러는 런타임 시 바이트코드를 기계코드로 컴파일해 자바 애플리케이션의 성능을 향상시키는 런타임 환경의 컴포넌트입니다.

JIT 컴파일러의 사용 이전의 자바를 보면, 컴파일 언어에 비해 이식성이 뛰어나지만 속도가 느리다는 평가가 많았습니다. 컴파일 언어는 처음 컴파일 과정에서 시간이 소요되지만, 그 이후에는 되풀이해서 번역할 필요가 없기에 인터프리터 언어에 비해 속도 빠를 수 밖에 없었고, 자바는 컴파일 언어와 인터프리터 언어의 절충안을 찾고자 시도하게 됩니다.

간단히 이해해보자면, 인터프리터 방식으로 실행하지만 효율적인 작업을 위해 캐싱 기능처럼 런타임 중에 바이트코드를 컴파일합니다. 그러면 컴파일 된 메소드의 호출 시 해석 없이 바로 호출이 가능하게 됩니다. 즉, JIT 컴파일러는 실행 중에 자바 애플리케이션에서 자주 사용되는 바이트코드 순서에 대해 동적으로 시스템 코드를 생성하게 됩니다.

image

(Sun Java에서 1.1.5 이상 버전부터 Symantec JIT 컴파일러를 도입하기 시작했으며, Symantec JIT 컴파일러의 문제점을 보완한 Hotspot JIT 컴파일러가 1.2.2에서 플러그인으로 추가되었고, 1.3부터 JIT 컴파일러가 기본적으로 활성화되었습니다.)



JVM 구성요소

JVM은 크게 ClassLoader, JVM memory, Execution engine, Native method interface, Native method library 5가지로 이루어져 있습니다.

image

(출처: [Is Java a Compiled or Interpreted Language? Baeldung](https://www.baeldung.com/java-compiled-interpreted))


1. ClassLoader

앞에서도 몇 번 언급되었는데, 클래스 로더는 런타임 시 동적으로 클래스(바이트코드)를 JVM 메모리 위로 올리는 역할을 합니다. 로딩, 링킹, 초기화를 수행합니다.

  • Loading

    • .class (바이트코드) 파일을 읽어 바이너리 데이터를 생성하고 메소드 영역에 저장

    • 로딩 후 해당 Class 타입의 객체 생성하여 힙 메모리에 저장

    • 3 종류의 클래스 로더가 있으며, Bootstrap -> Extension -> Application 순서로 클래스를 찾는다. Bootstrap부터 시작해 상위 클래스로 올라가는데, 보통 Application에 존재. 찾지 못 할 경우 java.lang.ClassNotFoundException 발생

  • Linking

    • Verify: 바이트코드 유효성 검사
    • Prepare: 클래스 변수(static 변수), 기본값에 필요한 메모리 준비
    • Resolve: 심볼릭 레퍼런스를 메소드 영역의 실제 레퍼런스로 교체
  • Initialization

    • Linking - Prepare 단계에서 확보한 메모리 영역에 클래스의 정적 변수 초기화


2. JVM memory(= Runtime Data Area)

JVM 메모리 구조는 총 5가지 영역으로 나뉘며, 스레드 별로 생성되는 Stack area, PC Register, Native method satcks가 있으며, 모든 스레드가 공유하는 Method area, Heap area가 있습니다.

image

  1. Method area: 메소드 영역에는 모든 클래스 레벨의 정보(클래스 이름, 부모 클래스 이름, Runtime Constant Pool, 메서드 및 변수 정보 등)가 저장됩니다. JVM마다 하나의 메소드 영역이 존재하며, 모든 스레드가 공유하는 자원입니다. JVM 시작 시 생성되며, 프로그램 종료 시까지 존재합니다.

  2. Heap area: 객체의 모든 정보가 힙 영역에 저장됩니다. 메소드 영역과 마찬가지로 JVM마다 하나의 힙 영역이 존재하며, 모든 스레드가 공유합니다.

  3. Stack area: 스레드마다 별도의 런타임 스택이 하나씩 생성되며, 스택에 쌓이는 블록들을 activation record/stack frame이라고 부르며 메소드 호출을 저장합니다. 스레드 종료 시 해당 스택은 JVM에 의해 파괴됩니다.

  4. PC Registers: 스레드의 현재 실행 명령어의 주소를 저장합니다. 스레드마다 개별 PC 레지스터를 가집니다.

  5. Native method stacks: 모든 스레드가 별도의 Native method stack을 가지며, 기본 메소드 정보를 저장합니다. 자바 바이트 코드가 아닌 다른 언어로 작성된(C, C++) 네이티브 코드를 위한 스택입니다.


3. Execution engine

클래스로더를 통해 JVM 메모리 영역에 배치된 바이트코드를 실행합니다. 바이트코드를 한 줄씩 읽고, 다양한 메모리 영역에 존재하는 데이터와 정보를 사용해 명령을 실행합니다. 실행 엔진은 3가지 영역으로 나뉘게 됩니다.

  • Interpreter: 바이트코드를 한 줄씩 해석해서 실행합니다. 여러 번 호출되어도 같은 작업을 수행합니다.

  • Just-In-Time Compiler: 전체 바이트코드를 컴파일하여 반복되는 메소드 호출로 인한 재해석 작업을 막아 효율성을 높입니다.

  • Garbage Collector: 참조되지 않는 객체를 삭제합니다. 메모리 관리의 핵심 기능을 담당하며, 추후 관련 내용을 다시 다룰 예정입니다.


4. Native method interface

Native method library와 상호작용하며 실행에 필요한 네이티브 라이브러리(C/C++)를 제공합니다. JVM이 C/C++ 라이브러리를 호출할 수 있도록 도와주며, 하드웨어에 특정 C/C++ 라이브러리에 의한 호출이 가능하게 합니다.


5. Native method library

실행 엔진에 필요한 네이티브 라이브러리(C, C++) 컬렉션입니다.



JDK와 JRE

image

(출처: Differences between JDK, JRE and JVM - GeeksforGeeks)

1. JDK

JDK(Java Development Kit)는 자바 애플리케이션 및 자바 애플릿 개발에 사용되는 소프트웨어 개발 도구를 말합니다. 자바 런타임 환경(JRE), 인터프리터/로더, 컴파일러(javac), 아카이버(jar), 문서 생성기(Javadoc) 및 자바 개발에 필요한 기타 도구가 포함됩니다.

운영 체제(예: 맥, 유닉스, 윈도우)마다 별도의 설치 프로그램이 필요합니다.

2. JRE

JRE(Java Runtime Environment)는 컴퓨터에서 자바 프로그램을 실행할 수 있는 환경을 제공하는 자바 실행 환경(설치 패키지)를 말합니다. JVM의 실행 환경을 제공하며 JDK에 포함됩니다.


정리하자면 JDK는 JRE와 컴파일러 등이 포함된 개발 도구를 말하며, JRE는 JDK와 라이브러리 파일 등을 가지는 실행 환경을 말합니다.




정리

JVM, 컴파일과 실행방법, 바이트코드, JIT Compiler, JVM 구성요소, JDK와 JRE에 대해 알아봤습니다.

자바 프로그램을 사용하면서도 막상 자바가 어떻게 동작하는 지에 대해 큰 고민을 하지 않았던 것 같습니다.

이번에 자바 소스 파일을 JVM으로 실행하는 과정에 대해 알아보며, 실행에 필요한 구성 요소들과 그 역할들을 알 수 있었습니다.


배운 내용을 아주 간략하게 정리해보겠습니다.

  • JVM은 플랫폼에 독립적일 수 있도록 컴파일 된 바이트코드를 메모리에 올려 번역 후 실행한다.(이 외에 GC가 메모리 관리도 수행)

  • javac 명령어로 컴파일러(javac.exe)가 소스코드를 JVM이 이해할 수 있는 바이트코드로 변환하게 하며, JVM의 클래스로더가 바이트코드를 JVM memory에 올린다.(로딩-연결-초기화)

  • 바이트코드는 실행 엔진에 의해 실행되며 처음에는 인터프리터 방식으로 번역되어 실행되지만, 런타임에 JIT compiler가 여러 번 호출되는 메소드들을 컴파일하는 방식으로 효율적으로 실행된다.

  • JVM은 클래스 로더, JVM memory, 실행 엔진, Native method interface 그리고 Native method library로 구성되며 JVM memory는 스레드가 공유하는 Method area, Heap area와 스레드 별로 생성되는 Stack area, PC registers, Native Method 영역으로 구성된다.

  • JVM과 라이브러리들을 포함한 개발 환경을 JRE라 하고, JRE와 컴파일러 등을 포함한 개발 도구를 JDK라고 한다.



참고

docs.oracle.com

자바 (프로그래밍 언어) - 위키백과, 우리 모두의 백과사전

바이트코드 - 위키백과, 우리 모두의 백과사전

history - When did Java get a JIT compiler? - Stack Overflow

Java Language => JIT (Just in Time) 컴파일러

https://velog.io/@youngerjesus/자바-JIT-컴파일러

Is Java a Compiled or Interpreted Language? Baeldung

How JVM Works - JVM Architecture? - GeeksforGeeks

JVM, JRE, JDK가 뭔가요? - YouTube

JVM vs. JRE vs. JDK: What's the Difference? IBM


맨 위로 이동하기

Java 카테고리 내 다른 글 보러가기

댓글남기기