JVM 구조

growdeveloper ㅣ 2021. 4. 19. 18:28

JVM 구조

  • 실행될 클래스 파일을 메로리에 로드 후 초기화 작업 수행
  • 메서드와 클래스 변수들을 해당 메로리 영역에 배치
  • 클래스 로드가 끝난 후 JVM은 main 매소드를 찾아 지역변수, 객체 변수, 참조 변수를 스택에 쌓음
  • 다음 라인을 진행하면서 상황에 맞는 작업 수행(함수 호출, 객체 할당 등)

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

클래스 로더

자바는 동적 로드, 즉 컴파일 타임이 아니라 런타임에 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크하는 특징 이 있다. 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더이다. 자바 클래스 로더의 특징은 다음과 같다.

  • 계층 구조 : 클래스 로더끼리 부모 - 자식 관계를 이루어 계층 구조로 생성된다. 최상위 클래스 로더는 부트스트랩 클래스 로더 (Bootstrap Class Loader)이다.
  • 위임 모델 : 계층 구조를 바탕으로 클래스 로더끼리 로드를 위임하는 구조로 동작한다. 클래스를 로드할 때 먼저 상위 클래스 로더를 확인하여 상위 클래스 로더에 있다면 해당 클래스를 사용하고, 없다면 로드를 요청받은 클래스 로더가 클래스를 로드한다.
  • 가시성(Visibility) 제한 : 하위 클래스 로더는 상위 클래스 로더의 클래스를 찾을 수 있지만, 상위 클래스 로더는 하위 클래스 로더의 클래스를 찾을 수 없다.
  • 언로드 불가 : 클래스 로더는 클래스를 로드할 수는 없지만 언로드 할 수는 없다. 언로드 대신, 현재 클래스 로더를 삭제하고 아예 새로운 클래스 로더를 생성하는 방법을 사용할 수 있다.
  • 네임스페이스

각 클래스 로더는 로드된 클래스들을 보관하는 네임스페이스(namespace)를 갖는다. 클래스를 로드할 때 이미 로드된 클래스인지 확인하기 위해서 네임스페이스에 보관된 FQCN(Fully Qualified Class Nmae)을 기준으로 클래스를 찾는다. 비록 FQCN이 같더라도 네임스페이스가 다르면, 즉 다른 클래스로더가 로드한 클래스이면 다른 클래스로 간주된다.


1.계층 구조

 

 

 

클래스 로더가 클래스 로드를 요청 받으면, 클래스 로더 캐시, 상위 클래스 로더, 자기 자신의 순서로 해당 클래스가 있는지 확인한다. 즉, 이전에 로드된 클래스인지 클래스 로더 캐시를 확인하고, 없으면 상위 클래스 로더를 거슬로 올라가며 확인 한다. 부트 스트랩 클래스 로더까지 확인해도 없으면 요청받은 클래스 로더가 파일 시스템에서 해당 클래스를 찾는다.

 

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

웹 애플리케이션 서버(WAS)와 같은 프레임 워크는 웹 애플리케이션들, 엔터프라이즈 애플리케이션들이 서로 독립적으로 동작하게 하기 위해 사용자 정의 클래스 로더를 사용한다. 즉, 클래스 로더의 위임 모델을 통해 애플리케이션의 독립성을 보장하고 있는 것이다. 이와 같은 WAS의 클래스 로더 구조는 WAS 벤더마다 조금씩 다른 형태의 계층 구조를 사용하고 있다. 따라서 WAS의 클래스 로더 구조는 WAS 벤더마다 조금씩 다른 형태의 계층 구조를 사용하고 있다.


2.위임모델

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

  • 클래스 로더 캐시
  • 상위 클래스 로더
  • 자기 자신

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

 

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

 

이러한 특성으로 인해서 아키텍처를 구성하는 수준의 개발자라면 JVM에 대한 지식이 꼭 필요한 것이다

(IBM 에서 만든 어떤 WAS는 옵션을 통해서 중간에 클래스를 발견함녀 부트스트랩 클래스 로더까지 올라가지 않도록 할 수있다고 한다)

 

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

 

3.가시성 제한

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

 

4. 언로드(Unload) 불가

언로드 불가 역시 간단한 개념으로 말 그대로 클래스를 로드하는 것은 가능하지만 반대로 언로드 (Unload) 하는 것은 불가능 하다는 특성입니다

 

5. 이름 공간(Name space)

네임 스페이스란 각 클래스 로더들이 가지고 있는 공간으로 써 로드된 클래스를 보관하는 공간이다. 클래스를 로드할 때 위임 모델을 통해서 상위 클래스 로더들을 확인하는데 그 때 확인하는 공간이 바로 네임스페이스 입니다. 네임스페이스에 보관되는 기준은 FQCN(Fully Qualified Class Name)을 기준으로 보관되는데 FQCN이란 패키지명까지 포함되어 있는 식별자를 뜻합니다. 각각의 클래스 로더가 각자 네임스페이스를 가지고 있기 떄문에 패키지 명까지 같은 즉, FQCN이 같은 클래스라도 네임스페이스가 다르면(다른 클래스 로더라 로드한 클래스이면) 다른 클래스로 간주하는 것이다. (이 특성을 이용하면 언로드를 대신해서 로드한 클래스 로더를 제거하면 마치 언로드한 것과 같은 효과를 줄 수 있습니다.)


클래스 로더가 아직 로드되지 않은 클래스를 찾으면, 다음 그림과 같은 과정을 거쳐 클래스를 로드하고 링크하고 초기화한다.

 

 

  • 로드 : 클래스를 파일에 가져와서 JVML의 메모리에 로드한다.
  • 검증(Verifying): 읽어 들인 클래스가 자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사한다. 클래스 로드의 전 과정 중에서 가장 까다로운 검사를 수행하는 과정으로서 가장 복잡하고 시간이 많이 걸린다. JVM TCK의 테스트 케이스 중에서 가장 많은 부분이 잘못된 클래스를 로드하여 정상적으로 검증 오류를 발생시키는지 테스트하는 부분이다.
  • 준비(Preparing):클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메서드, 인터페이스 들을 나타내는 데이터 구조를 준비한다.
  • 분석(Resolving):클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
  • 초기화: 클래스 변수들을 적절한 값으로 초기화한다. 즉,static initializer들을 수행하고, static 필드들을 설정된 값으로 초기화 한다.

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

런타임 데이터 영역은 jvm이라는 프로그램이 운영체제 위에서 실행되면서 할당받는 메모리 영역이다. 런타임 데이터 영역은 6개의 영역으로 나눌 수 있다. 이중 pc 레지스터(PC Register), JVM 스택(JVM Stack), 네이티브 메서드 스택(Native Method Stack)은 스레드 마다 하나씩 생성되며 힙(Heap), 메서드 영역(Method Area), 런타임 상수 풀 (Runtime Constant Pool)은 모든 스레드가 공유해서 사용한다.

 

  • 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가 기계 명령어를 하나씩 실행하듯이) 실행 엔진은 자바 바이트코드를 명령어 단위로 읽어서 실행한다.CPU가 기계 명령어를 하나씩 실행하는 것과 비슷하다. 바이트코드를 가 명령어는 1 바이트 짜리 OpCode와 추가 피 연산자로 이루어져 있으며, 실행 엔진은 하나의 OpCode를 가져와서 피연산자와 함께 작업을 수행한 다음, 다음 OpCode를 구행하는 식으로 동작한다.

 

그런데 자바 바이트코드는 기계가 바로 수행할 수 있는 언어보다는 비교적 인간이 보기 편한 형태로 기술된 것이다. 그래서 실행 엔진은 이와 같은 바이트코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경하여, 그 방식은 다음 두 가지가 있다.

  • 인터프리터:바이트 코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하나씩 해석하고 실행하기 때문에 바이트코드 하나하나의 해석은 빠른 대신 인터프리팅 결과의 실행은 느리다는 단점을 가지고 있다. 흔히 얘기하는 인터프리터언어의 단점을 그대로 가지는 것이다. 즉, 바이트 코드라는 언어는 기본적으로 인터프리터 방식으로 동작한다.
  • JIT(Just-In-Time) 컴파일러 : 인터프린터의 단점을 보완하기 위해 도입된 것이 JIT 컴파일러 이다. 인터프리터 방식으로 실행하다가 적절한 시점에 바이트 코드 전체를 컴파일하여 네이티브 코드로 변경하고 이후에는 해당 메서드를 더 이상 인터 프리팅하지 않고 네이티브 코드로 직접 실행하는 방식이다. 네이티브 코드를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 수행 하게 된다.

JIT 컴파일러가 컴파일하는 과정은 바이트 코드를 하나씩 인터프리팅하는 것보다 훨씬 오래 걸리므로, 만약 한번만 실행되는 코드라면 컴파일하지 않고 인터프리팅하는 것이 훨씬 유리하다. 따라서, JIT 컴파일러를 사용하는 JVM들은 내부적으로 해당 메서드가 얼마나 자주 수행되는지 체크하고, 일정 정도를 넘을 때에만 컴파일을 수행한다.

 

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

클라이언트 VM과 서버 VM은 각각 오라클 핫스팟 VM을 실행할 때 입력하는 -client, -server 옵션으로 실행된다, 클라리언트 vm과 서버 vm은 동일한 런타임을 사용하지만, 위 그림과 같이 다른 jit 컴파일러를 사용한다. 서버 vm 에서 사용하는 advanced Dynamic Optimizing Compiler 가 더 복잡하고 다양한 성능 최적화 기법을 사용하고 있다. 

 

IBM JVM은 JIT 컴파일러뿐만 아니라 IBM JDK 6 부터 AOT(Ahead-Of-Time) 컴파일러라는 기능을 도입했다. 이는 한번 컴파일된 네이티브 코드를 여러 JVM이 공유 캐시를 통해 공유해서 사용하는 것을 의미한다. 즉, AOT 컴파일러를 통해 이미 컴파일된 코드는 다른 JVM 에서도 컴파일하지 않고 사용할 수 있게 하는 것이다. 또한, 아예 AOT 컴파일러를 이용하여 JXE(Java Executeable) 라는 파일 포맷으로 프리컴파일(pre-compile)된 코드를 작성하여 빠르게 실행하는 방법도 제공하고 있다.

 

자바 성능 개선의 많은 부분은 바로 이 실행 엔진을 개선하여 이뤄지고 있다. jit 컴파일러는 물론 다양한 최적화 기법을 도입하여 jvm의 성능은 계속해서 향상되고 있다. 초창기 jvm과 지금은 jvm 은 이 실행 엔진에서 큰 차이가 있다.

 

 

 

'자바' 카테고리의 다른 글

자바의 특징  (0) 2022.05.08
JAVA public, private, protected 접근자의 차이점  (0) 2021.08.12
스레드  (0) 2021.04.08
추상 메소드  (0) 2021.04.08
클래스와 객체  (0) 2021.03.16