[Java] 6. JVM 동작원리
JRE(Java Runtime Environment)가 JVM(Java Virtual Machine)의 구현으로 JVM에 의해 자바 바이트코드가 실행된다. JRE 안에 JVM이 존재한다.
◼︎ JVM
이란?
가상머신이란 물리적 머신을 구현한 소프트웨어입니다. 자바는 애초에 WORA(Write Once Run Anywhere, 한번 쓰고 언제 어디서나 실행 가능) 컨셉으로 개발된 언어이며 가상머신으로 운영할 수 있는 언어이다.
JVM 아키텍쳐와 실행과정을 정리한 포스팅이다.
◼︎ JVM 아키텍쳐 다이어그램
◼︎ JVM 실행과정
- 작성한 자바소스(JAVA Source, Person.java)를 자바 컴파일러(JAVA Compiler)를 통해 자바 바이트코드(JAVA Byte Code, Person.class)로 컴파일한다.
- 컴파일된 바이트코드를 JVM의 클래스로더(Class Loader)에게 전달한다.
Class Loader(클래스로더)
는 동적로딩(Dynamic Loading)을 통해 클래스들을 로딩 및 링크하여Runtime Data Area(런타임 데이터 영역)에 로드
한다. Runtime Data Areas는 JVM 이 Java Bytecode 를 실행하기 위해 사용되는 메모리 공간이다.Execution Engine(실행엔진)
은 JVM메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서실행
한다.
정리하자면 클래스 로더(Class Loader)
가 컴파일된 자바 바이트코드를 런타임 데이터 영역(Runtime Data Areas)
에 로드하고, 실행 엔진(Execution Engine)
이 자바 바이트코드를 실행한다.
위에 그림과 같이 JVM은 크게 3개로 서브시스템으로 구분된다.
- Class Loader SubSystem
- Runtime Data Areas
- Execution Engine
1. Class Loader SubSystem
1.1. 클래스 로더 특징
자바는 동적 로드, 즉 컴파일타임이 아니라 런타임에 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크하는 특징이 있다. 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더이다. 자바 클래스 로더의 특징은 다음과 같다.
- 계층 구조: 클래스 로더끼리 부모-자식 관계를 이루어 계층 구조로 생성된다. 최상위 클래스 로더는 부트스트랩 클래스 로더(Bootstrap Class Loader)이다.
- 위임 모델: 계층 구조를 바탕으로 클래스 로더끼리 로드를 위임하는 구조로 동작한다. 클래스를 로드할 때 먼저 상위 클래스 로더를 확인하여 상위 클래스 로더에 있다면 해당 클래스를 사용하고, 없다면 로드를 요청받은 클래스 로더가 클래스를 로드한다.
- 가시성(visibility) 제한: 하위 클래스 로더는 상위 클래스 로더의 클래스를 찾을 수 있지만, 상위 클래스 로더는 하위 클래스 로더의 클래스를 찾을 수 없다.
- 언로드 불가: 클래스 로더는 클래스를 로드할 수는 있지만 언로드할 수는 없다. 언로드 대신, 현재 클래스 로더를 삭제하고 아예 새로운 클래스 로더를 생성하는 방법을 사용할 수 있다.
클래스 로더가 클래스 로드를 요청받으면, 클래스 로더 캐시, 상위 클래스 로더, 자기 자신의 순서로 해당 클래스가 있는지 확인한다. 즉, 이전에 로드된 클래스인지 클래스 로더 캐시를 확인하고, 없으면 상위 클래스 로더를 거슬러 올라가며 확인한다. 부트스트랩 클래스 로더까지 확인해도 없으면 요청받은 클래스 로더가 파일 시스템에서 해당 클래스를 찾는다.
만약 요청 받은 클래스가 시스템 클래스 로더에 존재하여도 부트스트랩 클래스 로더까지 확인을 하고 부트스트랩에도 해당 클래스가 존재하면 부트스트랩에 있는 클래스를 로드한다.
1.2. 클래스 로드 과정
자바의 동적 클래스 로딩 기능은 클래스로더 서브시스템에 의해서 처리된다. 컴파일 타임이 아닌 클래스를 처음 참조하는 런타임을 할때 클래스파일을 Loading(로딩), Linking(연결), Initialization(초기화) 작업이 이루어진다.
1) Loading(로딩)
Bootstrap, Extension, Application 컴포넌트들에 의해 클래스들이 로드되며, 이 세가지 클래스 로더들은 모두 상속관계로 정의되어 있으며 delegate(위임) 방식으로 작업을 진행합니다.
- Bootstrap ClassLoader :
- JVM을 기동할 때 생성되며, 다른 클래스 로더와 달리 자바가 아니라 네이티브 코드로 구현되어 있다.
jre의 lib폴더에 있는 rt.jar 파일을 찾아 Object 클래스들을 비롯하여 자바 API들을 로드하며 이 로더가 우선 순위가 가장 높다.
- Extension ClassLoader :
- 기본 자바 API를 제외한 확장 클래스들을 로드한다. 다양한 보안 확장 기능 등을 여기에서 로드하게 된다.
- jre의 lib 폴더에 있는 ext 폴더에 있는 모든 확장 코어 클래스파일들을 로드합니다. 최근에는 Platform ClassLoader라고 부르기도 합니다. Bootstrap ClassLoader의 child 입니다. Extension 클래스 로더는 jdk 확장 디렉토리(JAVA_HOME/lib/ext 디렉토리 혹은 java.ext.dirs 에 저장된 경로)에서 로드됩니다.
- Application ClassLoader :
- Extension ClassLoader의 child이며 시스템 클래스로더(System ClassLoader)라고도 불립니다.
어플리케이션 레벨에 있는 클래스들을 로드합니다. 즉, 사용자가 직접 정의한 클래스파일들을 로드
합니다. 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드한다
- Extension ClassLoader의 child이며 시스템 클래스로더(System ClassLoader)라고도 불립니다.
2) Linking(연결)
- verify(검증) : 바이트코드 검증기는 생성된 자바 바이트코드가 적절한지 아닌지에 대해서 검증하며 검증이 실패할 경우 검증오류를 발생시킴.
- prepare(준비) : 모든 정적변수의 메모리가 할당되며 기본 default 값으로 할당
- resolve(해석) : 모든 심볼릭한(명확하게 정의되지 않은) 메모리 참조를 메소드 영역에 있는 타입으로 직접 참조합니다.
3) Initialize(초기화)
클래스 로딩의 마지막 단계로써 모든 정적 변수가 자바 코드에 명시된 값으로 초기화되며 정적블록이 실행된다.
2. Runtime Data Area(실행 데이터 영역)
크게 5가지 영역으로 구분할 수 있다. Runtime Data Area
는 JVM 이 Java Bytecode 를 실행하기 위해 사용되는 메모리 공간이다.
2.1. Method Area(메서드 영역)
모든 클래스 수준(클래스명, 부모클래스명, 메소드, 변수)의 데이터가 저장됩니다. 공유자원이며 JVM당 하나의 영역밖에 존재하지 않습니다.
2.2. Heap Area(힙 영역)
모든 인스턴스 오브젝트(클래스, 배열 등)가 저장되는 공간입니다. JVM당 하나의 영역밖에 존재하지 않으며 또한 공유자원입니다. 위의 메소드 영역에 있는 데이터와 마찬가지로 멀티스레드에서 접근이 가능한 공유자원이기 때문에 스레드에 안전하지 않습니다.
2.3. Stack Area(스택 영역)
각각의 스레드마다 개별의 스택영역이 존재합니다. 메소드 콜스택이 메소드가 호출될 때마다 스택에 스택 프레임이라는 스택메모리에 쌓이게 됩니다. 모든 지역변수가 스택 메모리에 저장될것입니다. 스택 영역은 공유자원이 아니기 때문에 스레드에 안전합니다. 스택 프레임은 세가지 서브엔티티로 나누어집니다.
- 지역변수 배열(Local Variable Array) : 메소드가 얼마나 많은 지역변수를 포함하는 가와 해당하는 값에 대한 정보가 저장된다.
- 피연산자 스택(Operand Stack) : 중간연산이 필요로 할 때, 연산작업을 수행하기 위한 작업공간 역할을 합니다.
- 프레임 데이터(Frame Data) : 메서드에 해당하는 모든 심볼이 여기에 저장됩니다.
2.4. PC Registers(PC 레지스터)
현재 실행중인 명령문의 주소를 가지기 위해 각각의 스레드(Thread)마다 개별 PC 레지스터가 생성된다.
2.5. Native Method Stacks
자바외 언어로 작성된 네이티브 코드를 위한 메모리 영역
3. Execution Engine(실행 엔진)
런타임 데이터 영역에 할당된 바이트 코드는 실행엔진에 의해서 실행
된다. 실행엔진은 바이트코드를 읽으며 조각단위별로 실행합니다. 명령어를 한줄씩 실행하는 인터프리터(Interpreter)방식이 있고, 정해진 시간안에 모든 바이트 코드를 컴파일하는 JIT(Just-In-Time) 컴파일러를 이용하는 방식이 있다.(byte code -> binary code)
3.1. 인터프리터(Interpreter)
인터프리터는 바이트코드를 빠르게 해석하지만 실행속도는 느립니다. 인터프리터의 단점은 하나의 메소드가 여러번 호출되었을 때, 매번 새로운 해석(interpretation)이 필요하다는 것입니다.
3.2. JIT 컴파일러(Just-In-Time)
JIT컴파일러는 인터프리터의 단점을 보완한 것으로 인터프리터의 느린 속도를 보완하기 위한 컴파일러로 정해진 시간 내에 모든 바이트 코드를 컴파일하는 개념이다.
실행엔진은 바이트코드를 변환하는데에 인터프리터의 도움을 받을테지만 반복되는 코드가 발견되었을 시에는 JIT컴파일러를 사용해서 반복되는 부분을 native code(원시코드)로 컴파일합니다. 변환된 원시코드는 인터프리터의 변환과정없이 직접적으로 사용이 가능하며(기존에는 바이트코드에서 원시코드로 변환 후 실행하였다면 JIT 컴파일러를 사용하여 변환된 원시코드는 변환하지않고 바로 실행) 이로 인해 시스템의 성능이 좋아지게 된다.
JIT 컴파일러는 바이트코드를 일단 중간 단계의 표현인 IR(Intermediate Representation)로 변환하여 최적화를 수행하고 그 다음에 네이티브 코드를 생성한다.
3.3. 가비지 컬렉터(Garbage Collector)
아무 참조가 없는 인스턴스들을 모아 제거하는 역할. System.gc() 메소드로 가비지 컬렉션을 실행할 수 있지만 보장되지는 않음.(반드시 실행되는건 아니라는 뜻), JVM의 가바지 컬렉션은 생성된 인스턴스들을 모음.
[참고]
- https://sas-study.tistory.com/262
- https://hoonmaro.tistory.com/19
- https://d2.naver.com/helloworld/1230
- https://velog.io/@litien/JVM-구조