훌륭한 자바 개발자가 되기 위한 소양 기르기 (1) JVM

김성혁·2022년 9월 22일
1

자바 바이트 코드를 실행하는 모든 하드웨어는 JVM을 통해 모든 하드웨어에서 자바 실행 코드를 변경하지 않고 실행할 수 있도록 합니다. 이는 플랫폼 의존적이지 않음을 의미합니다.

JVM의 특징

  • 스택 기반의 동작 : JVM은 피연산자를 저장하고 가져올 때 스택을 활용한다. 이는 레지스터 기반의 동작보다 하드웨어에 덜 의존적이며(레지스터를 직접 다루지 않고 스택을 통한 연산), 명령어의 길이가 짧아진다. (다음 피연산자가 스택의 TOP에 존재하므로 피연산자의 메모리 주소를 사용할 필요가 없어진다.) 하지만 이는 장점만 있는 것은 아니다. 명령어의 길이가 짧아지는 대신 명령어의 수가 많아지며 스택을 사용하는 오버헤드가 존재한다.
  • 심볼릭 레퍼런스 : 참조하는 클래스의 특정 메모리 주소를 참조 관계로 구성한 것이 아닌, 참조하는 대상의 이름을 통해 연결. Class 파일이 JVM에 올라가게 되면 심볼릭 레퍼런스를 통해 그 이름에 맞는 객체의 주소를 찾아서 연결하는 작업을 수행한다. 그러므로 실제 메모리 주소가 아닌 대상의 이름을 가진다.
  • 가비지 컬렉션
  • 기본 자료형을 명확하게 정의하여 호환성을 유지하고 플랫폼 독립성 보장
  • 네트워크 바이트 오더(빅 엔디안)

컴파일을 통해 만들어진 클래스 파일 자체는 바이너리 파일이므로 사람이 이해하기 쉽지 않습니다. 이에 JVM 벤더들은 역어셈블리를 제공하고 이를 통해 나온 Opcode와 Operand를 해석합니다.

자바 클래스 파일 포맷의 여러 제한으로 인해 자바 메서드는 65535 바이트를 넘을 수 없다.

JVM 구조 그리기

  • 클래스 로더가 컴파일된 자바 바이트 코드를 런타임 데이터 영역에 로드
  • 실행 엔진이 자바 바이트 코드를 실행

Class Loader

  • 자바의 가장 큰 특징 : 런타임에 클래스를 처음 참조할 때 해당 클래스를 로드
    • 클래스를 처음 참조할 때 해당 클래스를 로드하고, 클래스 로더 캐시도 존재
  • 클래스를 로드할 때 먼저 상위 클래스 로더를 확인하여 상위 클래스 로더에 있다면 해당 클래스를 사용하고, 없다면 로드를 요청받은 클래스 로더가 클래스를 로드 (위임, 계층 구조)
  • 하위 클래스 로더는 상위 클래스 로더의 클래스를 찾을 수 있지만, 상위 클래스 로더는 하위 클래스 로더의 클래스를 찾을 수 없다.
  • 클래스 로더는 클래스 언로드 불가. 즉 언로드 대신 현재 클래스 로더를 삭제하고 아예 새로운 클래스 로더를 생성하는 방법을 사용
  • 각 클래스 로더는 로드된 클래스들을 보관하는 네임스페이스(namespace)를 갖는다. 클래스를 로드할 때 이미 로드된 클래스인지 확인하기 위해서 네임스페이스에 보관된 FQCN(Fully Qualified Class Name)을 기준으로 클래스를 찾는다. 비록 FQCN이 같더라도 네임스페이스가 다르면, 즉 다른 클래스 로더가 로드한 클래스이면 다른 클래스로 간주된다.

클래스 로더 위임 모델

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

  • 부트스트랩 클래스 로더: JVM을 기동할 때 생성되며, Object 클래스들을 비롯하여 자바 API들을 로드한다. 다른 클래스 로더와 달리 자바가 아니라 네이티브 코드로 구현되어 있다.
  • 익스텐션 클래스 로더(Extension Class Loader): 기본 자바 API를 제외한 확장 클래스들을 로드한다. 다양한 보안 확장 기능 등을 여기에서 로드하게 된다.
  • 시스템 클래스 로더(System Class Loader): 사용자가 지정한 클래스 패스의 클래스들을 로드
  • 사용자 정의 클래스 로더(User-Defined Class Loader): 애플리케이션 사용자가 직접 코드 상에서 생성해서 사용하는 클래스 로더

클래스 로드 단계


Runtime Data Area

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



런타임 데이터 영역 구성

  • PC Register : 현재 실행 중인 JVM 명령의 주소 저장
  • JVM Stack : 지역 변수 배열, 피연산자 스택, 현재 실행 중인 메서드가 속한 클래스의 런타임 상수 풀에 대한 레퍼런스 저장
    • 저장되는 것들이 컴파일 타임에 결정되기 때문에 스택 프레임의 크기도 메서드에 따라 크기가 고정

  • 지역 변수 배열 : 0부터 시작하는 인덱스를 가진 배열. 0은 메서드가 속한 클래스 인스턴스의 this 레퍼런스이고, 1부터는 메서드에 전달된 파라미터들이 저장되며, 메서드 파라미터 이후에는 메서드의 지역 변수들이 저장
  • 피연산자 스택 : 메서드의 실제 작업 공간
  • 네이티브 메서드 스택 : 자바 외의 언어로 작성된 네이티브 코드를 위한 스택
  • Method Area : JVM이 읽어들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, static 변수, 메서드의 바이트코드 등을 보관
  • Runtime Constant Pool : 각 클래스와 인터페이스의 상수 뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블. 런타임 상수 풀을 통해 메서드나 필드의 실제 메모리상 주소를 찾음.

실행 엔진

  • 클래스 로더를 통해 JVM 내의 런타임 데이터 영역에 배치된 바이트 코드는 실행 엔진에 의해 실행
  • 명령어 단위로 읽어서 실행
  • 자바 바이트 코드는 비교적 인간이 보기 편한 형태로 기술되었기 때문에 JVM 내부에서 기계가 실행할 수 있는 형태로 변경

방식 1. 인터프리터

바이트코드 명령어 하나씩 해석하기 때문에 빠른 해석 but 인터프리팅 결과의 실행은 느리다

방식 2. JIT(Just-In-Time) 컴파일러

인터프리터 방식으로 실행하다가 적절한 시점에 바이트 코드 전체를 컴파일하여 네이티브 코드로 변경하고, 이후에는 해당 메서드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행

  • 네이티브 코드로 직접 실행 → 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 수행
  • 단, 한 번만 실행되는 코드는 인터프리팅하는 것이 유리 → JIT 컴파일러의 컴파일 과정은 오래 걸림.

NAVER D2

스택 기반 VM과 레지스터 기반 VM

0개의 댓글