[Java] JVM이란?

Livenow·2020년 12월 3일
0

자바 바이트코드(.class 파일)는 JRE(Java Runtime Enviromnment) 위에서 동작합니다. 이 JRE에서 가장 중요한 요소는 자바 바이트코드를 해석하고 실행하는 JVM(Java Virtual Machine)입니다. JRE는 자바 API와 JVM으로 구성되며, JVM의 역할은 자바 애플리케이션을 클래스 로더(Class Loader)를 통해 읽어 들여서 자바 API와 함께 실행하는 것입니다. 개발 관련 도구는 JDK에서 제공합니다.


JVM에 들어가기 앞서 먼저 Java의 특징을 얘기하겠습니다.

Java의 큰 특징은 아무 하드웨어(CPU)던, 아무 운영체제(OS)던 상관없이 컴파일 된 코드(바이트코드)가 플랫폼 독립적이라는 점입니다.

이러한 특징을 구현하기 위해 JVM이 필요합니다.

단순하게 말하면 컴파일된 코드(바이트 코드)를 실행시켜주는 가상의 컴퓨터라고 생각하면 됩니다.

H/W, OS위에 실행되기 때문에 JVM자체는 플랫폼에 종속적입니다. ( 즉, 플랫폼에 따라 호환되는 JVM을 실행해야함)

JVM은 사용자 언어인 자바와 기계어 사이의 중간 언어인 자바 바이트코드를 사용합니다.


자바 코드 실행과정

자바 코드가 JVM에 전달되는 과정을 먼저 살펴보겠습니다.

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

즉, JVM은 자바 바이트 코드(.class 파일)를 OS에 특화된 코드로 변환(인터프리터와 JIT 컴파일러)하여 실행합니다.


JVM 구조


클래스 로더 시스템

.class 에서 바이트코드를 읽고 메모리에 저장

로딩

  • 클래스 파일을 가져와서 JVM의 메모리에 로드

링크

  • 검증 :
    • 클래스 로드 전 과정 중에서 가장 복잡하고 시간이 많이 걸리는 과정으로 읽어들인 클래스가 자바 언어 명세(JAVA Language Specification) 및 JVM 명세에 명시된 대로 구성되어 있는지 검사. 즉, .class 파일 형식이 유효한지 체크한다.
  • 준비
    • 클래스가 필요로 하는 메모리를 할당. 필요한 메모리란 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조들 등등을 말함. 즉, 클래스 변수(static 변수)와 기본값에 필요한 메모리 할당
  • 분석
    • 심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체함.

초기화

  • 클래스 변수들을 적절한 값으로 초기화. ( static 필드들을 설정된 값으로 초기화 등 )
  • Static 변수의 값을 할당한다. (static 블럭이 있다면 이때 실행된다.)

메모리(Runtime Data Area)

JVM이 OS위에서 실행되면서 할당받는 메모리 영역입니다.

이 중 PC 레지스터(PC Register), JVM 스택(JVM Stack), 네이티브 메서드 스택(Native Method Stack)스레드(Thread)마다 하나씩 생성되고, 힙(Heap), 메서드영역(Method Area)모든 스레드가 공유해서 사용됩니다.

PC 레지스터(PC Register)

  • 쓰레드 마다 쓰레드 내 현재 실행할 스택 프레임을 가리키는 포인터가 생성.
  • 현재 수행 중인 명령의 주소를 가지며 스레드가 시작될 때 생성되며 각 스레드마다 하나씩 존재

JVM 스택(JVM Stack)

  • 스택 프레임(Stack Frame)이라는 구조체를 저장하는 스택. 예외 발생 시 printStackTrace() 메서드로 보여주는 Stack Trace의 각 라인 하나가 스택 프레임을 표현. JVM 스택 역시 PC 레지스터와 마찬가지로 스레드가 시작될 때 생성되며 각 스레드마다 하나씩 존재.
  • 쓰레드 종료하면 런타임 스택도 사라짐.

네이티브 메서드 스택(Native Method Stack)

  • JAVA 외의 언어로 작성된 네이티브 코드를 위한 스택. JNI(JAVA Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 스택이 생성. (C면 C스택, C++이면 C++스택 생성)

힙(Heap)

  • 인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션(Garbage Collection) 대상. JVM 성능 등의 이슈에서 가장 많이 언급되는 공간. 힙 구성 방식이나 가비지 컬렉션 방법 등은 JVM 벤더(오라클, 아마존, Azul, .)들의 재량.

메서드 영역(Method Area)

  • 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성. JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드에 대한 정보, Static 변수, 메서드의 바이트 코드 등을 보관

런타임 상수 풀(Runtime Constant Pool)

  • JVM 동작에서 가장 핵심적인 역할을 수행하는 곳으로 JVM 명세에서도 따로 중요하게 기술. 각 클래스와 인터페이스의 상수 뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블로 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아서 참조.

실행 엔진

실행 엔진은 클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행합니다.
즉, 바이트코드를 기계어로 번역하여 실행합니다. 이때 사용하는 방식이 2가지인데 이를, 인터프리터JIT 컴파일러라 부릅니다.
인터프리터

  • 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행. 하나하나의 해석은 빠르지만 전체적인 실행 속도는 느리다는 단점을 가짐. JVM안에서 바이트코드는 기본적으로 인터프리터 방식으로 동작.

JIT(Just In Time) 컴파일러

  • 인터프리터의 단점을 보완하기 위해 도입된 방식으로 바이트 코드 전체를 컴파일하여 네이티브 코드로 변경하고 이후에는 해당 메서드를 더 이상 인터프리팅 하지 않고 네이티브 코드로 직접 실행하는 방식. 하나씩 인터프리팅하여 실행하는것이 아니라 바이트 코드 전체가 컴파일된 네이티브 코드를 실행하는 것이기 때문에 전체적인 실행 속도는 인터프리팅 방식보다 빠름.
  • 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 캐시에서 바로 꺼내어 실행하기 때문에 빠르게 수행됩니다. 하지만 JIT 컴파일러가 컴파일하는 과정은 바이트 코드를 하나씩 인터프리팅 하는 것보다 훨씬 오래 걸리기 때문에 JIT 컴파일러를 사용하는 JVM은 내부적으로 해당 메서드가 얼마나 자주 호출되고 실행되는지 체크하고, 일정 기준을 넘었을 때에만 JIT 컴파일러를 통해 컴파일하여 네이티브 코드를 생성합니다.
  • 즉, JIT 컴파일러는 같은 코드를 매번 해석하지 않고 실행할 때 컴파일을 하면서 해당 코드를 캐싱해버립니다. 이후엔, 바뀐 부분만 컴파일 하고 나머지는 캐싱된 코드를 사용 합니다. 결론적으로, 인터프리터의 속도를 개선할 수 있습니다.

GC(Garbage Collector)

  • 더이상 참조되지 않는 객체를 모아서 정리한다. (Stop the world)

JNI(Java Native Interface)

  • 자바 애플리케이션에서 C, C++, 어셈블리로 작성된 함수를 사용할 수 있는 방법 제공
  • Native 키워드를 사용한 메소드 호출
  • 네이티브 메소드 라이브러리
    • C, C++로 작성 된 라이브러리
  • 만들어서 사용하는 경우는 거의 없음.
profile
경험의 연장선

0개의 댓글