[CS] JVM의 이해

Hyeonjun·2022년 9월 29일
0

JVM의 특징

  • 스택 기반의 가상 머신
    - 대표적인 컴퓨터 아키텍처들은 레지스터 기반으로 동작함.
    - JVM은 스택 기반으로 동작
  • 심볼릭 레퍼런스
    - 기본 자료형(Primitive data type)을 제외한 모든 타입(클래스와 인터페이스)을 명시적인 메모리 주소 기반의 레퍼런스가 아닌 심볼릭 레퍼런스를 통해 참조함.
  • 가비지 컬렉션
    - 클래스 인스턴스는 사용자 코드에 의해 명시적으로 생성되고 가비지 컬렉션에 의해 자동으로 파괴됨.
  • 기본 자료형을 명확하게 정의하여 플랫폼 독립성 보장
    - 전통적인 언어들(C, C++)은 플랫폼에 따라 int형의 크기가 변함.
    - JVM은 기본 자료형을 명확하게 정의해 플랫폼 독립성을 보장함.
  • 네트워크 바이트 오더
    - 리틀 엔디안/빅 엔디안 사이에서 플랫폼 독립성을 유지하기 위해 네트워크 전송시 바이트 오더인 네트워크 바이트 오더를 사용함.
    - 네트워크 바이트 오더는 빅 엔디안을 사용.

자바 바이트코드

  • WORA를 구현하기 위해 JVM은 사용자언어인 자바와 기계어 사이의 중간 언어인 자바 바이트코드를 사용한다.
  • 자바 바이트코드는 자바 코드를 배포하는 가장 작은 단위.
  • JVM은 자바 바이트코드를 실행하는 실행기.
  • 자바 컴파일러는 C/C++등의 컴파일러처럼 고 수준 언어를 기계어, 즉 직접적인 CPI명령으로 변환하는 것이 아니라, 개발자가 이해하는 자바 언어를 JVM이 이해하는 자바 바이트 코드로 번역한다.
  • 자바 바이트코드는 플랫폼 의존적인 코드가 없기 때문에 JVM(정확히는 JRE)이 설치된 장비라면 CPU나 운영체제가 달라도 실행할 수 있고, 컴파일 결과물의 크기가 소스코드의 크기와 크게 다르지 않으므로 네트워크로 전송하여 실행하기 쉽다.
  • javap : 자바 역 어셈블러 (disassembler)
    - 이를 통한 결과물을 자바 어셈블리라고 함.
    - javap -c로 활용 가능

클래스 파일 포맷

  • JVM에서 한 메서드의 크기는 65535바이트를 넘을 수 없다.
  • TCK(Technology Compatibility Kit)
    - JVM이 명세를 만족하는지 검증하기 위한 테스트 툴
    - TCK를 통과해야 JVM이라고 이름붙일 수 있음.
  • JCP
    - 새로운 자바 기술 명세를 제안하고 제공함.

JVM구조

  1. 클래스로더가 컴파일된 자바 바이트코드를 런타임 데이터 영역(Runtime Data Areas)에 로드하고
  2. 실행 엔진(Execution Engine)이 자바 바이트 코드를 실행한다.

클래스 로더

  • 자바는 동적 로드 특징이 있다.
    - 컴파일타임이 아니라 런타임에 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크함.
  • 동적 로드를 담당하는 부분이 JVM의 클래스 로더이다.
  • 자바 클래스로더의 특징
    - 계층구조
    - 클래스 로더끼리 부모-자식 관계를 이루어 계층 구조로 생성됨
    - 최상위 클래스 로더는 부트스트랩 클래스로더이다.
    - 위임 모델
    - 계층 구조를 바탕으로 클래스 로더끼리 로드를 위임하는 구조로 동작
    - 클래스를 로드할 때 먼저 상위 클래스 로더를 확인하여 상위 클래스 로더에 있다면 해당 클래스를 사용함
    - 없다면 로드를 요청받은 클래스 로더가 클래스를 로드한다.
    - 가시성(Visiblity) 제한
    - 하위 클래스 로더는 상위 클래스 로더의 클래스를 찾을 수 있지만, 상위 클래스 로더는 하위 클래스 로더의 클래스를 찾을 수 없다.
    - 언로드 불가
    - 클래스 로더는 클래스를 로드할 수 있지만 언로드할 수는 없다.
    - 언로드 대신 현재 클래스 로더를 삭제하고 아예 새로운 클래스 로더를 생성하는 방법을 사용할 수 있다.
  • 각 클래스 로더는 로드된 클래스를 보관하는 네임스페이스를 갖는다.
  • 클래스를 로드할 때 이미 로드된 클래스인지 확인하기 위해서 네임스페이스에 보관된 FQCN(Full Qualified Class Name)을 기준으로 클래스를 찾는다.
  • 비록 FQCN이 같더라도 네임스페이스가 다르면, 즉 다른 클래스 로더가 로드한 클래스이면 다른 클래스로 간주된다.
  • 클래스로더가 클래스 로드를 요청받으면, 클래스 로더 캐시, 상위 클래스 로더, 자기 자신의 순서로 해당 클래스가 있는지 확인한다.
  • 이전에 로드된 클래스인지 클래스 로더 캐시를 확인하고, 없으면 상위 클래스 로더를 거슬러 올라가면서 확인한다.
  • 부트스트랩 클래스 로더까지 확인해도 없으면 요청받은 클래스 로더가 파일 시스템에서 해당 클래스를 찾는다.
    - 부트스트랩 클래스 로더
    - JVM을 기동할 때 생성.
    - Object 클래스들을 비롯하여 자바 API들을 로드한다.
    - 다른 클래스 로더와 달리 자바가 아니라 네이티브 코드로 구현되어 있다.
    - 익스텐션 클래스 로더 (Extension Class Loader)
    - 기본 자바 API를 제외한 확장 클래스들을 로드한다.
    - 다양한 보안 확장 기능들을 여기에서 로드하게 된다.
    - 시스템 클래스 로더 (System Class Loader)
    - 부트스트랩 클래스 로더와 익스텐션 클래스 로더가 JVM 자체의 구성 요소들을 로드하는 것이라 한다면, 시스템 클래스 로더는 애플리케이션의 클래스들을 로드한다고 할 수 있다.
    - 사용자가 지정한 $CLASSPATH내의 클래스들을 로드한다.
    - 사용자 정의 클래스 로더 (User-Defined Class Loader)
    - 애플리케이션 사용자가 직접 코드 상에서 생성해서 사용하는 클래스 로더
  • 웹 애플리케이션 서버(WAS)와 같은 프레임워크는 웹 애플리케이션들, 엔터프라이즈 애플리케이션들이 서로 독립적으로 동작하게 하기 위해 사용자 정의 클래스 로더를 사용한다.
    - 클래스로더의 위임 모델을 통해 애플리케이션의 독립성을 보장하는 것.
  • 이와 같은 WAS의 클래스 로더 구조는 WAS 벤더마다 조금씩 다른 형태의 계층 구조를 사용하고 있다.

클래스 로더가 로드되지 않은 클래스를 찾는 경우

  • 클래스 로더가 아직 로드되지 않은 클래스를 찾으면, 다음 그림과 같은 과정을 거쳐 클래스를 로드하고 링크하고 초기화한다.
  • 로드
    - 클래스를 파일에서 가져와서 JVM의 메모리에 로드한다.
  • 검증
    - 읽어들인 클래스가 자바 언어 명세 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사한다.
    - 클래스 로드의 전 과정 중에서 가장 까다로운 검사를 수행하는 과정으로서 가장 복잡하고 시간이 많이 걸린다.
    - JVM TCK의 테스트 케이스 중에서 가장 많은 부분이 잘못된 클래스를 로드하여 정상적으로 검증 오류를 발생시키는지 테스트 하는 부분이다.
  • 준비
    - 클래스가 필요로하는 메모리를 할당하고, 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조를 준비한다.
  • 분석
    - 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
  • 초기화
    - 클래스 변수들을 적절한 값으로 초기화한다.
    - static initializer들을 수행하고, static 필드들을 설정된 값으로 초기화한다.
    JVM 명세는 이들 작업들에 대해 명시하고 있으나, 작업에 따라서 수행 시점은 유연하게 적용될 수 있도록 하고 있다.

런타임 데이터 영역

JVM이라는 프로그램이 운영체제 위에서 실행되면서 할당받는 메모리 영역이다.

  • 런타임 데이터 영역은 6개의 영역으로 나눌 수 있다.
  • 이중 PC 레지스터(PC Register), JVM 스택, 네이티브 메서드 스택(Native Method Stack)은 스레드마다 하나씩 생성되며 힙(Heap), 메서드 영역(Method Area), 런타임 상수 풀(Runtime Constant Pool)은 모든 스레드가 공유해서 사용한다.

런타임 데이터 영역의 구성요소

  • PC 레지스터
    - Program Counter 레지스터는 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다.
    - PC 레지스터는 현재 수행중인 JVM 명령의 주소를 갖는다.

  • JVM 스택
    - JVM 스택은 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다.
    - 스택 프레임(Stack Frame)이라는 구조체를 저장하는 스택으로, JVM은 오직 JVM 스택에 스택 프레임을 추가하고(push) 제거하는(pop) 동작만 수행한다.
    - 예외 발생 시 printStackTrace()등의 메서드로 보여주는 Stack Trace의 각 라인은 하나의 스택 프레임을 표현한다.

  • 스택 프레임
    - JVM 내에서 메서드가 수행될 때마다 하나의 스택 프레임이 생성되어 해당 스레드의 JVM스택에 추가되고 메서드가 종료되면 스택 프레임이 제거된다.
    - 각 스택 프레임은 지역 변수 배열(Local Variable Array), 피연산자 스택(Operand Stack), 현재 실행 중인 메서드가 속한 클래스의 런타임 상수 풀에 대한 레퍼런스를 갖는다.
    - 지연 변수 배열, 피연산자 스택의 크기는 컴파일 시에 결정되기 때문에 스택 프레임의 크기도 메서드에 따라 크기가 고정된다.

  • 지역 변수 배열 (Local Variable Array)
    - 0부터 시작하는 인덱스를 가진 배열이다.
    - 0-은 메서드가 속한 클래스 인스턴스의 this 레퍼런스이고, 1부터는 메서드에 전달된 파라미터들이 저장되며, 메서드 파라미터 이후에는 메서드의 지역 변수들이 저장된다.

  • 피연산자 스택(Operand Stack)
    - 메서드의 실제 작업 공간이다.
    - 각 메서드는 피연산자 스택과 지역 변수 배열 사이에서 데이터를 교환하고, 다른 메서드 호출 결과를 추가하거나(push) 꺼낸다(pop).
    - 피연산자 스택 공간이 얼마나 필요한지는 컴파일할 때 결정할 수 있으므로, 피연산자 스택의 크기도 컴파일 시에 결정된다.

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

  • 메서드 영역
    - 모든 스레드가 공유하는 영역으로 JVM이 시작될 떄 생성된다.
    - JVM이 읽어들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, static 변수, 메서드의 바이트코드 등을 보관한다.
    - 메서드 영역은 JVM 벤더마다 다양한 형태로 구현할 수 있으며, 오라클 핫스팟 JVM(HotSpot JVM)에서는 흔히 Permanent Area 혹은 Permanent Generation(PermGen)이라고 불린다.
    - 메서드 영역에 대한 가비지 컬렉션은 JVM 벤더의 선택 사항이다.

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


  • - 인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션의 대상이다.
    - JVM 성능 등의 이슈에서 가장 많이 언급되는 공간이다.
    - 힙 구성 방식이나 가비지 컬렉션 방법 등은 JVM 벤더의 재량이다.

실행 엔진

  • 클래스 로더를 통해 JVM 내의 런타임 데이터 영역에 배치된 바이트코드는 실행 엔진에 의해 실행된다.
  • 실행 엔진은 자바 바이트코드를 명령어 단위로 읽어서 실행한다.
  • CPU가 기계 명령어를 하나씩 실행하는 것과 비슷하다.
  • 바이트코드의 각 명령어는 1바이트짜리 OpCode와 추가 피연산자로 이루어져 있으며, 실행 엔진은 하나의 OpCode를 가져와 피연산자와 함께 작업을 수행한 다음 OpCode를 수행하는 식으로 동작한다.
  • 자바 바이트코드는 기계가 바로 수행할 수 있는 언어보다는 비교적 인간이 보기 편한 형태로 기술된 것이다.
  • 그래서 실행 엔진은 이와 같은 바이트코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경하며, 그 방식은 다음 두가지가 있다.

바이트코드 to 기계어

  • 인터프리터
    - 바이트코드 명령어를 하나씩 읽어서 해석하고 실행한다.
    - 하나씩 해석하고 실행하기 때문에 바이트코드 하나하나의 해석은 빠른 대신 인터프리팅 결과의 실행은 느리다는 단점을 가지고 있다.
    - 보통의 인터프리터 언어의 단점을 그대로 가지게 됨.
    - 바이트코드라는 언어는 기본적으로 인터프리터 방식으로 동작한다.
  • JIT(Just-In-Time) 컴파일러
    - 인터프리터의 단점을 보완하기 위해 도입.
    - 인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경
    - 이후 해당 메서드를 더이상 인터프리팅 하지 않고 네이티브 코드로 직접 실행하는 방식
    - 네이티브 코드를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 수행되게 된다.
    - JIT 컴파일러가 컴파일하는 과정은 바이트코드를 하나씩 인터프리팅하는 것보다 훨씬 오래 걸리므로, 만약 한 번만 실행되는 코드라면 컴파일하지 않고 인터프리팅 하는 것이 훨씬 유리하다.
    - 따라서 JIT 컴파일러를 사용하는 JVM들은 내부적으로 해당 메서드가 얼마나 자주 수행되는지 체크하고, 일정 정도를 넘을 때에만 컴파일을 수행한다.

JIT 컴파일러의 동작

  • 실행 엔진이 어떻게 동작하는지는 JVM 명세에 규정되지 않았다.

  • 따라서 JVM 벤더들은 다양한 기법으로 실행 엔진을 향상시키고 다양한 방식의 JIT 컴파일러를 도입하고 있다.

  • 대부분의 JIT 컴파일러는 다음과 같은 형태로 동작한다.

  • JIT 컴파일러는 바이트코드를 일단 중간 단계의 표현인 IR(Intermediate Representation)로 변환하여 최적화를 수행하고 그 다음 네이티브 코드를 생성한다.

오라클 핫스팟 VM

  • 오라클 핫스팟 VM은 핫스팟 컴파일러라고 불리는 JIT 컴파일러를 사용한다.

  • 핫스팟이라 불리는 이유는 내부적으로 프로파일링을 통해 가장 컴파일이 필요한 부분, 즉 '핫스팟'을 찾아낸 다음, 이 핫스팟을 네이티브 코드로 컴파일하기 때문이다.

  • 핫스팟 VM은 한 번 컴파일된 바이트코드라도 해당 메서드가 더이상 자주 불리지 않는다면, 즉 핫스팟이 아니게 된다면 캐시에서 네이티브 코드를 덜어내고 다시 인터프리터 모드로 동작한다.

  • 핫스팟 VM은 서버 VM과 클라이언트 VM으로 나뉘어있고, 각각 다른 JIT 컴파일러를 사용한다.

  • 클라이언트 VM과 서버 VM은 각각 오라클 핫스팟 VM을 실행할 때 입력하는 -client, -server옵션으로 실행된다.

  • 클라이언트 VM과 서버 VM은 동일한 런타임을 사용하지만, 위 그림과 같이 다른 JIT 컴파일러를 사용한다.

  • 서버 VM에서 사용하는 Advanced Dynamic Optimizing Compoiler가 더 복잡하고 다양한 성능 최적화 기법을 사용하고 있다.

IBM JVM

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

정리

  • 자바 성능 개선의 많은 부분이 실행엔진을 개선하여 이루어지고 있다.
  • JIT 컴파일러는 물론 다양한 최적화 기법을 도입하여 JVM의 성능은 계속해서 향상되고 있다.

The Java Virtual Machine Specification, Java SE 7 Edition

  • 오라클에서 Java SE 7을 발표하면서 JVM명세도 자바 SE 7버전으로 업데이트하여 발표했다.
  • 변경된 점
    - 자바 SE 5.0에서 도입된 Generics, 가변 인자 메서드 지원
    - 바자 SE 6부터 변화된 바이트코드 검증 프로세스 기술
    - 동적 타입 언어 지원을 위해 invokedynamic 명령어 및 관련 클래스 파일 포맷 추가
    - 자바 언어 자체의 개념에 대한 내용을 삭제하고, 자바 언어 명세에서 찾도록 유도
    - 자바 Thread와 Lock에 대한 내용을 삭제하고, 자바 언어 명세로 내용을 넘김
  • 자바 SE 7의 자바 컴파일러에 의해 생성된 클래스 파일의 버전은 51.0이고, 자바 SE 6는 50.0이다.
    - 두 버전의 차이가 많아 호환안됨.
  • 이렇게 많이 바뀌었는데도 자바 메서드의 65535바이트 제한은 안풀렸다.
    - JVM 클래스 파일 포맷을 획기적으로 변경하지 않는 한, 앞으로도 안될 것으로 보임.
  • 오라클 자바 SE 7 VM은 G1GC를 지원함.
    - 이는 오라클 JVM만의 특징으로 JVM 자체는 GC방식에 아무런 제한을 두지 않아 JVM명세에는 이를 기술하지 않음.

invokedynamic

  • 자바 SE 7부터 JVM 자체에서 자바 언어 뿐만 아니라 다른 언어, 특히 스크립트 언어들과 같이 타입이 고정되어 있지 않은 동적 타입 언어를 지원함에 따라, JVM 내부 명령어에도 변화가 생김.
  • 기존에 사용하지 않던 OpCode186을 새로운 명령어인 invokedynamic에 할당하고, invokedynamic을 지원하기 위해 클래스 파일 포맷에도 새로운 내용들이 추가되었다.
profile
더 나은 성취

0개의 댓글