JVM

서정범·2023년 3월 29일
0

CS

목록 보기
4/7

JVM

JVM이란?

자바 가상 머신 JVM(Java Virtual Machine)은 자바 프로그램 실행환경을 만들어 주는 소프트웨어입니다.

JVM은 자바 실행 환경(JRE)에 포함되어 있습니다.
JVM 덕분에 가지고 있는 특징이 있다.

JAVA는 어떠한 플랫폼에 영향을 받지 않는다.

이러한 특징을 보여주는 그림이 아래와 같다.

따라서 JAVA는 어느 하드웨어(CPU)던, 어느 운영체제(OS)이던 상관없이 컴파일된 코드(바이트코드)가 플랫폼 독립적이다.

Java는 플랫폼에 종속적이지 않지만 JVM은 플랫폼에 종속적입니다.

Java는 컴파일된 바이트코드로 어떤 JVM에서도 동작시킬 수 있기 때문에 플랫폼에 의존적이지 않습니다. 하지만 반대로 자바 가상 머신(JVM)은 플랫폼에 의존적입니다.

즉, JVM은 H/W와 OS위에서 실행되기 때문에 JVM 자체는 플랫폼에 따라 호환되는 JVM을 실행시켜야 하는 것이다. (ex. 리눅스의 JVM과 윈도우의 JVM은 서로 다릅니다.)

JVM은 바이트코드를 명령어 단위로 읽어서 해석하는데, Interpreter 방식과 JIT 컴파일 방식 두가지 방식을 혼합하여 사용합니다.

Interpreter의 단점인 느린 속도를 보완하기 위해 나온 것이 JIT(Just In Time) 컴파일 방식이다.

바이트 코드를 JIT 컴파일러를 이용해 프로그램을 실제 실행하는 시점(바이트코드를 실행하는 시점)에 각 OS에 맞게 Native Code로 변환하여 실행해 속도를 개선하였습니다.

하지만, 바이트코드를 Native Code로 변환하는 데에도 비용이 소요되므로, JVM은 모든 코드를 JIT 컴파일러 방식으로 실행하지 않고, 인터프리터 방식을 사용하다가 일정 기준이 넘어가면 JIT 컴파일 방식을 명령어를 실행합니다.

네이티브 코드(Native Code)

네이티브 코드는 CPU와 운영체제가 직접적으로 실행할 수 있는 코드들을 말한다.

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

기존의 자바는 인터프리터 방식으로 명령어를 하나씩 실행하게끔 이루어져 있어 실행 속도가 느렸습니다.

하지만, 하드웨어가 발전하면서 자바 컴파일러도 JIT 컴파일러 방식으로 개선되어 속도적인 측면에서 상단한 개선을 이루었습니다.

JVM은 JIT 컴파일러라도 합니다. 또한, JIT 컴파일러는 같은 코드를 매번 해석하지 않고, 실행할 때 컴파일을 하면서 해당 코드를 캐싱해버립니다.

이후에는 바뀐 부분만 컴파일하고, 나머지는 캐싱된 코드를 사용합니다. 이렇게 JIT 컴파일러는 운영체제에 맞게 바이트 실행 코드로 한번에 변환하여 실행하기 때문에 이전의 자바 해석기보다 성능이 10 ~ 20배 정도 더 좋습니다.

자바 코드(Java Code) 실행 과정

JVM의 속을 들여다 보기 전에 JVM에게 코드가 전달되기 까지의 과정을 간단하게 살펴보자.

  1. 작성한 자바소스(Java Source), 즉 확장자가 .java인 파일을 자바 컴파일러(JAVA Compiler)를 통해 자바 바이트코드(Java Byte Code)로 컴파일한다.

    참고: javac.exe는 /bin 폴더에 있습니다.

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

동적 로딩(Dynamic Loading)

Java에서의 동적 로딩은 실행 시에 모든 클래스가 로딩되지 않고 필요한 시점에 클래스를 로딩하여 사용할 수 있도록 해주는 방식입니다.

장점

  • 일부 클래스가 변경되어도 전체 어플리케이션을 다시 컴파일 하지 않아도 된다.
  • 변경사항이 발생해도 비교적 적은 작업만으로도 처리할 수 있는 유연한 어플리케이션을 작성할 수 있다.
  • C언어와 다르게 코드 블럭이 이곳 저곳 위치해 있어도 된다.
  • 다형성 같은 객체지향 개념이 적용될 수 있게 해준다.

단점

  • 실행시 연결된 부분에 대한 판단을 해야 하므로 속도 측면에서 불리

클래스 동적 로딩 종류

  • 로드 타임 동적 로딩: 하나의 클래스를 로딩하는 과정에서 필요한 다른 클래스를 동적으로 로딩하는 것
  • 런타임 동적 로딩: 코드를 실행하는 순간에 필요한 클래스를 로딩하는 것

이제 작성한 자바 소스가 어떻게 동작하는지 파악했으니 JVM의 속을 들여다 보자.

JVM 동작 원리

순서는 다음과 같다.

  1. 클래스 로더(Class Loader)
  2. 런타임 데이터 영역(Runtime Data Area)
  3. 실행 엔진(Excution Engine)

클래스 로더(Class Loaer)

클래스 로더의 특징은 크게 보면 5가지로 나눌 수 있다.

  1. 계층 구조
  2. 위임 모델
  3. 가시성 제한
  4. 언로드(Unload) 불가
  5. 이름공간(Name Space)

1. 계층 구조

클래스 로더는 단순하게 하나로 이루어져 있지 않습니다. 위의 그림처럼 여러 클래스 로더끼리 부모-자식 관계를 이루고 있어서 계층적인 구조로 되어 있다.

  • 부트스트랩 클래스 로더(Bootstrap Class Loader)
    • 최상위 클래스로더로 유일하게 JAVA가 아니라 네이티브 코드로 구현이 되어있다.
    • JVM이 실행될 때 같이 메모리에 올라간다
    • Object 클래스를 비롯하여 JAVA API들을 로드한다.
  • 익스텐션 클래스 로더(Extension Class Loader)
    • 기본 Java API를 제외한 확장 클래스들을 로드한다. (다양한 보안 확장기능 로드)
  • 시스템 클래스 로더(System Class Loader)
    • 부트스트랩과 익스텐션 클래스로더가 JVM 자체의 구성요소들을 로드한다면, 시스템 클래스 로더는 어플리케이션의 클래스들을 로드한다.
    • 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드한다.
  • 사용자 정의 클래스 로더(User-Defined Class Loader)
    • 어플리케이션 사용자가 직접 코드상에 생성하여 사용하는 클래스 로더

웹 어플리케이션 서버(Web Aplication Server: WAS)와 같은 프레임 워크는 웹 어플리케이션, 엔터프라이즈 어플리케이션이 서로 독립적으로 동작하게 하기 위해서 사용자 정의 클래스 로더들을 사용하여 클래스 로더의 위임 모델을 통해 어플리케이션의 독립성을 보장한다고 합니다.

따라서 WAS의 클래스 로더 구조는 WAS 벤더마다 조금씩 다른 형태의 계층 구조를 사용하고 있다고 합니다.

EA(Enterprise Application)

EA(Enterprise Application)는 비즈니스 또는 정부와 같은 회사 환경에서 작동하도록 설계도니 대규모 소프트웨어 시스템 플랫폼입니다.

EA는 복잡하고 확장가능하며 구성 요소 기반, 분산 및 미션 크리티컬입니다.

참고:

2. 위임 모델

위임 모델이란 바이트코드를 넘겨받은 클래스 로더가 필요한 클래스를 로드할 때 혹은 실행엔진에서 명령어 단위로 바이트코드를 실행하다가 처음으로 참조하는 클래스에 대해 클래스 로더에게 로드를 요청할 때 로드를 요청받은 클래스 로더는 다음 순서대로 요청받은 클래스가 있는지 확인합니다.

  1. 클래스 로더 캐시
  2. 상위 클래스 로더
  3. 자기 자신

이전에 로드된 클래스인지 클래스 로더 캐시를 확인하고, 없으면 상위 클래스 로더를 하나씩 거슬러 올라가며 확인하는데 이 때 중요한 점은 올라가는 도중에 클래스를 발견하더라도 부트스트랩 클래스 로더까지 확인을 해서 부트스트랩 클래스 로더에도 해당 클래스가 존재하면 부트스트랩 클래스 로더에 있는 클래스를 로드한다는 점입니다.

즉, 클래스를 로드하려고 하고 캐시 클래스 로더에 있을 때 찾고자 하는 클래스를 찾기 위해 사용자 정의 클래스를 전부 탐색한 후에 찾으면 부트 스트랩 클래스 로더까지 확인하고 그 다음 사용자 정의 클래스 로드로 돌아오는 것이다.

예를 들어, 요청 받은 클래스가 시스템 클래스 로더에 존재하여도 부트스트랩 클래스 로더까지 확인 하고 부트스트랩에도 해당 클래스가 존재하면 부트스트랩에 있는 클래스를 로드하게 되는 것입니다.

이러한 특성으로 인해서 아키택처를 구성하는 수준의 개발자라면 JVM에 대한 지식이 꼭 필요한 것이다.(참고로 IBM에서 만든 어떤 WAS는 옵션을 통해서 중간에 클래스를 발견하면 그 이상 로드를 멈추게 했다고 합니다.)

마지막으로 부트스트랩 클래스 로더에도 해당 클래스가 없으면 로드를 요청받은 클래스 로더가 파일 시스템에서 해당 클래스를 찾는 것으로 마무리 됩니다.

(파일 시스템에서도 찾지 못하면 클래스를 차지 못했다는 예외가 발생)

3. 가시성 제한

클래스 로더가 클래스 로드를 요청받았을 때 위임모델에 의해서 클래스 로더 캐시를 확인하고 없으면 상위 클래스 로더를 확인하는데 이 때 하위 클래스 로더에 있는 클래스는 확인이 불가능한 특성이 바로 가시성 제한입니다.

4. 언로드(Unload) 불가

언로드 불가는 말 그대로 클래스를 로드하는 것은 가능하지만 반대로 언로드(Unload)하는 것은 불가능하다는 특성입니다.

5. 이름 공간(Name Space)

네임스페이스각 클래스 로더들이 가지고 있는 공간으로써 로드된 클래스를 보관하는 공간입니다. 클래스를 로드할 때 위임 모델을 통해서 상위 클래스 로더들을 확인하는데 그 때 확인하는 공간이 바로 네임스페이스입니다.

네임스페이스에 보관되는 기준은 FQCN(Fully Qualified Class Name)을 기준으로 보관되는데 FQCN이란 패키지명까지 포함되어있는 식별자를 뜻합니다.

각각의 클래스 로더가 각자 네임스페이스를 가지고 있기 때문에 패키지명까지 같은 즉, FQCN이 같은 클래스라도 네임스페이스가 다르면(다른 클래스가 로드한 클래스이면) 다른 클래스로 간주하는 것이다.

(이 특성을 이용하면 언로드 대신해서 로드한 클래스 로더를 제거하면 마치 언로드한 것과 같은 효과를 줄 수 있습니다.) => 이것도 정확한 의미를 모르겠네요..
(무엇이 이 역할을 해주는 것인데…?)

클래스 로드 과정

클래스 로더가 아직 로드되지 않은 클래스를 로드하는 과정을 간단하게 살펴보면

  1. 로드: 클래스 파일을 가져와서 JVM의 메모리에 로드한다.
  2. 검증: 클래스 로드 전 과정 중에서 가장 복잡하고 시간이 많이 걸리는 과정으로 읽어들인 클래스가 자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 구성되어 있는지 검사한다.
  3. 준비: 클래스가 필요로 하는 메모리를 할당한다. 필요한 메모리란 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조들 등등을 말한다.
  4. 분석: 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.

심볼릭 레퍼런스(Symbolic Reference)

참고하는 클래스의 특정 메모리 주소를 참조 관계로 구성한 것이 아닌 참조하는 대상의 이름을 참조하는 것

다이렉트 레퍼런스(Direct Reference)

참조하는 클래스의 특정 메모리 주소를 참조하는 것

  1. 초기화: 클래스 변수들을 적절한 값으로 초기화한다. (static 필드들을 설정된 값으로 초기화 등)

런타임 데이터 영역(Runtime Data Area)

JVM이 OS위에서 실행되면서 할당받은 메모리 영역이 바로 런타임 데이터 영역(Runtime Data Area)입니다. 이 영역은 크게 5가지 조금 세분화하면 6가지 영역으로 나눌 수 있다.

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

각 영역을 정리해보면

  • PC 레지스터(PC Register): PC(Program Counter) 레지스터는 현재 수행 중인 명령의 주소를 가지며 스레드가 시작될 때 생성되며 각 스레드마다 하나씩 존재한다.
  • JVM 스택(JVM StacK): 스택 프레임(Stack Frame)이라는 구조체를 저장하는 스택이다. 예외 발생시 printStackTrace() 메서드로 보여주는 Stack Trace의 각 라인 하나가 스택 프레임을 표현한다. JVM 스택 역시 PC 레지스터와 마찬가지로 스레드가 시작될 때 생성되며 각 스레드마다 하나씩 존재한다.
  • 네이티브 메서드 스택(Native Method Stack): Java 외의 언어로 작성된 네이티브 코드를 위한 스택이다. JNI(JAVA Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 스택이 생성된다.
  • : 인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션(Garbage Collection) 대상이다. JVM 성능 등의 이슈에서 가장 많이 언급되는 공간이다. 힙 구성 방식이나 가비지 컬렉션 방법 등은 JVM 벤더들의 재량이다.
  • 메서드 영역(Method Area): 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다. JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드에 대한 정보, Static 변수, 메서드의 바이트 코드 등을 보관한다.

    메서드 영역은 JVM 벤더마다 다양한 형태로 구현할 수 있으며, 오라클 핫스팟 JVM(HotSpot JVM)에서는 흔히 Permanent Area, 혹은 Permanent Generation(PermGen) 이라고 불린다. 메서드 영역에 대한 가비지 컬렉션은 JVM 벤더의 선택 사항이다.

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

실행 엔진(Execution Engine)

실행 엔진은 클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행한다. (CPU가 기계 명령어를 하나씩 실행하듯이)

바이트 코드의 각 명령어는 1바이트 크기의 OpCode(Operation Code)추가 피연산자로 이루어져 있다. 실행 엔진은 하나의 OpCode를 가져와서 피연산자와 작업을 수행한 다음, 그 다음 OpCode를 수행하는 식으로 동작합니다.

이 수행 과정에서 실행 엔진은 바이트 코드를 기계가 실행할 수 있는 형태로 변경하는데 다음 두 가지 방식으로 변경한다.

  • 인터프리터: 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하나하나의 해석은 빠르지만 전체적인 실행 속도는 느리다는 단점을 가진다. JVM안에서 바이트 코드는 기본적으로 인터프리터 방식으로 동작한다.
  • JIT 컴파일러(Just-In-Time Compiler): 인터프리터의 단점을 보완하기 위해 도입된 방식으로 바이트 코드 전체를 컴파일하여 네이티브 코드로 변경하고 이후에는 해당 메서드를 더 이상 인터프리팅 하지 않고 네이티브 코드로 직접 실행하는 방식이다. 하나씩 인터프리팅하여 실행하는 것이 아니라 바이트 코드 전체가 컴파일된 네이티브 코드를 실행하는 것이기 때문에 전체적인 실행 속도는 인터프리팅 방식보다 빠르다.

네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 캐시에서 바로 꺼내어 실행하기 때문에 빠르게 수행된다.

하지만 JIT 컴파일러가 컴파일 하는 과정은 바이트 코드를 하나씩 인터프리팅 하는 것보다 훨씬 오래 걸리기 때문에 JIT 컴파일러를 사용하는 JVM은 내부적으로 해당 메서드가 얼마나 자주 호출되고 실행되는지 체크하고, 일정 기준을 넘었을 때에만 JIT 컴파일러를 통해 컴파일하여 네이티브 코드를 생성한다.

JIT 컴파일러를 통한 컴파일 과정은 바이트 코드를 바로 네이티브 코드로 만드는 것이 아니라 안에서 IR(Intermediate Representation)로 변환하여 최적화를 수행하고 그 다음에 네이티브 코드로 변환하는 과정을 거칩니다.

오라클 핫스팟 VM은 핫스팟 컴파일러라고 불리는 JIT 컴파일러를 사용하는데 내부적으로 프로파일링(Profiling)을 통해 가장 컴파일이 필요한 부분, 즉 '핫스팟'을 찾아낸 다음, 이 핫스팟을 컴파일 하기 때문에 핫스팟이라 부른다고 합니다. 핫스팟 VM은 한번 컴파일된 바이트 코드라도 해당 메소드가 더이상 자주 불리지 않는다면, 캐시에서 네이티브 코드를 덜어내고 다시 인터프리터 모드로 동작합니다.

핫스팟 VM은 서버 VM과 클라이언트 VM으로 나뉘어 있고, 각각 다른 JIT 컴파일러를 사용합니다. 각각은 동일한 런타임을 사용하지만, 다른 JIT 컴파일러를 사용합니다. 서버 VM에서 사용되는 컴파일러가 더 복잡하고 다양한 성능 최적화 기법을 사용하고 있다고 합니다.


Reference

profile
개발정리블로그

0개의 댓글