JVM 아키텍쳐
과거 여러번 JVM에 대해 읽어 봤지만
머릿속에 잘 남아있지 않아
이번에 제대로 이해 해보려고 정리한 글입니다.
이 글의 번역이라 봐도 되겠습니다.
VM
: 실제 컴퓨터를 가상화한 기계
- 한 컴퓨터에 여러 가장 머신이 있을 수 있다.
- 각 가상머신은 고립되어 있다.
- 다른 가상머신의 영향을 받지 않는다.(독립적이다.)
- 실제 컴퓨터 -> Host machine
- 가상 머신 -> Gueset machine
다른 언어들과 비교
- C/C++ :
- 컴파일 언어
- 해당 플랫폼에 맞는 기계어로 컴파일
- javascript, python :
- 인터프리터 언어
- 컴파일 없이 바로 실행
- jvm 언어들 :
- 바이트 코드(.class) 생성(컴파일)
- 바이트 코드를 JVM이 인터프리트(해석;실행)함
- 해당 플랫폼에 맞게
- 컴파일 된 바이트 코드(.class)는
다른 플랫폼의 JVM에서도 실행가능함
- 가상 머신과 유사하게
JVM도 host machine에 독립적
JVM
: Java 프로그램을 실행시킬 수 있는 추상화 머신
- specification : 규격 ; 상세
- JVM이 어떻게 작동하는지에 대한 규격
- implementation : 구현
- JRE
- 구현은 Sun 또는 다른 벤더(회사)들이 제공
- instance : 개체
- Java class 를 실행하면 JVM 개체가 생성됨
JVM 기능
- loads the code
- verify the code
- executes the code
- menage memory
JVM architecture
크게 3가지로 이루어짐
- Class Loader
- Runtime Memory/Data Area
- Execution Engine
출처 : https://www.freecodecamp.org/news/content/images/size/w1000/2021/01/image-39.png
Class Loader
- 프로그램에서
(.java파일을 컴파일해서 생성된) .class 파일에서 class를 사용할 때
Class Loaser가 메인 메모리에 해당 class를 로드시킴 - 보통 main() 메서드를 포함하는 class를
가장 먼저 메모리에 로드한다.
class loading에는 3가지 상태가 존재
- Loading
- Linking
-
Initialization
출처 : https://www.freecodecamp.org/news/content/images/size/w1000/2021/01/image-40.png
Loading
: 특정 이름의 class 또는 interface의 바이트 코드를 포함해
원래 class 또는 interface를 생성
- 3가지 Loader가 있음
- Bootstrap Class Loader
- root class Loader
- superclass Of Extension class loader
다른 확장 클래스들의 상위 클래스이다. - java.lang, java.net, java.util, java.io 등
기본 패키지들을 로드함- 이 패키지들은
rt.jar
에 포함됨 - 다른 주요 라이브러리들은
$JAVA_HOME/jre/lib 에 포함됨
- 이 패키지들은
- root class Loader
- Extension Class Loader
- Bootstrap Class Loader의 하위 클래스이고
Application Class Loader의 상위 클래스이다. - 확장 라이브러리들을 로드함
- $JAVA_HOME/jre/lib/ext에 포함된 라이브러리
- Bootstrap Class Loader의 하위 클래스이고
- Application Class Loader
- classpath에 있는 파일들을 로드함
- classpath의 디폴트는
현재 애플리케이션의 디렉토리로 설정됨 - classpath는 command line 옵션으로
-classpath
or-cp
로 설정 가능함
- Bootstrap Class Loader
-
JVM은 ClassLoader.loadClass()메서드를 이용해
클래스들을 메모리로 로딩시킨다. - 만약
상위 클래스 로더
가 클래스를 찾지 못하면
하위 클래스 로더
에게 일을 위임한다.
마지막까지 클래스를 못찾으면
NoClassDefFouondError or ClassNotFoundException 을 던진다.
Linking
: class들이 메모리에 로드된 후
linking(연결) 과정을 실행된다.
프로그램에 필요한 클래스 또는 인터페이스
그리고 각각의 의존성들을 포함해 연결한다.(linking)
- Linking 과정
- Verification phase(상;상태):
- .class 파일이 제약 및 룰에 따라
구조적으로 알맞는지 확인 - 만약 맞지 않으면 VerifyException 발생
- ex) java12에서 작성된 코드가
Java8이 설정된 시스템에서 실행시키려 하면 실패하게 됨
- .class 파일이 제약 및 룰에 따라
- Preparation phase:
- JVM은 static field나 기본 초기화 (변수)값들을 메모리에 할당함
- Resolution phase:
- 다른 클래스나 다른 클래수의 상수인 변수(constant variables)를 참조할 때
이 상태에서 resolve됨- resolve는 풀다, 녹이다, 분해하다, 결정하다 이런 뜻인데
코딩에서는결정;사용;적용
된다 정도로 이해하면 될거 같음
- resolve는 풀다, 녹이다, 분해하다, 결정하다 이런 뜻인데
- 다른 클래스나 다른 클래수의 상수인 변수(constant variables)를 참조할 때
- Verification phase(상;상태):
Initialization
: 클래스 로딩의 마지막 과정
- 클래스나 인터페이스의 메서드를 초기화
- class’s constructor; 초기화
- static block을 실행
-
static 변수들의 값 초기화 진행
ex) private static final boolean enabled = true;
라고 선언 되어 있으면
preparation 상태에서 enabled
는 boolean의 기본값인 false로 값이 지정되었다가 Initialization과정에서 true로 값은 지정하게 됨
주의
JVM은 multi-thread 환경에서 실행 될 수 있다.
그래서 multi-thread에서 한 클래스를 동시에 초기화하는
동시성 문제가 있을 수 있으니 이 점을 고려해야 함
Runtime Data Area
아래의 5가지로 구성되어 있음
- Method Area
- Heap Area
- Stack Area
- Pc Register
- Native Method Stack
Method Area
- 모든 클래스 레벨의 데이터들이 저장되어 있음
- runtime constant pool, field, method data, methods, constructors
- 만약 메모리가 프로그램 시작에 부족하면
JVM은OutOfMemoryError
를 던짐 - VM이 시작할 때 Method Area가 생성됨
- JVM당 Method Area(1:1)
Heap Area
- 모든 object와 그와 연관된 변수들이 저장됨
- 모든 인스턴스, 배열들이 할당되는 메모리 공간
- vm이 시작할 때 생성됨
- JVM당 Heap Area(1:1)
주의
multi-thread시 Mathod area 와 Heap area 가 같은 메모리를 공유할 수 있으니 안전하지 않을 수 있음
Stack Area
- JVM에서 새 쓰레드가 생성될 때 seperate runtime stack 이 동시에 생성됨
- 지역 변수, 메서드 호출, 및 부분 결과들이 저장됨
- All local variables, method calls, and partial results are stored in the stack area.
- 만약 한 쓰레드에서 가능한 스택 크기보다
더 많은 스택 크기가 요구되면
StackOverFlowError
를 던짐 - 모든 메서드 호출에 있어서
Stack Frame
이라는 스택 메모리에 한 진입점이 만들어짐
메서드 호출이 완료되면Stack Frame
이 사라짐
Stack Frame은 아래 세가지로 이루어짐
- Local variables
- 각 Frame은 지역 변수들의 배열을 포함하고 그 변수들과 값들은 여기에 저장됨
- 컴파일 시간에 배열의 길이가 결정됨
- Operand Stack
- 각 Frame은 LIFO stack을 포함함
- 런타임시 workspace로 실행함
- 런타임시 발생하는 작업때에 메모리 할당의 장소가 됨
- 컴파일 시간에 최대 스택깊이가 결정됨
- Frame Data
- All symbols corresponding to the method are stored here.
- (method의 심볼? 이름?이 저장되는 장소)
- 또한 catch block 정보도 포함됨
- All symbols corresponding to the method are stored here.
ex)
double calculateNormalisedScore(List<Answer> answers) {
double score = getScore(answers);
return normalizeScore(score);
}
double normalizeScore(double score) {
return (score – minScore) / (maxScore – minScore);
}
answer, score 은 Local Variables 배열에 저장됨
Operand Stack 은 계산에 필요한 연산자와 변수들을 포함함
Stack Frame
|Local Variables|Operand Stack|
|——|—|
|List
- Stack Area 공유되지 않음. 그래서 스레드에 안전함
Program Counter (PC) Registers
- JVM은 멀티스레드를 지원함
- 각 스레드는 JVM을 실행하는 명령 주소를 가진 각자의 PC Register가 있음
- 한 번 실행하면 PC Register는 다음 명령으로 변경됨
Native Method Stacks
- Java가 아닌 C/C++ 같은 네이티브 언어를 포함할 수 있는 곳
- 각 스레드마다 분리된(각자의) navtive method stack이 할당됨
Excution Engine
- 각 클래스마다 현재 실행해야 할 코드를 실행함
- 실행되기 전 바이트코드(.class)는 기계어로 변경되야 함
- JVM은 인터프리터 또는 JIT(just-in-time) 컴파일러를 Execution engine으로 사용
출처 : https://www.freecodecamp.org/news/content/images/2021/01/image-33.png
Interpreter
- 바이트코드를 한 줄 한 줄 읽고 실행함
- 한 줄 한 줄 읽으므로 비교적 느림
- 한 메서드를 여러번 호출할 경우 매번 새로은 해석(interpretation)이 필요함
JIT Compiler
- interpreter의 단점을 보완
- 먼저 바이트코드를 실행하기 위해 interpreter를 사용하고
- 만약 중복되는 코드를 발견하면 JIT 컴파일러를 사용
- JIT컴파일러가 컴파일 후 native 기계어로 변경
- navtive 기계어는 중복되는 함수 호출을 바로 사용가능
- Interpreter처럼 다시 interprete할 필요 없음
- JIT 컴파일러는 아래 구성요소로 이루어짐
- Intermediate Code Generator - generates intermediate code
- 매개(중간) 코드 생성함
- Code Optimizer - optimizes the intermediate code for better performance
- 매개 코드 최적화함
- Target Code Generator - converts intermediate code to native machine code
- 매개 코드를 네이티브 기계어로 변경함
- Profiler - finds the hotspots (code that is executed repeatedly)
- 중복 실행될 코드를 찾음
- Intermediate Code Generator - generates intermediate code
ex)
int sum = 10;
for(int i = 0 ; i <= 10; i++) {
sum += i;
}
System.out.println(sum);
인터프리터
는 메모리에 있는 sum을 매 루프마다 접근해서 가져와
1을 더하고 다시 메모리에 적용함
즉 매 루프마다 메모리를 접근하는 구조
JIT 컴파일러
는 중복되는 코들(HotSpot)를 발견하면
sum의 사본을 스레드의 PC register에 저장해서 매 루프마다 1을 더함
그리고 루프가 끝나면 다시 sum 메모리 접근해서 값을 적용함
Note
JIT compiter는 interpreter가 한 줄 한 줄 해석하는 것보다 시간이 더 걸림
그래서 한 번만 실행하면 되는 코드는 interpreter를 사용하는게 좋음
Garbage Collector
- Heap area에서 참조되지 않는 객체들을 모아 제거함
- 사용하지 않는 메모리를 다시 회수하는 작업을 함
두 상태가 있음
- Mark
- GC가 메모리에서 사용하지 않는 객체를 확인함
- Sweep
- GC가 Mark 단계에서 발견된 객체들을 삭제
- JVM이 정기적으로 알아서 GC를 함
- System.gc()해서 호출가능하나 안전하지 않음
JVM은 3가지 다른 타입의 GC를 포함
- Serial GC
- 가장 간단한 구현
- 싱글 스레드의 작은 애플리케이션에서 적합
- 실행하면
stop the world
를 호출- 모든 애플리케이션을 정지시킴
- argument to run: -XX : +UseSerialGC
- Parallel GC
- JVM의 default GC구현
- Throughput Collector라고도 함
- 멀티 스레드 가능 그러나
stop the world
발생하여
움직이는 애플리케이션을 정지시킴 - argument to run : -XX:+UseParallelGC
- G1 GC
- 큰 heap size를 사용하는 멀티 스레드를 위해 설계된 GC
- more than 4GB
- 쓰레기가 가장 많은 곳(메모리 상의)을 확인하고 거기에 먼저 GC 실행
- argument to run : -XX:+UseG1GC
- 큰 heap size를 사용하는 멀티 스레드를 위해 설계된 GC
Concurrent Mark Sweep (CMS) GC is Deprecated since Java 9 and completely removed Java 14
JNI(Java Native Interface)
- C/C++ 같은 네이티브 코드를 사용할 때 필요
- 하드웨어와의 통신이 필요하거나
메모리 관리의 최적화가 필요할 때 필요 (java로 한계가 있을 시)
Native Method Libraries
Native Method Libraries are libraries that are written in other programming languages, such as C, C++, and assembly. These libraries are usually present in the form of .dll or .so files. These native libraries can be loaded through JNI.
일반적인 JVM Errors
- ClassNotFoundException
- Class.forName(), ClassLoader.loadClass() or ClassLoader.findSystemClass()을 호출 후
(클래스를 로드할 때)정의된 클래스가 없으면 발생
- Class.forName(), ClassLoader.loadClass() or ClassLoader.findSystemClass()을 호출 후
- NoClassDefFouondError
- 컴파일은 잘 됐지만 런타임시 ClassLoader가 클래스 파일을 찾지 못할 경우 발생
- OutOfMemoryError
- 메모리 할당할 공간이 없을 경우
- GC로 더이상 메모리를 못 생성할 경우(꽉찬상태)
- StackOverFlowError
- 스택이 필요한데 더 스택을 사용할 수 없을 경우
끝
Leave a comment