글 작성자: 자바니또

목차

  • JVM
  • JVM의 구성요소
  • 바이트코드?
  • 컴파일?
  • 컴파일된 코드 실행
  • JIT(Just-In-Time)컴파일러?
  • JDK? JRE?

JVM

JVM이란, 바이트코드로 컴파일 된 파일들을 운영체제 대신 실행하는 가상의 컴퓨터(소프트웨어)이다. JVM은 H/W와 OS 위에서 실행되기 때문에 JVM자체는 플랫폼에 종속적이다. 즉, 플랫폼에 따라 호환되는 JVM을 실행시켜줘야 한다. Java뿐만아니라 Scala등과 같이 다른언어로 작성되었어도 바이트코드로 컴파일만 시킨다면 JVM으로 실행이 가능하다.

JVM의 구성요소

JVM구성요소

JVM은 JRE안에있는 가상머신으로서 작은 가상 컴퓨터이기 때문에 독자적인 데이터메모리 영역과 실행엔진을 가지고 있다.

  • Class Loader : 동적로딩을 통해 필요한 클래스들을 로딩하여 Runtime Data Area의 Method영역에 저장한다.
  • Runtime Data Area : JVM의 메모리로서 운영체제로부터 할당받는다. 크게 쓰레드 스택, Heap, Method Area로 나뉜다.
  • Execution Engine : 메모리에 올라온 바이트코드를 명령어 단위로 하나씩 실행해준다. JIT컴파일러와 Interpreter, GC(Garbage Collector)로 이루어져 있다.

바이트코드?

바이트코드란 Java, C#같은 고급언어로 작성한 코드를 JVM이 읽기 쉽도록 변환한 코드이다. 각 라인은 명령 코드(opcode)와 매개변수로 이루어져 있다. 바이트코드라는 이름은 명령코드의 크기가 1바이트여서 붙여진 이름이다.

JVM은 운영체제와 H/W에 호환이 되어야 하기 때문에 플랫폼에 종속적이지만, 바이트코드는 어떤 플랫폼이든지 JVM만 설치되어있다면 쉽게 실행되어질 수 있다. 즉, 플랫폼에 독립적이다. Java가 플랫폼에 독립적이라는 것은 바이트 코드 덕분에 가능한 것이다.

컴파일?

컴파일이란, 특정 프로그래밍 언어로 쓰여있는 문서에서 다른 프로그램이나 하드웨어가 처리하기에 용이한 형태의 코드로 변환하는 과정을 말한다. 고급언어를 바이트코드로 바꾸는 것과 바이트코드를 기계어로 바꾸는 것 모두 컴파일이라 할 수 있다.

컴파일 명령어

$ javac <파일이름.java>    //ex. javac HelloWorld.java

javac는 JDK에 내포되어있는 Java 컴파일러(바이트코드 컴파일러)이다. 원시파일(.java)을 인자로 실행시키면 목적파일(.class)이 생성된다. 이때 .class파일은 JVM이 읽기 좋은 바이트코드로 작성되어있다.

컴파일된 코드 실행

Java에서 컴파일된 코드 실행은 다음의 단계를 거친다.

  1. 바이트코드로 작성된 .class파일을 JVM의 클래스로더(Class Loader)에게 전달한다.
  2. 클래스 로더는 동적로딩(Dynamic Loading)을 통해 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역(Runtime Data Area)이라 불리는 JVM의 메모리에 올린다.
  3. 실행엔진(Execution Engine)은 JVM메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 해석하고 실행한다.

실행명령어는 다음과 같다.

$ java <.class파일 이름>    //ex. java HelloWorld

JIT(Just-In-Time)컴파일러?

JIT컴파일러 전에 컴파일러인터프리터에 대해 알아보자. 둘은 컴퓨터가 해석하기 쉬운 코드로 변환해준다는 공통점이 있지만, 몇가지 중요한 차이점이 있다.

  • 인터프리터 :
    • 프로그램 실행 중(RunTime)에 동작하기 때문에 설치되어 있어야 한다.
    • 명령어를 한줄 씩 읽어서 해석하고 곧바로 실행한다.
    • 명령어 하나 하나의 해석은 빠르지만, 실행중에 해석을 하기 때문에 컴파일러에 비해 실행되는 속도가 느리다.
    • 최적화 과정이 없기 때문에 중복되는 명령어가 많을수록 성능이 떨어진다.
    • OS의 종류에 구애받지 않고 해당 OS에 맞는 인터프리터만 설치되어 있다면 실행할 수 있기 때문에 이식성이 높다.
  • (정적)컴파일러 :
    • 프로그램을 실행하기 전에 동작한다.
    • 전체 코드를 운영체제에 맞춰 해석과 최적화를 하고나서 디스크에 저장해 둔다.
    • 디스크에 저장되어있는 코드를 가져와서 실행만하면 되기 때문에 실행속도가 빠르다.
    • 운영체제가 달라지만 컴파일한 파일도 다시 최적화 컴파일을 진행해야 하는 경우가 많다.

이식성이 높다는 인터프리터의 장점은 Java가 추구하는 플랫폼 독립성과 일치하기 때문에 초기에 Java는 인터프리터 방식만을 사용하여 바이트코드를 해석하고 실행하였다. 하지만 컴파일러를 사용하는 다른 언어들에 비해 실행속도가 느리다는 평가는 피할 수 없었고 그것을 보완하기위해 나온 것이 바로 JIT컴파일러이다.

JIT컴파일러는 기존 자바의 인터프리터 방식의 단점을 보완하기 위해 등장한 동적 컴파일러이다. 인터프리터의 장점과 컴파일러의 장점을 융합하여 RunTime에 동작하며, 바이트 코드 전체를 컴파일하여 메모리 캐시에 저장해 둔다.

JVM은 내부적으로 특정 메서드가 얼마나 자주 호출되고 실행되는지 체크하고, 일정 기준을 넘으면 최적화를 수행 후 기계어로 변환하여 메모리 캐시에 저장해 둔다. 모든 코드를 캐시에 저장하지 않는 이유는 JIT컴파일러는 최적화까지 하기 때문에 전체코드를 컴파일하면 인터프리터보다 훨씬 느려진다.

인터프리터와 JIT컴파일러는 따로 따로 동작하는 것이 아니라 RunTime에 같이 동작하게 되는데 결국 이식성을 잃지 않으면서 실행속도를 높여주는 결과를 가져왔다. 이렇게 Java는 JIT컴파일러를 도입하면서 오늘날에는 실행속도가 느리다는 평가는 없어졌다.

JDK? JRE?

JDK와 JRE의 포함관계

JDK와 JRE는 포함관계이다. JDK의 풀네임은 Java Development Kit로 개발할 때 필요한 도구들과 실행환경을 제공한다. 대표적으로 작성한 코드를 바이트코드로 컴파일해주는 javac가 있다. JRE는 Java Runtime Environment로 자바의 실행환경만을 제공한다. 대표적으로 JVM과 기본적으로 제공하는 Java Class Library가 있다.

Java프로그램을 실행하기 위해서는 JRE를 설치하고, 개발을 하고 싶다면 JDK를 설치하여야 한다.

Reference