사전 JVM
표준

JVM

gabury1고친 사람 github-actions[bot]

JVM 은 자바로 짠 프로그램을 대신 실행해 주는 가상의 컴퓨터입니다. 프로그램을 짜는 쪽은 어느 운영체제에서 돌지 정하지 않습니다. 그 프로그램을 이 기계에 맞춰 돌리는 일은 JVM 이 맡습니다. 메모리를 내주고 다 쓴 메모리를 치우는 일도 JVM 이 대신합니다.

쉽고 빠른 이해

JVM 은 자바 프로그램을 대신 실행해 주는 가상의 컴퓨터입니다. java -jar app.jar 라고 쳤을 때 뜨는 그 프로세스가 JVM 입니다.

컴퓨터는 기종마다 알아듣는 명령이 다릅니다. JVM 이 없으면 같은 프로그램을 리눅스용과 윈도우용으로 따로 빌드해야 합니다. JVM 은 그 차이를 자기 안에서 받아 냅니다.

어떻게 도는가:

  1. 컴파일러가 소스를 클래스 파일로 바꿉니다. 그 안에는 JVM 만 알아듣는 명령이 들어 있습니다
  2. JVM 이 그 파일을 읽어 올린 뒤 수상한 명령이 없는지 훑습니다
  3. 명령을 하나씩 해석해 실행하다가, 자주 도는 대목은 이 기계의 명령으로 번역해 둡니다

대가도 있습니다. 프로그램을 켜는 데 시간이 걸립니다. 한동안 돌아야 제 속도가 납니다. 다 쓴 메모리를 치우는 때도 JVM 이 정하기 때문에 그 순간 프로그램이 잠깐 멈출 수 있습니다. 그래서 몇 초 만에 끝나는 작은 명령줄 도구에는 이 대가만 남습니다.

상세

JVM(Java Virtual Machine, 자바 가상 머신)은 명세 문서 하나로 정의된 가상의 컴퓨터입니다. 쇠와 실리콘으로 만든 물건이 아니라, 어떤 명령을 받으면 어떻게 동작해야 하는지를 글로 적어 둔 문서입니다. 그 문서대로 만들어진 프로그램이 컴퓨터에 깔려 있습니다. java 명령을 치면 그 프로그램이 뜹니다. 자바 개발자가 「JVM 이 떴다」고 말할 때 가리키는 것도 이 프로그램입니다.

콘센트에 견줄 만합니다. 전압과 구멍 모양을 미리 적어 둔 문서가 있어서, 어느 회사가 만든 콘센트에 꽂아도 같은 가전이 돕니다. 자바 프로그램에게 그 콘센트에 해당하는 것이 JVM 입니다. 다만 콘센트는 전기만 흘려보냅니다. JVM 은 프로그램을 받아 실행까지 대신합니다.

명령을 미리 정해 둔 가상의 컴퓨터

CPU(Central Processing Unit, 중앙 처리 장치)는 저마다 알아듣는 명령이 정해져 있습니다. 칩 계열이 다르면 같은 덧셈도 다른 명령으로 적습니다. 프로그램을 그 명령으로 바로 번역해 두면 그 계열에서만 돕니다. JVM 은 어느 칩에도 없는 자기 명령을 따로 정해 두고 그 어긋남을 받아 냅니다. 명령 하나가 대개 1바이트로 적히기 때문에 이 명령들을 바이트코드라고 부릅니다.

계산하는 방법도 정해져 있습니다. 칩은 값을 레지스터라는 이름 붙은 칸에 두고 계산합니다. JVM 은 그 대신 계산에 쓰는 스택에 값을 쌓았다 꺼내며 계산합니다.

두 수를 더하라는 명령은 인자를 받지 않습니다. 쌓여 있는 위쪽 두 값을 꺼내 더한 뒤 결과를 다시 쌓으라고 정해져 있습니다.

flowchart TD
    subgraph A["명령 전"]
        A1["계산에 쓰는 스택<br/>비어 있다"]
    end
    subgraph B["2 와 3 을 쌓은 뒤"]
        B1["위쪽 3<br/>아래 2"]
    end
    subgraph C["더하기 명령 뒤"]
        C1["위쪽 5"]
    end
    A --> B --> C

이 그림의 스택은 계산에 쓰는 스택입니다. 뒤에 나오는 호출 기록이 쌓이는 스택과는 다른 것입니다.

정해 둔 것은 넷으로 추릴 수 있습니다.

정해 둔 것 무엇을 정하나
명령 바이트코드 명령 하나하나가 무슨 일을 하는가
클래스 파일의 모양 실행할 프로그램을 어떤 바이트 배열로 적어 오는가
실행 중 메모리 구획 객체와 호출 기록을 어디에 두는가
스레드가 값을 보는 규칙 한 스레드가 고친 값이 다른 스레드에 언제 보이는가

JVM 만 따로 내려받는 일은 없습니다. JDK(Java Development Kit, 자바 개발 도구 모음)나 JRE(Java Runtime Environment, 자바 실행 환경)를 깔면 그 안에 함께 들어옵니다.

flowchart TD
    subgraph JDK["JDK · 자바 개발 도구 모음"]
        subgraph JRE["JRE · 자바 실행 환경"]
            JVM["JVM"]
        end
    end

소스에서 실행까지의 경로

자바 소스 파일은 JVM 에 바로 들어가지 못합니다. javac 같은 컴파일러가 먼저 소스를 읽어 클래스 파일로 바꿔 놓아야 합니다. 그 파일 안에 들어 있는 것이 바이트코드입니다.

소스에서 클래스 파일까지가 컴파일입니다. 그 파일을 받아 이 기계에서 돌리는 것부터가 JVM 의 몫입니다.

클래스 파일은 어느 운영체제에서 만들었든 같은 바이트입니다. 기종마다 갈리는 것은 JVM 쪽입니다. 「한 번 짜서 어디서나 돈다」는 말이 가리키는 것이 이 나눔입니다.

JVM 은 자바 소스를 아예 읽지 않습니다. 클래스 파일만 봅니다. 그래서 소스를 적은 언어가 자바가 아니어도 됩니다. kotlinc 가 만들어 낸 클래스 파일도 JVM 에게는 똑같은 입력입니다.

flowchart TD
    J["자바 소스"] --> JC["javac"]
    K["코틀린 소스"] --> KC["kotlinc"]
    S["스칼라 소스"] --> SC["scalac"]
    JC --> CF["클래스 파일 · 바이트코드가 들었다"]
    KC --> CF
    SC --> CF
    CF --> L["리눅스의 JVM"]
    CF --> W["윈도우의 JVM"]
    CF --> M["맥의 JVM"]

여러 언어가 클래스 파일 하나로 모입니다. 그 하나를 기계마다 다른 JVM 이 받아 각자의 명령으로 실행합니다.

클래스를 찾아 올리고 훑는 단계

JVM 은 프로그램을 켤 때 클래스를 전부 읽어 두지 않습니다. 어떤 클래스가 처음 쓰이는 때에 그 클래스 하나를 읽어 올립니다. 이 일을 맡는 부품이 클래스 로더입니다.

어디서 찾을지는 클래스패스가 정합니다. 클래스패스는 클래스 파일과 JAR(Java Archive, 자바 압축 묶음) 묶음이 놓인 경로 목록입니다. 목록에 없는 클래스를 불러도 컴파일은 통과합니다. 그런데 실행 중에 못 찾겠다는 오류가 납니다. 분명히 있는 클래스를 못 찾는 사고는 대개 이 목록이 어긋난 것입니다.

읽어 올린 다음에는 훑는 단계가 옵니다. JVM 은 받은 바이트코드를 믿지 않고 한 번 검사합니다. 없는 타입을 쓰지 않는지, 스택에서 꺼낼 것이 없는데 꺼내려 들지 않는지를 봅니다. 컴파일러가 만든 파일이라도 그 뒤에 누가 고쳤을 수 있어서입니다.

검사를 통과하면 초기화가 돕니다. static 은 객체마다가 아니라 클래스마다 하나만 두는 값과 그 값을 채우는 코드입니다. static 초기화 블록과 static 필드의 첫 값이 이때 정해집니다.

어떤 클래스를 처음 건드리는 코드가 유독 오래 걸리는 것은 읽어 올리기 · 훑기 · 초기화, 이 세 단계가 그 순간에 몰려서입니다.

flowchart TD
    A["클래스를 처음 건드린다"] --> B["클래스 로더가 클래스패스에서 찾는다"]
    B --> C{"찾았나"}
    C -->|못 찾았다| D["실행 중에 못 찾겠다는 오류"]
    C -->|찾았다| E["읽어 올리기"]
    E --> F["훑기 · 바이트코드 검사"]
    F --> G["초기화 · static 블록과 static 필드"]

실행 중에 나뉘는 메모리 구획

JVM 이 뜨면 메모리가 몇 구획으로 나뉩니다. 구획마다 꽉 찼을 때 나는 오류가 다릅니다. 그래서 이 나눔은 장애를 들여다볼 때 바로 쓰입니다.

flowchart TD
    subgraph T["스레드마다 따로 갖는다"]
        S1["스레드 1<br/>호출 기록이 쌓이는 스택"]
        S2["스레드 2<br/>호출 기록이 쌓이는 스택"]
    end
    subgraph G["모든 스레드가 함께 쓴다"]
        H["힙 · new 로 만든 객체"]
        M["메서드 영역 · 클래스 정보와 상수"]
    end
    S1 --> H
    S2 --> H

객체는 힙에 놓입니다. 메서드를 한 번 부를 때마다 스택에는 한 칸이 쌓입니다. 이 스택은 앞서 계산에 쓴 스택과는 다른 것입니다. 여기서는 「호출 기록이 쌓이는 스택」이라고 부릅니다.

그 칸에는 그 메서드의 지역 변수와 돌아갈 곳이 들어갑니다. 클래스 정보와 상수는 메서드 영역에 따로 모입니다.

flowchart TD
    subgraph K1["먼저 쌓인 칸 · main"]
        C1["지역 변수 · 돌아갈 곳"]
    end
    subgraph K2["그다음 칸 · 첫 번째 메서드"]
        C2["지역 변수 · 돌아갈 곳"]
    end
    subgraph K3["맨 나중 칸 · 두 번째 메서드"]
        C3["지역 변수 · 돌아갈 곳"]
    end
    K1 --> K2 --> K3

메서드가 끝나면 그 칸이 걷힙니다. 걷히는 것보다 쌓이는 것이 계속 앞서면 스택이 꽉 차서 StackOverflowError 가 납니다.

구획 무엇이 놓이나 누가 갖나 넘치면 나는 오류
힙 new 로 만든 객체 모든 스레드가 함께 OutOfMemoryError
호출 기록이 쌓이는 스택 메서드 호출 한 번마다 한 칸 스레드마다 따로 StackOverflowError
메서드 영역 읽어 올린 클래스 정보와 상수 모든 스레드가 함께 OutOfMemoryError

힙이 꽉 차기 전에 아무도 안 가리키는 객체를 치우는 일을 가비지 컬렉션이라고 부릅니다. 자바 프로그램이 메모리를 손으로 반납하지 않아도 되는 것이 이 덕입니다. 대신 치우는 때를 JVM 이 정하므로, 그 순간 프로그램이 잠깐 멈출 수 있습니다.

해석으로 시작해 번역으로 옮겨 가는 실행

JVM 은 바이트코드를 두 가지 방법으로 실행합니다. 처음에는 인터프리터가 명령을 하나씩 읽어 그때그때 해석합니다. 켜자마자 돌 수 있지만, 이 방법으로 도는 동안은 기계 명령으로 도는 코드에 비해 느립니다.

돌다 보면 몇몇 메서드가 유난히 자주 불립니다. JVM 은 그런 메서드를 골라 이 기계의 명령, 곧 기계어로 통째로 번역해 둡니다. 다음부터는 번역해 둔 것을 부릅니다. 이것이 JIT(Just In Time) 컴파일입니다.

앞에서 말한 컴파일은 소스를 클래스 파일로 바꾸는 일입니다. 이쪽은 그 클래스 파일 안의 바이트코드를 기계어로 바꾸는 일입니다.

그래서 자바 서버는 뜨자마자 제 속도를 내지 않습니다. 얼마간 요청을 받아 봐야 자주 도는 길이 드러납니다. 그 길부터 번역이 붙습니다.

이 구간을 워밍업이라고 부릅니다. 성능을 잴 때 앞 구간을 버리고 재는 까닭이 이것입니다.

기다린 만큼 얻는 것도 있습니다. 실행 전에 미리 번역해 두는 방식은 아직 안 돌려 본 코드를 짐작으로 번역합니다. JVM 은 어느 분기가 자주 도는지 보고 나서 번역합니다.

거꾸로 오래 안 도는 프로그램은 그것을 못 얻습니다. 몇 초 만에 끝나는 명령줄 도구는 번역이 붙기 전에 일이 끝나서, 켜는 시간과 인터프리터 구간만 떠안습니다.

명세가 정하지 않은 대목

명세는 무엇을 해야 하는지만 정합니다. 어떻게 해야 하는지는 대개 만드는 쪽에 맡깁니다. 다 쓴 객체를 언제 어떤 방법으로 치울지, 바이트코드를 언제 번역할지, 힙을 어떤 구획으로 다시 나눌지가 그렇게 열려 있는 대목입니다.

그래서 JVM 은 하나가 아닙니다. HotSpot · OpenJ9 · GraalVM 은 같은 클래스 파일을 받아 같은 결과를 내지만, 메모리 씀씀이와 멈추는 시간이 서로 다릅니다. 튜닝 옵션 이름이 구현마다 갈리는 것도 이 열린 대목에서 옵니다.

자바 언어와 JVM 도 갈립니다. 자바 문법을 정하는 문서와 JVM 을 정하는 문서는 따로 있습니다. 그래서 코틀린처럼 자바가 아닌 언어도 클래스 파일만 내놓으면 JVM 위에서 돕니다.

관련 항목

JVM 을 담아 배포하는 설치 꾸러미

JDK · JRE · OpenJDK · Java SE

JVM 이 읽어 들이는 입력 파일

클래스 파일 · 바이트코드 · JAR · 상수 풀

클래스 파일을 만들어 내는 컴파일러

javac · kotlinc · scalac · 컴파일러

JVM 이 클래스를 찾아 올리는 장치

클래스패스 · 클래스 로더 · 자바 모듈 시스템 · 바이트코드 검증

실행 중에 나뉘는 메모리 구획

힙 · 스택 · 메서드 영역 · 메타스페이스 · 가비지 컬렉션

JVM 이 바이트코드를 실행하는 방식

인터프리터 · JIT 컴파일 · AOT 컴파일 · 워밍업

JVM 위에서 도는 프로그래밍 언어

자바 · 코틀린 · 스칼라 · 클로저 · 그루비

같은 명세를 저마다 구현한 제품

HotSpot · OpenJ9 · GraalVM

JVM 과 맞세워지는 다른 실행 방식

가상 머신 · 네이티브 이미지 · WebAssembly · CLR · BEAM

JVM 을 돌릴 때 자주 나는 오류

OutOfMemoryError · StackOverflowError · ClassNotFoundException · NoClassDefFoundError · UnsupportedClassVersionError

JVM 속을 들여다보는 도구

javap · jstack · jmap · 힙 덤프 · JFR · JMX

다른 이름: Java Virtual Machine · 자바 가상 머신