JVM 메모리 구조 및 아키텍쳐(클래스 로더, 실행엔진 등)
모든 자바 개발자들은 JRE(Java Runtime Environment)에 의해 자바 바이트코드가 실행된다고 알고있습니다. 하지만 많은 개발자은 JRE가 JVM(Java Virtual Machine)의 구현이라는 것은 알지 못하는 것 같습니다. JVM은 컴파일된 자바 바이트 코드를 interpret하고 분석하고 실행합니다. 개발자로서 JVM의 아키텍쳐를 아는 것은 매우 중요합니다. 개발자가 코드를 더욱 효율적으로 작성할 확률을 올려줄 것입니다. 해당 포스팅에서는 자바에서의 JVM 아키텍쳐에 대한 깊은 이해와 다른 컴포넌트들을 알아볼 것입니다.
JVM 이란..?
가상머신이란 물리적 머신을 구현한 소프트웨어입니다. 자바는 애초에 WORA(Write Once Run Anywhere, 한번 쓰고 언제어디서나 실행 가능) 컨셉으로 개발된 언어이며 가상머신으로 운영할 수 있는 언어입니다. 컴파일러로 .java 소스파일을 컴파일을 하면 .class 클래스 파일(바이트코드가 포함된)을 JVM에게 건네주게 되면 JVM이 클래스파일을 메모리에 로드하며 실행할 수 있습니다. 아래는 JVM 아키텍쳐의 다이어그램입니다.
JVM이 어떻게 작동하는가..?
위의 그림과 같이 JVM은 세가지 서브시스템으로 구분할 수 있다.
1. 클래스 로더 시스템
2. 실행 데이터 영역
3. 실행 엔진
클래스 로더 서브시스템
자바의 동적 클래스 로딩 기능은 클래스로더 서브시스템에 의해서 처리된다. 컴파일 타임이 아닌 클래스를 처음 참조하는 런타임을 할때 클래스파일을 로딩, 연결, 초기화 작업이 이루어진다.
1. 로딩(Loading)
Bootstrap, Extension, Application - 이 컴포넌트들에 의해 클래스들이 로드된다. 클래스 로더들이면서 이 세가지 클래스로더에 의해 클래스가 로드됩니다. 이 세가지 클래스 로더들은 모두 상속관계로 정의되어 있으며 delegate(위임) 방식으로 작업을 진행합니다.
- Bootstrap ClassLoader - jre의 lib폴더에 있는 rt.jar 파일을 뒤져 기본 자바 API 라이브러리를 로드합니다. 가장 최우선으로 로드됩니다.
- Extension ClassLoader - 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 환경변수에 있는 클래스 파일이나 -classpath 또는 -cp 명령어 옵션이 있는 파일들을 로드합니다.
2. 연결(Linking)
- 검증 (verify) - 바이트코드 검증기는 생성된 자바 바이트코드가 적절한지 아닌지에 대해서 검증하며 검증이 실패할 경우 검증오류를 내보내게 됩니다.
- 준비 (prepare) - 모든 정적변수의 메모리가 할당되며 기본 default 값으로 할당됩니다.(아직 초기화되지는 않음)
- 해석 (resolve) - 모든 심볼릭한(명확하게 정의되지 않은) 메모리 참조를 메소드 영역에 있는 타입으로 직접 참조합니다.
3. 초기화(initialize)
클래스 로딩의 마지막 단계로써 모든 정적 변수가 자바 코드에 명시된 값으로 초기화되며 정적블록이 실행됩니다.
실행 데이터 영역(Runtime Data Area)
주로 5가지 영역으로 구분할 수 있습니다.
- 메서드 영역(Method Area) : 모든 클래스 수준(클래스명, 부모클래스명, 메소드, 변수)의 데이터가 저장됩니다. 공유자원이며 JVM당 하나의 영역밖에 존재하지 않습니다.
- 힙 영역(Heap Area) : 모든 인스턴스 오브젝트(클래스, 배열 등)가 저장되는 공간입니다. JVM당 하나의 영역밖에 존재하지 않으며 또한 공유자원입니다. 위의 메소드 영역에 있는 데이터와 마찬가지로 멀티스레드에서 접근이 가능한 공유자원이기 때문에 스레드에 안전하지 않습니다.
- 스택 영역(Stack Area) : 각각의. 스레드마다 개별의 스택영역이 존재합니다. 메소드 콜스택이 메소드가 호출될 때마다 스택에 스택 프레임이라는 스택메모리에 쌓이게 됩니다. 모든 지역변수가 스택 메모리에 저장될것입니다. 스택 영역은 공유자원이 아니기 때문에 스레드에 안전합니다.
스택 프레임은 세가지 서브엔티티로 나누어집니다.
지역변수 배열(Local Variable Array) - 메소드가 얼마나 많은 지역변수를 포함하는 가와 해당하는 값에 대한 정보가 저장된다.
피연산자 스택(Operand Stack) - 중간연산이 필요로 할 때, 연산작업을 수행하기 위한 작업공간 역할을 합니다.
프레임 데이터(Frame Data) - All symbols corresponding to the method is stored here.(symbol의 의미를 알고계신분은 댓글로 알려주시면 감사드리겠습니다. 컨셉을 모르겠습니다ㅜㅜ) 예외가 있는 경우, 캐치블록의 정보는 프레임 데이터에서 유지가 될 것입니다.
- PC 레지스터 : 현재 실행중인 명령문의 주소를 가지기 위해 각각의 스레드마다 개별 PC 레지스터가 존재한다.
- Native Method Stacks : native 메소드 정보를 가지고 있는 스택. 개별 스레드마다 생성됨.
실행 엔진
실행 데이터 영역에서 할당된 바이트 코드는 실행엔진에 의해서 실행된다. 실행엔진은 바이트코드를 읽으며 조각단위별로 실행합니다.
인터프리터(Interpreter) - 인터프리터는 바이트코드를 빠르게 해석하지만 실행속도는 느립니다.(한줄씩) 인터프리터의 단점은 하나의 메소드가 여러번 호출되었을 때, 매번 새로운 해석(interpretation)이 필요하다는 것입니다.
JIT 컴파일러(Just-In-Time) - JIT컴파일러는 인터프리터의 단점을 보완해줍니다. 실행엔진은 바이트코드를 변환하는데에 인터프리터의 도움을 받을테지만 반복되는 코드가 발견되었을 시에는 JIT컴파일러를 사용해서 반복되는 부분을 native code(원시코드)로 컴파일합니다. 변환된 원시코드는 인터프리터의 변환과정없이 직접적으로 사용이 가능하며(기존에는 바이트코드에서 원시코드로 변환 후 실행하였다면 JIT 컴파일러를 사용하여 변환된 원시코드는 변환하지않고 바로 실행) 이로 인해 시스템의 성능이 좋아지게 된다.
JIT 컴파일러 역할
1. Intermediate Code 생성기 - Intermediate Code를 생성
2. 코드 최적화 - Intermediate Code를 최적화함.
3. 타겟 코드 생성 - 원시코드(native code) 생성
4. 프로파일 - 특정 메소드가 여러번 실행했는지 안했는지를 판별해주는 special component.
가비지 컬렉터(Garbage Collector) - 아무 참조가 없는 인스턴스들을 모아 제거하는 역할. System.gc() 메소드로 가비지 컬렉션을 실행할 수 있지만 보장되지는 않음.(반드시 실행되는건 아니라는 뜻), JVM의 가바지 컬렉션은 생성된 인스턴스들을 모음.
etc.
자바 원시 인터페이스(Java Native Interface, JNI) - JNI는 Native Method Libraries와 관련있으며 실행엔진에 필요한 원시 라이브러리들을 제공합니다.
원시 메소드 라이브러리(Native Method Libraries) - Native Libraries의 집합이며 실행엔진에 필수적이다.
추후 기술면접에서 경쟁력을 확보하기 위해서 다음 웹사이트를 참조하여 제가 해석한 문장입니다. 해석하기 어려웠던 매우 추상적인 문장은 원문 그대로 적어두었습니다.