이 글을 쓰면서 너무 중구난방 한 글을 작성하지 않나 하는 생각이 머릿속을 계속 접근하고 있습니다.. class 파일의 내부 정의 키워드를 넣지 않는 등 일부 당장 필요가 없다고 판단되는 부분들을 임의로 삭제한다고 삭제하였는데 과연 좋은 판단이었는지에 대해서는 더 생각해보고 이 글을 수정하겠습니다.
0. 들어가며
JVM은 Java Virtual Machine의 약자이다. Virtual Machine이라는 단어에 주목해야한다.
VM이라는 건 프로그램을 실행하기 위해 물리적 머신(즉, 컴퓨터)과 유사한 머신을 소프트웨어로 구현한 것을 말한다고 할 수 있다. 요즈음에 와서는 JAVA의 목적성에서 변색된 개념(왜 변색되었다고 말하는지는 추후 조사하여 작성하도록 하겠다..)이긴 하지만 자바는 원래 WORA(Write Once Run Anywhere)를 구현하기 위해 물리적인 머신과 별개의 가상 머신을 기반으로 동작하도록 설계되었다. 그래서 자바 바이트코드를 실행하고자 하는 모든 하드웨어에 JVM을 동작시킴으로써 자바 실행 코드를 변경하지 않고도 모든 종류의 하드웨어에서 동작되게 한 것이다.
0-1 JVM종류가 많다고?
초기 JVM은 Sun 회사에서 만들었지만 JVM 명세(The Java Virtual Machine Specification)를 따르기만 하면 어떤 벤더든 JVM을 만들고 배포할 수 있다. 그 결과로 오라클 핫스팟 JVM(오라클이 SUN을 인수한 건 유명한 이야기이다.) 말고도 IBM JVM 등 다양한 JVM이 존재한다. (별도의 설정이나 설치를 하지 않고 일반적인 자바 설치를 했다면 오라클 핫스팟 JVM을 사용 중일 확률이 높다.) (안드로이드 스마트폰에 기본 탑재된 Dalvik VM은 JVM이긴 하지만 JVM 명세를 따르지는 않는다. 스택 머신인 다른 JVM과는 달리 Dalvik VM은 레지스터 머신이며, 따라서 독자적인 툴을 이용해 자바 바이트코드를 Dalvik VM용의 레지스터 기반 명령어 코드로 변환한다.)
1. java 파일의 실행 순서
우리가 자바 코드를 실행하면 아래와 같은 상황이 발생한다.
-
.java 파일에 작성한 코드를 java 컴파일러(javac)가 byte코드로 이루어진 .class파일(binary)로 변환시킨다. (모든 application의 entry point는 main() 메서드이다.)
-
생성된 .class파일을 Class Loader가 load 한다.
-
Class Loader내에서는 제일 먼저 bootstrap class loader가 동작을 시작한다. bootstrap class loader의 기능은 다른 class loader가 나머지 시스템에 필요한 class loader를 load 할 수 있게 최소한의 필수 클래스(ex : java.lang.Object, Class, Classloader)만 load 한다. (여담으로 이 부분과 연관된 내용인지 정확하지는 않지만 아마 java에서 말하는 lazy loading과 관련이 있지 않을까 생각된다.)
-
확장 클래스 로더(extension class loader)가 생성된다. bootstrap class loader를 parents object로 설정한 후 필요할 때 class loading 작업을 bootstrap class loader에 넘긴다. 잘 쓰이는 방식은 아니지만 extension class loader를 이용하면 특정 OS나 플랫폼에 native코드를 제공하고 기본 환경에 대한 override가 가능하다. (추가적으로 자바 8부터 도입된 javascript runtime engine인 nashhorn을 사용하여 java에서 javascript를 사용할 수 있다.)
-
application class loader(또는 system class라고 하는데 system class를 load하지 않기 때문에 system class loader라고는 하지 않는다.) 가 생성되고 지정된 ClassPATH에 위치한 user class를 load한다.
-
최종적으로 class loader를 통해 class 파일을 jvm으로 load 한 후 load 된 class 파일이 Execution Engine을 통해 해석된다.
-
해석된 class파일(byte code)은 Runtime Data Areas에 배치되고 수행이 이루어진다.
2. JIT(Just In Time)
자바 프로그램은 byte code interpreter가 가상화한 stack machine에서 명령어를 실행하며 시작된다. stack machine은 CPU를 추상화한 구조로 다른 플랫폼에서도 .class파일을 문제없이 실행할 수 있지만 성능을 최대화하기 위해서는 native기능을 이용해 stack machine이 아닌 실제 cpu에서 직접 프로그램을 실행시켜야 한다.
위에서 말한 성능의 최대화를 하기 위해 오라클 핫스팟 JVM에서는 프로그램 단위(메서드, loop)를 interpret byte code에서 native code로 compile 하는데 이때 사용되는 기술이 JIT이다. 핫스팟이라 불리는 이유는 내부적으로 application을 모니터링 하며 가장 컴파일이 필요한 부분(가장 자주 실행되는 부분), 즉 '핫스팟'을 찾아낸 다음, 이 핫스팟을 네이티브 코드로 컴파일하기 때문이다.
오라클 핫스팟 JVM은 한번 컴파일된 바이트코드라도 해당 메서드가 더 이상 자주 불리지 않는다면, 즉 핫스팟이 아니게 된다면 캐시에서 네이티브 코드를 덜어내고 다시 인터프리터 모드로 동작한다. 핫스팟 VM은 서버 VM과 클라이언트 VM으로 나뉘어 있고, 각각 다른 JIT 컴파일러를 사용한다
JIT은 인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고, 이후에는 해당 메서드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행하는 방식이다. 네이티브 코드를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 수행되게 된다.
JIT 컴파일러가 컴파일하는 과정은 바이트코드를 하나씩 인터프리팅하는 것보다 훨씬 오래 걸리므로, 만약 한 번만 실행되는 코드라면 컴파일하지 않고 인터프리팅하는 것이 훨씬 유리하다. 따라서, JIT 컴파일러를 사용하는 JVM들은 내부적으로 해당 메서드가 얼마나 자주 수행되는지 체크하고, 일정 정도를 넘을 때에만 컴파일을 수행한다.
3. Class Loader
class loader 가 아직 로드되지 않은 클래스를 찾으면, 다음과 같은 과정을 거쳐 클래스를 로드하고 링크하고 초기화한다.
로드
클래스를 파일에서 가져와서 JVM의 메모리에 로드한다.
링킹의 3가지 동작
검증(Verifying)
읽어 들인 클래스가 자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사한다. 클래스 로드의 전 과정 중에서 가장 까다로운 검사를 수행하는 과정으로서 가장 복잡하고 시간이 많이 걸린다.
준비(Preparing)
클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조를 준비한다.
분석(Resolving)
클래스의 상수 풀 내 모든 symbolic reference를 direct reference로 변경한다.
symbolic references란?
참고하는 클래스의 특정 메모리 주소를 참조 관계로 구성한 것이 아닌 참조하는 대상의 이름으로 참조하는 것
direct references란?
참조하는 클래스의 특정 메모리 주소를 참조하는 것
초기화(Initializing)
클래스 변수들을 적절한 값으로 초기화한다. 즉, static initializer들을 수행하고, static 필드들을 설정된 값으로 초기화한다.
4. Runtime Data Areas
PC 레지스터(PC Register), JVM 스택(JVM Stack), 네이티브 메서드 스택(Native Method Stack)은 스레드마다 하나씩 생성되며 힙(Heap), 메서드 영역(Method Area), 런타임 상수 풀(Runtime Constant Pool)은 모든 스레드가 공유해서 사용한다.
PC 레지스터
PC 레지스터는 현재 수행 중인 JVM 명령의 주소를 갖는다. 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다.
Method Area
모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다. JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀(Runtime Constant Pool), 필드와 메서드 정보, Static 변수, 메서드의 바이트코드 등을 보관한다. 메서드 영역은 JVM 벤더마다 다양한 형태로 구현할 수 있으며, 오라클 핫스팟 JVM에서는 흔히 Permanent Area, 혹은 Permanent Generation(PermGen)이라고 불린다. 메서드 영역에 대한 가비지 컬렉션은 JVM 벤더의 선택 사항이다.
Runtime Constant Pool?
클래스 파일 포맷에서 constant_pool 테이블에 해당하는 영역이다. Method Area에 포함되는 영역이긴 하지만, JVM 동작에서 가장 핵심적인 역할을 수행하는 곳이기 때문에 JVM 명세에서도 따로 중요하게 기술한다.(하지만 아래 그림에서는 넣지 않았다.) 각 클래스와 인터페이스의 상수뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다. 즉, 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아서 참조한다.
Stack Area(JVM 스택)
JVM 스택은 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성되고 Stack Area에 적재된다. JVM스택은 프레임(Stack Frame)이라는 구조체를 저장하는 스택으로, JVM은 오직 JVM스택에 스택 프레임을 추가하고(push) 제거하는(pop) 동작만 수행한다. 예외(exception) 발생 시 printStackTrace() 등의 메서드로 보여주는 Stack Trace의 각 라인은 하나의 스택 프레임을 표현한다.
힙(heap)
인스턴스 또는 객체가 생성되면 저장하는 공간으로 가비지 컬렉션의 대상이 되는 영역이다.
Native Method Stack
자바 외의 언어로 작성된 네이티브 코드를 위한 스택이다. 즉, JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 C 스택이나 C++ 스택이 생성된다.
referecne :
[1] : 자바 최적화 : 가장 빠른 성능을 구현하는 검증된 10가지 기법
[2] : https://jeong-pro.tistory.com/148
[3] : https://d2.naver.com/helloworld/1230
[4] : https://mygumi.tistory.com/115
'JAVA' 카테고리의 다른 글
저는 Eclipse로 개발합니다(1) - debugging (0) | 2021.03.25 |
---|---|
JVM -XX Options (0) | 2021.03.01 |
자바 GUI프로그램 IOConsole 글자 수 조절하기 (0) | 2020.11.15 |
자바 toUpperCase, toLowerCase를 이용한 대소문자 변환 (0) | 2020.11.12 |
JVM 32bit, 64bit 무엇을 골라야 하는가 (0) | 2020.11.10 |