JVM은 Java Virtual Machine의 약자입니다. JVM이 있음으로써 Java는 플랫폼 독립적인 코드로 작성될 수 있고, 실행될 수 있습니다. 흔히 얘기하는 "Write once, run anywhere"가 바로 JVM에서 나옵니다.
JVM은 Java Bytecode를 인터프리터 방식으로 읽어서 기계어로 번역하기 때문에 초창기에 느린성능을 보였지만, JIT Compiler HotSpot JVM등의 도입으로 Native 언어와 유사한 수준의 실행속도를 보이게 되었습니다.
JVM은 JDK에 포함되어 있으며, Oracle을 비롯한 여러 Vendor가 JDK를 개발하고 공급하고 있습니다. JVM은 JVM Specification 문서로 정의되어 있으며, JVM은 이를 따라야 합니다. 하지만 Spec 문서는 많은 부분을 강제하고 있지 않아서 JDK마다 구현 방식이 다를 수 있습니다.
JVM은 메모리 관리를 개발자가 하지 않고, Garbage Collector가 수행합니다. 따라서, JVM을 사용하는 개발자는 Garbage Collector가 어떤 방식으로 동작하는지 학습할 필요성이 있습니다.
또, 오늘날의 JVM은 Java만을 위한 가상머신이 아니게 되었습니다. 여러 언어(Scala, Kotlin, Groovy 등)들이 JVM을 이용하며, JVM을 사용하지 않는 환경을 찾기란 힘듭니다.
Java 코드를 JVM에서 바로 실행할 수 있는 것은 아니고, 컴파일 과정을 거쳐야 합니다.
이 때, 컴파일 된 코드는 Java Bytecode라고 합니다.
javac <filename>.java
와 같은 명령어를 터미널에 입력해서 컴파일을 수행할 수 있습니다.
package가 있는경우, classpath를 지정해주어야 하는 경우 등이 주의해야하는 경우입니다.
보통은 IDE를 사용하고, build tool등을 사용해서 실제 해당 명령어를 사용해서 컴파일을 하는 경우는 적습니다.
컴파일을 수행하면서 문법에 맞지 않을 경우 오류가 발생하는데, 이를 컴파일 에러라고 합니다.
java <package>.<classname>
으로 실행합니다.
-cp
옵션으로 classpath를 지정해줄 수 있고, classname
뒤에 문자열 형태의 매개변수를 전달해줄 수 있습니다.
javac
와의 차이점을 주의해야 합니다.(javac
는 파일을 가리켜서 컴파일하지만, java
는 패키지에 존재하는 클래스를 가리켜서 실행합니다.)
이렇게 실행할 때 JVM에서 일어나는 일은 다음과 같습니다.
먼저 JVM은 사용자의 입력을 받아 해당 클래스의 main
메서드를 호출하려고 합니다.
클래스는 당연히 로드되지 않았기 때문에 클래스 로더가 해당 클래스를 로드합니다. 이 과정이 실패하면 오류가 발생합니다.
클래스는 main 메서드가 호출되기 전에 초기화됩니다. 이 과정에서도 문제가 있다면 오류가 발생합니다.
그 다음 main 메서드를 호출합니다. main 메서드가 동작하기 위해 필요한 클래스 및 인터페이스를 로드합니다.
학습할 때, 어떤 프로그램의 옵션을 공부하는 것 만으로도 큰 도움이 될 수 있다는 백기선님의 말이 어느정도 공감이 갔습니다. java, javac, javap의 각 옵션에 대한 학습은 따로 진행하도록 하겠습니다.
class 파일에는 해당 바이트코드가 어떤버전에 호환되도록 작성되었는지가 기록되어 있습니다.
Java 바이트코드는 사실 몰라도 사용하는데 아무런 문제가 없습니다.
하지만, 이런 바이트코드의 생성원리를 이해하는 것은 분명 우리에게 도움이 될 수 있습니다.
바이트코드는 JVM 언어들을 컴파일한 결과로 나오는 코드로 JVM이 읽어서 기계어로 번역할 수 있는 코드입니다.
바이트코드는 JVM 스펙문서에 정의되어 있으며, 바이너리 코드기 때문에 일반적으로는 읽을 수 없고 다음의 방법을 사용해야합니다.
javap -c <classname>
혹은 javap -v <classname>
바이트코드는 명령어들이 1byte를 차지하기 때문에 바이트코드라는 이름이 붙었습니다.
Just In Time 컴파일러라고 하여 런타임에 인터프리터 방식으로 동작하는 Java의 한계를 보완한 기술입니다.
기존 JVM은 다른 인터프리터 언어와 동일하게 코드를 한 줄 한 줄 읽어서 기계어로 번역하면서 실행했습니다. 아무리 사전에 컴파일 과정을 거쳤다 한들, 이런 번역과정의 반복은 실행속도를 늦추는 원인이 됩니다.
JVM은 가장 자주 실행되는 코드 블록, 메서드 또는 메서드의 일부, 특히 반복문을 모니터링해서 네이티브 코드로 컴파일 시킵니다. 이렇게 하면 불필요한 번역과정을 생략할 수 있기 때문에 더 빠른 실행속도를 보일 수 있습니다.
JIT 컴파일 과정은 별도의 스레드에서 실행되고, JVM 스레드는 JIT 컴파일 스레드의 영향을 받지 않기 때문에 애플리케이션의 실행에 영향을 주지 않습니다. 컴파일이 진행중일 때에는 인터프리터 방식으로 동작하지만, 컴파일이 완료되면 컴파일 된 버전을 사용하게 됩니다. 이를 on-stack replacement(OSR) 이라고 합니다.
JIT 컴파일 과정은 컴파일 과정에서는 분명 느려질 것입니다. 하지만, 네이티브 코드로 컴파일 되었을 때의 장점이 더 크므로 이런 성능저하는 무시할 수 있습니다.
JIT 컴파일러는 C1 (클라이언트 컴파일러), C2(서버 컴파일러)로 구분됩니다. C1과 C2는 동작이 다릅니다. C1은 빠르게 실행되지만 덜 최적화 된 코드를 생성하며, C2는 실행 시간이 좀 더 걸리지만 더 최적화된 코드를 생성합니다.
오늘날의 JVM은 두 가지를 모두 사용하며, 처음엔 C1을 사용하지만, 호출의 수가 증가하게 되면, C2를 사용합니다. 이를 tiered compilation이라고 합니다.
C2는 C++과 견줄 수 있는 속도의 코드를 생성하며, 몇가지 문제가 있어서 새로운 프로젝트가 진행되었는데, 이를 GraalVM이라고 합니다.
GraalVM은 하기 글에 자세히 설명되어 있으며, 이를 설명하는 것은 목적을 벗어나기 때문에 여기까지 하겠습니다.
JVM은 다음의 요소로 구분할 수 있습니다.
Native가 붙은 친구들은 Native 언어를 위한 영역입니다.
클래스로더는 클래스 파일을 읽어들이는 부분입니다. 자바는 동적 로딩을 사용하기 때문에 실제 애플리케이션에서 필요한 부분만 로딩해서 사용하게 됩니다.
클래스로더는 다음 세 가지 과정으로 동작합니다.
아래의 요소들로 클래스가 로드됩니다.
jre/lib/rt.jar
에 담긴 JDK 클래스 파일을 로딩합니다. Native C로 구현되어있다고 합니다. Java9 이후엔 rt.jar
가 제거되면서 범위가 축소되었습니다.jre/lib/ext
또는 java.ext.dirs
환경 변수로 지정된 폴더에 있는 클래스 파일을 로딩합니다.Class-Path
속성값으로 지정된 폴더에 있는 클래스를 로딩합니다. 개발자가 애플리케이션 구동을 위해 직접 작성한 대부분의 클래스가 이 클래스로더에 의해 로딩됩니다.ClassLoader의 마지막 단계입니다. 모든 static 변수들은 기본값으로 할당되고, static block이 실행됩니다.
런타임 데이터 영역은 JVM의 메모리 부분이라고 볼 수 있습니다.
런타임 데이터 영역은 다음의 다섯 가지 주요 영역으로 구분할 수 있습니다.
실행 엔진은 런타임 데이터 영역에 할당된 Java 바이트 코드를 읽어서 기계어로 번역해 실행하는 영역입니다.
실행 엔진은 다음의 세 가지 요소를 가집니다.
예전에는 JRE를 따로 제공했었는데, 최근에는 JDK만 제공하는 추세입니다.(Oracle에서만)
좋은글 감사합니다.