0. JVM 기초와 자바의 컴파일러 및 인터프리터에 관하여

2025. 5. 14. 19:46·Java
728x90

JDK, JRE, JVM

JDK(Java Development Kit)는 Java 환경에서 돌아가는 프로그램을 개발하기 위해 필요한 툴을 모아놓은 소프트웨어 개발 키트(SDK)이다.

 

JRE(Java Runtime Environment)는 Java가 실행되기 위한 환경을 제공한다. JVM과 프로덕션 환경에서 제공되는 모든 클래스 라이브러리 및 국제화나 IDL 라이브러리와 같이 개발자들에게 도움이 되는 라이브러리로 구성된다.

 

JVM(Java Virtual Machine)은 Java 애플리케이션이 구동되기 위한 기반이 되는 가상 머신이다. Java는 OS 위에서 구동되지 않으며, JVM이라는 가상 머신 위에서 실행된다.

https://www.edureka.co/blog/java-architecture/

 

 

플랫폼 독립적 (Platform Independent)

기존 기계어가 하드웨어 종속적으로 컴파일 되는 반면, 자바는 바이트 코드(중간 언어) 형식으로 컴파일 되어 JVM이 설치된 모든 곳에서 OS와 하드웨어에 종속적이지 않게 동작한다.

 

이런 의미에서 자바는 플랫폼 독립적이다.

 

그러나 JVM 자체는 OS, 하드웨어마다 다르게 설치할 필요가 있으므로 플랫폼 종속적이다.

 

자바의 처음 슬로건은 Write Once, Read Anywhere (WORA) 였다.

  • 여기에는 한 가지 함정이 있는데, 네이티브 코드를 사용하기 위해 JNI를 거치는 순간 WORA는 실패하고 만다…

 

Java 애플리케이션 구동 과정

  1. 클래스 로딩
    • Class Loader가 바이트 코드(.class 파일)를 로딩
  2. 메모리 할당
    • Runtime Data Area에 프로그램 구동에 필요한 메모리 할당
  3. 코드 실행
    • Executor Engine이 코드 실행
      • 인터프리터로 바이트 코드 해석
      • 혹은 JIT Compiler로 변환된 기계어 실행
        • 런타임에 자주 실행된 코드에 한하여 (= 핫스팟)

 

Managed Code와 Unmanaged Code

매니지드 코드

  • Java와 같이 OS가 아닌 VM을 사용해 구동되며, 메모리 관리가 VM에 의해서 자동적으로 수행(GC)되는 프로그래밍 언어
    • Java의 가상 머신을 JVM이라고 한다.
  • OS가 아닌 VM 위에서 실행되기 때문에, 운영체제의 모든 기능을 직접적으로 이용하기 힘들다.
    • ex) 하드웨어 자원에 접근 및 이용
    • Java는 운영체제 기능 사용을 위하여 JNI를 통해 네이티브 메서드를 활용함
    • ⇒ System 클래스를 통해 시스템 콜 사용할 때 일부 메서드에서 네이티브 메서드 활용

 

언매니지드 코드

  • C, C++ 등과 같이 OS를 사용해 구동되며, 프로그래머가 직접 메모리를 관리해야하는 프로그래밍 언어
  • 네이티브 코드라고도 부른다.
  • 언매니지드 코드(네이티브 코드)를 작성하기 위한 언어 자체(C, C++)는 플랫폼 종속적이지않다. 하지만 이를 실행하기 위해서 컴파일을 거쳐 기계어로 변환한 결과물인 실행 파일은 하드웨어마다 다른 명령어를 사용하기 때문에, 언매니지드 코드는 플랫폼 종속적이다.

 

프로그래밍 언어론 기초

이 쯤에서 기본적인 용어들을 정리해보자.

 

Source File : 개발자가 작성하는 고수준 언어인 소스 코드로 구성된 파일

  • ex) .java, .c, .py

Object File : 소스 파일을 컴파일해서 생긴 파일

  • ex) byte code, binary code

Byte Code (바이트 코드) : JVM이 이해할 수 있는 언어

  • JVM에서 실행가능한 기계어로, 이에 대응하는 어셈블리어도 존재한다.
  • JVM만 설치되어 있다면, 바이트 코드는 어떤 OS에서든 실행될 수 있다.

Native Code (네이티브 코드) : Java에서 부모가 되는 C언어나 C++, 어셈블리어로 구성된 코드를 의미한다.

  • 때에 따라서 네이티브 코드라는 용어를 machine code라는 의미로 쓰기도 하고, umanaged code라는 의미로 쓰기도 한다.
  • Native Code ≒ Machine Code : 하드웨어 아키텍쳐에 종속적(플랫폼 종속적)인 코드라는 의미로 쓰이는 경우.
  • Native Code ≒ Unmanaged Code : 메모리 자원의 직접 관리가 필요한 코드라는 의미로 쓰이는 경우.

Binary Code (바이너리 코드) : 이진수(0과 1)로 구성된 CPU가 이해할 수 있는 언어

저수준 언어 : 사람이 이해할 수 있는 고수준 언어에 반대되는 기계가 이해할 수 있는 언어이다.

  • 기계어와 어셈블리어는 유이한 저수준 언어이다.
  • 저수준 언어들은 컴퓨터 구조에 따라 달라질 수 있다.
  1. 기계어 (Machine Code) : 바이너리 코드로 이루어진 CPU 명령어 집합
    • 기계어는 컴퓨터 구조(hardware architecture, 머신 machine)에 종속적이다.
    • 머신마다 다른 명령어 셋(instruction sets)을 가지기 때문이다.
  2. 어셈블리어 : 기계어와 1 : 1로 대응되는 저수준 언어로, 실행을 위해 어셈블 과정을 거쳐 기계어로 변환 후 실행된다.
    • 기계어와 달리 사람이 읽을 수 있다. 저급 언어로 분류되지만, 기계어와 고급 언어 중간 수준에 있기 때문에 중간 언어라고 부르기도 한다.
    • operator operand1 operand2 명령어 구조를 가진다.
      • 피연산자로 상수, 레지스터, 메모리가 올 수 있다.

 

인터프리트 언어와 컴파일 언어의 속도 차이

개발 환경 속도 : 인터프리트 언어 > 컴파일 언어

  • 인터프리트 언어는 필요한 부분만 실행 가능하므로, 빠른 피드백
  • 컴파일 언어는 수정을 할 때마다 매번 새롭게 컴파일을 해야한다. 반면 인터프리트 언어는 컴파일이 필요 없으므로, 컴파일 언어에 비해서 비교적 수정이 간단하다는 장점이 있다

실행 속도 : 컴파일 언어 > 인터프리트 언어

  • 매 실행 시마다 소스 코드 해석이 필요하므로 대규모 프로그램이나 복잡한 연산 수행 시 성능 저하 발생

 

자바 바이트 코드는 인터프리트 방식으로 동작하지만, 다음과 같은 속도 차이가 있다.

  • 순수 컴파일 언어(제일 빠름) > 바이트 코드 > 순수 인터프리트 언어

 

자바의 컴파일 과정

컴파일러 언어에서 컴파일(compile) 은 보통 소스 코드 → 기계어로의 변환 과정을 의미한다.

하지만 자바의 경우 기본적으로 소스 코드 → 바이트 코드로 컴파일하여 바이트 코드를 인터프리팅 하는 방식으로 동작하며, 추가적으로 JIT 컴파일러를 사용해 인터프리트 언어의 단점을 보완한다.

 

먼저 자바의 컴파일 및 인터프리트 과정을 알아보자.

 

Java Compiler (javac, 자바 컴파일러) : 컴파일 타임 동작

  • 소스 코드(고수준 언어) → 바이트 코드(중간 수준 언어) 변환
  • 산출물인 바이트 코드는 일반적으로 .class 형식의 클래스 파일이다.
    • .class 파일과 라이브러리, 리소스, 메타 데이터파일들을 패키징화 한 것이 바로 .jar, .war와 같은 패키징 파일이다. (일종의 압축파일)
      • JAR(Java ARchive) : Executable Jar를 보통 JAR라고 부른다. 외부 의존성을 포함하며 따라서 내장 톰캣을 가지고 있다. JRE만 있으면 실행 가능한 애플리케이션. 스프링부트 표준
        • 스프링, 스프링 부트 이전의 Plain JAR의 경우, 라이브러리 및 외부 의존성을 포함하지 않는다. 오직 프로젝트 소스 코드(클래스 + 리소스)만을 패키징하고있다.
      • WAR(Web Application Archive) : 별도의 웹 서버 필요. 서블릿, JSP 컨테이너에 배포 가능한 파일 형식이다. META-INF, WEB-INF와 같이 고정된 리소스 디렉토리 구조를 가진다.

 

Java Interpreter(자바 인터프리터) : 런타임 동작

  • 런타임에 바이트 코드를 해석
  • 바이트 코드는 Executor Engine 내부의 Java Interpreter에 의해 인터프리트 방식으로 해석됨

 

JIT Compiler : 런타임 동작

  • 핫스팟(자주 실행된 바이트 코드) → 기계어(저수준 언어) 변환
  • Native Area에 존재하는 Code Cache에 기계어 캐싱
  • JIT Compiler에 의해 컴파일(바이트 코드 → 네이티브 코드)된 네이티브 코드는 Native Call Stack에서 실행됨
  • Code Cache가 가득 차면, JIT는 컴파일러 스레드를 중지하고 인터프리터 코드를 더 이상 최적화하지 않는다.
  • 이와 같이 자바에는 소스 코드를 바이트 코드로 변환하는 자바 컴파일러와 바이트 코드를 기계어로 변환하는 JIT 컴파일러, 그리고 인터프리터가 존재한다.
  • 자바는 JVM이 설치된 머신에서 하드웨어와 OS 종류에 구애받지 않고 동작할 수 있다.
    • 이러한 특징을 플랫폼 독립성이라고 한다.
  • 자바가 플랫폼 독립적으로 실행될 수 있는 이유는 다음과 같다.
    1. 기계어는 명령어 셋이 하드웨어 종속적이다. 반면 바이트 코드는 JVM이 이해할 수 있는 언어로 JVM만 설치되어 있으면 실행 가능하다. 또한 JVM 위에서 동작하기 때문에 OS에 종속되지 않는다.
    2. JIT Compiler가 동작하는 경우, 기계어로의 변환이 이루어지는데 이때 실행중인 하드웨어와 OS에 맞춰서 기계어로 변환하게 된다.

 

JIT Compiler의 특징

  • 실행해야 할 바이트 코드 일부분을 기계어(바이너리 코드)로 변환한다.
  • 자바 컴파일러 ≠ JIT 컴파일러
    • 자바 컴파일러 : 소스코드 → 바이트 코드 변환
    • JIT 컴파일러 : 바이트 코드 → 바이너리 코드 변환
  • JIT는 실행 속도가 느린 인터프리트 방식의 단점을 보완한다. 초기에는 실행되지 않으며 (초기에는 인터프리트로만 동작), 런타임에 자주 호출되는 메서드나 루프를 감지하여 JIT 컴파일러가 기계어로 변환한다.
    • Hot Spot (핫스팟) : 자주 호출되는 메서드
    • JIT 컴파일러는 핫스팟을 네이티브 코드로 변환 후, Native area 내부의 Code Cache 공간에 저장한다. 이후 호출 시 빠르게 실행된다.
  • 컴파일 언어의 컴파일러와 차이점은 단순 기계어로 변환 뿐만이 아니라 런타임 최적화를 한다는 점이다.
    • 인라인, 루프 최적화, 동적 타입 분석

 

JIT Compiler의 장단점

  • 장점
    1. 실행 속도 향상 : 핫스팟 코드를 기계어로 변환하여 실행 속도를 향상
    2. 동적 최적화 : 런타임 정보를 바탕으로 최적화하므로 정적 컴파일러보다 효율적인 최적화 가능
    3. 플랫폼 독립성 유지 : 기본적으로 바이트 코드를 사용하여 JVM만 있으면 어디서든 동작할 수 있도록 하면서도, 필요에 따라 바이너리 코드로 변환하여 실행 성능을 향상시킬 수 있다.
  • 단점
    1. 초기 컴파일 오버헤드 : JIT 컴파일러는 런타임에 실행되므로 초기 실행 시 오버헤드가 발생할 수 있다.
    2. 메모리 사용량 증가 : 기계어를 저장하기 위해 추가 메모리 사용
    3. 복잡성 증가 : 동적 컴파일과 최적화 작업으로 인해 JVM의 실행 환경이 복잡해짐

 

Stack Based VM VS. Register Based VM

JVM은 스택 기반의 VM이다.

이는 javac로 변환된 기계어(JVM이 이해할 수 있는 바이트 코드)가 레지스터 기반이 아닌, 스택 기반으로 동작한다는 것을 의미한다.

레지스터 기반 VM과 스택 기반 VM이 어떤 차이가 있는지 알아보자.

  • Register Based VM
    • 피연산자를 CPU의 레지스터 영역에 저장
    • 적은 명령어 수, 긴 명령어
    • 명령어에 피연산자의 레지스터 주소를 담아야 하므로 명령어가 길어진다.
    • 구현에 높은 CPU 의존도 (CPU 자원인 레지스터를 직접적으로 활용)
      • 레지스터의 갯수, 레지스터 크기에 따라서 구현이 좌우된다.
  • Stack Based VM
    • 피연산자를 스택에 저장
    • 많은 명령어 수, 짧은 명령어
    • 피연산자를 stack의 top에서 꺼내면 되므로, 메모리 주소를 저장할 필요가 없어서 명령어가 짧아진다.
    • 스택 push, pop을 위한 오버헤드 발생
    • 모든 메서드 호출을 위해서는 스택 프레임이 필요하다.
  • 레지스터 기반 VM은 스택 기반 VM에 비하여 명령어 수가 47% 작지만, 명령어 크기가 25% 더 크다. 더 많은 VM 명령어를 가져올 수 있기 때문에, 명령어 당 CPU 부하를 1.07% 줄일 수 있다. 명령어 디스패치 > 컴퓨터 부하 라는 점을 감안했을 때, 명령어 크기가 커지더라도 명령어 수를 줄이는 것이 이점일 수 있다.
    • ⇒ 레지스터 기반 VM 성능이 스택 기반 VM에 비해 32.3% 좋다는 벤치마크 결과 존재

 

그럼에도 불구하고 JVM이 스택 기반으로 동작하는 이유는, 하드웨어 자원에 적게 의존하여 플랫폼 독립성을 지키기 위한 선택이라고 볼 수 있다.

https://www.korecmblog.com/blog/jvm-stack-and-register

 

 

728x90

'Java' 카테고리의 다른 글

2. JVM 뜯어보기 - Java Runtime Data Area  (2) 2025.05.14
1. JVM 뜯어보기 - Class Loader  (0) 2025.05.14
'Java' 카테고리의 다른 글
  • 2. JVM 뜯어보기 - Java Runtime Data Area
  • 1. JVM 뜯어보기 - Class Loader
suhsein
suhsein
티끌모아 태산
  • suhsein
    기억 못 할 거면 기록이라는 좋은 수단이 있다
    suhsein
  • 전체
    오늘
    어제
    • 분류 전체보기
      • ASAC
      • Next.js
      • Docker
      • MySQL
      • Java
      • Spring-Proxy, AOP
      • Spring Boot, JPA
      • Spring Security
      • DB
      • 알고리즘
      • PS
      • TOPCIT
      • AWS 자격증
      • 비공개
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 안녕하세요
  • 인기 글

  • 태그

    동적프로그래밍
    티스토리챌린지
    포인터
    해시
    Alias
    오블완
    tsp
    DP
    외판원순회
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
suhsein
0. JVM 기초와 자바의 컴파일러 및 인터프리터에 관하여
상단으로

티스토리툴바