본문으로 건너뛰기
  1. Posts/

[Java] Java는 왜 한 번 컴파일하면 여러 OS에서 실행될까? JVM, GC, JIT, JDK/JRE

·8 분
NineKoo9
작성자
NineKoo9
목차

이 문서는 의문을 가지고 있던 지점을 AI 에이전트와 대화하며 정리한 메모입니다. Java가 플랫폼 독립성을 얻는 방식과 JVM, GC, JIT, JDK/JRE의 역할을 한 흐름으로 정리합니다.

Java는 왜 한 번 컴파일하면 여러 OS에서 실행될까?
#


1. Java가 JVM으로 얻는 장점
#

“Write Once, Run Anywhere”
#

Java 소스코드를 컴파일하면 .class 파일(바이트코드)이 생성된다.
이 바이트코드는 특정 OS/CPU에 종속되지 않은 중간 언어다.

Java 소스코드(.java)
    ↓ javac 컴파일
바이트코드(.class)  ← OS와 무관한 중간 언어
    ↓ JVM 실행
각 OS에서 동작

2. “JVM도 OS에 맞게 설치해야 하지 않아?” → 맞다!
#

여기서 핵심 개념이 나온다.

역할누가 부담?
OS 의존성 처리JVM (JVM이 OS별로 따로 존재)
OS 신경 쓰는 주체개발자 → JVM 으로 이전됨

즉, 개발자가 OS를 신경 쓰던 부담을 JVM 개발사(Oracle 등)가 대신 져주는 구조다.

[Windows JVM]  ┐
[Mac JVM]      ├──── 동일한 .class 바이트코드를 실행
[Linux JVM]    ┘

단순한 예제 수준에서는 .class 파일 하나를 들고 다닌다고 생각해도 된다.
다만 실제 배포에서는 호환 Java 버전, 의존 라이브러리, 네이티브 모듈 여부도 함께 맞춰야 한다.
즉, “OS 독립성"은 크게 얻지만 “아무 환경에서나 무조건 실행"까지 의미하는 것은 아니다.


3. Python, C는? 그냥 실행되지 않아?
#

C의 경우 — 플랫폼마다 다시 컴파일 필요
#

같은 C 코드라도:
  → Windows에서 컴파일 → Windows 실행파일(.exe)  → Mac에서 실행 불가
  → Mac에서 컴파일    → Mac 실행파일            → Windows에서 실행 불가

C는 소스코드를 각 OS/CPU에 맞는 기계어로 직접 변환하기 때문에,
배포할 때 플랫폼마다 따로 빌드해야 한다.
오픈소스 프로그램에서 “Windows 버전 / Mac 버전” 을 따로 제공하는 이유가 이것이다.

Python의 경우 — 인터프리터/런타임이 실행을 담당
#

Python 소스코드(.py)
    ↓ Python 구현체(CPython 등)
파이썬 바이트코드(.pyc 등, 내부 캐시)
    ↓ 인터프리터 실행
각 OS에서 동작

Python도 플랫폼별 인터프리터를 설치해야 한다는 점에서는 Java와 비슷하다.
하지만 구조가 “거의 동일"하다고 보긴 어렵다.

  • CPython은 소스를 내부적으로 바이트코드로 컴파일한 뒤 실행한다.
  • 이 바이트코드는 CPython 구현과 버전에 의존하는 내부 표현에 가깝다.
  • 반면 Java의 .class 바이트코드는 JVM 명세에 의해 표준화된 배포 포맷이다.

그래서 Python은 보통 소스코드(.py)나 wheel 같은 패키지를 배포하고,
Java는 .class/.jar 같은 JVM 바이트코드 산출물을 배포하는 경우가 많다.

언어별 비교 정리
#

배포 단위OS 의존성 처리특징
C플랫폼별 실행파일개발자가 직접가장 빠름, 이식성 낮음
Java바이트코드 (.class)JVM이 처리이식성 높음, JVM 설치 필요
Python소스코드 (.py), 패키지인터프리터/런타임이 처리편리하지만 구현체별 차이 존재, 인터프리터 설치 필요

결국 “OS 의존성을 누가 처리하느냐"의 차이다.
Java의 혁신은 “개발자가 플랫폼을 신경 쓰지 않아도 되도록 그 책임을 JVM으로 옮긴 것"이다.


4. JVM의 혁신들
#

플랫폼 독립성이 가장 유명하지만, JVM이 가져온 혁신은 여러 가지다.


혁신 1. 플랫폼 독립성 (가장 유명)
#

“Write Once, Run Anywhere”
위에서 설명한 것과 동일.


혁신 2. 자동 메모리 관리 (Garbage Collection)
#

C/C++ 개발자가 겪던 가장 큰 고통 중 하나가 메모리를 직접 관리해야 한다는 것이었다.

// C에서는 개발자가 직접 해제해야 함
int* arr = malloc(100);
// ... 쓰다가
free(arr);  // 이걸 까먹으면? → 메모리 누수(Memory Leak)

JVM은 GC(Garbage Collector) 가 안 쓰는 메모리를 자동으로 회수한다.
개발자가 free() 같은 걸 신경 쓸 필요가 없어진다.
생산성과 안정성 면에서 엄청난 혁신이었다.


혁신 3. 바이트코드 검증과 역사적 샌드박스 모델
#

바이트코드는 JVM 위에서만 실행되기 때문에, JVM은 로드/링크 과정에서 class 파일을 검증할 수 있다.

class 파일 로드
    ↓ 형식 검사 + 바이트코드 검증
타입 안정성/기본 무결성 확인
    ↓ 실행

이 검증 과정은 잘못된 바이트코드, 잘못된 타입 사용, 스택 언더플로/오버플로 같은 문제를 줄이는 데 중요했다.
즉, Java 보안의 한 축은 “JVM이 아무 바이트코드나 그대로 실행하지 않는다"는 점이다.

초기 Java에는 여기에 더해 Security Manager 기반 샌드박스가 있었고,
Applet 같은 원격 코드를 제한된 권한으로 실행할 수 있었다.

다만 이건 역사적 맥락으로 이해하는 게 맞다.

  • Java 9 (2017): Applet API deprecated
  • JDK 11 (2018): 브라우저 플러그인, Applet Viewer, Java Web Start 제거
  • Java 17 (2021): Applet API / Security Manager 제거 예정(deprecated for removal)
  • JDK 24 (2025): Security Manager 영구 비활성화

그래서 오늘날의 Java를 설명하면서 “샌드박스가 핵심” 이라고 말하면 다소 옛 설명에 가깝다.
현재도 중요한 건 바이트코드 검증, 타입 안전성, 메모리 안전성 쪽이다.


혁신 4. JIT 컴파일 (Just-In-Time)
#

초창기 JVM은 바이트코드를 한 줄씩 해석해서 느렸는데, 나중에 JIT 컴파일러가 도입됐다.

바이트코드
    ↓ 인터프리터로 실행 + 프로파일링
hot code 발견
    ↓ JIT가 기계어로 컴파일
이후 더 빠르게 실행

“자주 실행되는 코드는 실행 중에 기계어로 최적화하자"는 아이디어다.
현대 JVM은 여기에 tiered compilation 같은 기법도 사용한다.
덕분에 Java는 많은 워크로드에서 높은 성능을 낼 수 있게 됐다.
즉, 이식성 + 런타임 최적화를 함께 가져간 셈이다.


혁신 5. JVM 자체가 생태계가 됨
#

JVM이라는 실행 환경이 표준화되면서, Java 말고 다른 언어들도 JVM 위에서 돌아가도록 만들어지기 시작했다.

Kotlin   ┐
Scala    ├──── 모두 JVM 위에서 실행됨 (Java 생태계 그대로 활용)
Groovy   ┘

특히 Kotlin/JVM은 Java와 높은 수준의 상호운용성을 제공하므로 Java 라이브러리를 그대로 활용할 수 있다.
그래서 JVM이 하나의 범용 실행 플랫폼이 된 것이다.

다만 Android는 한 가지 구분이 필요하다.

  • Google은 Kotlin을 Android 개발에서 공식 지원한다.
  • 하지만 Android 앱의 최종 런타임은 표준 JVM이 아니라 ART(Android Runtime) 다.
  • 빌드 과정에서 Java/Kotlin의 .class 바이트코드는 DEX 바이트코드로 변환된다.

즉, “Kotlin이 Android에서 JVM 위에서 그대로 실행된다"는 말은 정확히는 아니다.
그래도 Java와의 상호운용성 + 기존 생태계 재사용성이 매우 크다는 점은 맞다.


혁신 우선순위 정리
#

순위혁신한 줄 요약
플랫폼 독립성개발자가 OS를 신경 안 써도 됨
GC (자동 메모리 관리)메모리 버그에서 해방
JVM 생태계화Kotlin 등 다른 언어도 Java 자산 활용
바이트코드 검증/역사적 샌드박스타입 안전성과 제한 실행 모델
JIT 컴파일이식성과 런타임 성능 최적화

플랫폼 독립성이 가장 유명하고 상징적인 혁신인 건 맞지만,
실제 실무에서 Java를 선택하는 이유는 GC + 성숙한 생태계 + JIT 성능이 복합적으로 작용한다.


5. JVM 설치 방식
#

brew install openjdk@21 하면 뭐가 설치되냐?
#

openjdk@21
└── JDK
    ├── JVM                       ← 바이트코드 실행 엔진
    ├── 표준 클래스 라이브러리/런타임 구성요소
    └── 개발 도구                ← javac, javadoc, jdb 등

Homebrew의 openjdk@21JDK 21 배포판이다.
예전 자료처럼 JVM / JRE / JDK를 따로 박스로 나누는 설명은 개념 구분용으로는 유효하다.

하지만 현재 기준으로는 주의가 필요하다.

  • JDK는 JVM을 포함하는 상위 개념이다.
  • JRE는 JVM + 실행용 라이브러리라는 전통적 개념이다.
  • 그러나 JDK 11(2018)부터는 별도 JRE 이미지가 사라졌다.

그래서 openjdk@21을 설치할 때는
“JDK 안에 JVM과 실행 구성요소, 개발 도구가 함께 들어온다” 고 이해하는 편이 더 정확하다.

brew로 설치하는 것 자체는 맞다.
JVM은 OS에 기본으로 깔려있지 않으며, 보통은 JDK 배포판을 설치해서 함께 받는다.


“OS별로 설치된다"는 말의 의미
#

brew로 설치할 때 내부적으로 내 OS/아키텍처에 맞는 바이너리를 받아온다.

brew install openjdk@21
  → Mac(ARM)용 OpenJDK 바이너리 다운로드
  → Linux라면 Linux용 바이너리 다운로드

JVM 자체가 OS별로 다르게 만들어져 있고, 사용자는 자기 OS에 맞는 걸 설치하는 것이다.
이 과정을 brew가 자동으로 처리해준다.

단, 여기서도 표현을 정확히 해야 한다.

  • Homebrew는 공식적으로 macOS와 Linux에서 쓰는 패키지 관리자다.
  • 그래서 이 문맥에서 brew install openjdk@21Mac/Linux 기준 설명이다.
  • Windows는 보통 다른 설치 경로를 사용한다.

전체 흐름 정리
#

[Oracle / OpenJDK 개발팀]
  → Mac용 JVM 빌드
  → Windows용 JVM 빌드       ← JVM 자체는 OS별로 존재
  → Linux용 JVM 빌드

[개발자]
  → brew install openjdk@21  ← Mac/Linux에서 내 환경에 맞는 JDK를 설치
  → javac Hello.java          ← 바이트코드 생성
  → java Hello                ← 설치된 JVM이 실행

“JVM이 OS 의존성을 처리해준다” 는 말은,
JVM 개발팀이 OS별로 JVM을 따로 만들어서 배포하고, 그 수고를 개발자 대신 해주는 것을 의미한다.
개발자는 그 구현체를 설치한 뒤 공통 바이트코드를 올려 실행한다는 뜻이다.

즉, 이후에는 .class / .jarOS마다 다시 컴파일될 필요는 없지만,
여전히 Java 버전 호환성, 라이브러리 의존성, 배포 방식(jar, jlink runtime, container 등) 은 따로 신경 써야 한다.

참고 자료
#