| 백기선님의 라이브 스터디를 참고하여 작성한 게시물입니다.
C, C++은 컴파일 언어, Python, JavaScript는 인터프리터 언어... (중얼 중얼)
우리는 컴퓨터는 소스코드를 컴파일 또는 인터프리팅하여 머신 코드를 생성한다
는 사실을 알고 있다.
Q: 그렇다면 자바는 컴파일 언어? 인터프리터 언어?
A: 하이브리드
언어
이게 무슨 띠용한 소리인가
참고: Is Java a Compiled or an Interpreted programming language ?
JDK(?)의 자바 컴파일러는 자바로 작성된 소스코드를 컴파일 한다. 컴파일 결과물인 자바 바이트코드(?)는 JRE(?)의 구성요소인 JVM(?) 의해 실행되는데, 순차적으로 JVM의 구성요소인 ClassLoader(?)는 메모리에 클래스 메타 데이터를 Loading하는 역할을 담당하고...(중략)
사진 출처: https://techvidvan.com/tutorials/java-virtual-machine/
/Library/Java/JavaVirtualMachines/jdk-17.0.1.jdk/Contents/Home/bin
에 위치한 실행 파일이 실행된다./Library/Java/JavaVirtualMachines/jdk-17.0.1.jdk/Contents/Home/lib
에 위치java -verbose -version
로 어떤 클래스 파일들이 포함되었는지 확인해보자자바 바이트 코드를 실행하는 녀석
/Library/Java/JavaVirtualMachines
를 확인해보자
더 이상의 자세한 설명은 생략한다.
는 아니고 아래에서 자세히 다루도록 하겠다.
참고: [조금 더 깊은 Java] Java Bytecode 를 알아보자 (자바를 컴파일하면 어떤 일이 일어날까?)
// Test.java
public class Test {
public static void main(String[] args) {
System.out.println("main");
printHi();
InnerClass.printBye();
}
public static void printHi() {
System.out.println("hi");
}
static class InnerClass {
static void printBye() {
System.out.println("bye");
}
}
}
위의 코드를 컴파일했더니, Test.class
, Test$InnerClass.class
생성 되었다.
과연 바이트코드란 놈은 어떻게 생겨먹었을까
그냥은 확인하지 못한다. hex editor로 열어보자
봐도 뭔소린지 잘 모르겠다. JDK의 도움을 받아보자
javap -v -p -s Test.class
다음을 참고하자
마침내 JVM에 대해 깊게 알아볼 시간이 왔다. (으악)
사진 출처: https://dzone.com/articles/jvm-architecture-explained
JVM은 크게 ClassLoader, Runtime Data Area, Execution Engine으로 구분할 수 있다.
참고
Java 클래스로더 훑어보기
Oracle Java Virtual Machine Specification
자바는 매우 효율적으로 작동하도록 설계되었다.
그렇기 때문에 JVM은 모든 클래스를 최초 실행 시점에 메모리에 Load하지 않는다.
대신에, 자바 응용 프로그램이 특정 class를 필요로 할 때 동적으로 메모리에 클래스를 load한다.
Class Loader
가 바로 그 기능을 수행한다.
ClassLoader의 작동 과정을 3 단계로 구분할 수 있다.
바이너리 데이터
를 생성 & 메모리(Method area)에 저장한다.바이너리 데이터
는 다음과 같은 정보들로 구성된다.Class
타입의 객체를 생성 & Heap Area에 저장한다.Loading하려는 클래스들의 성격, 목적, 타이밍에 따라 서로 다른 ClassLoader가 사용된다.
ClassLoader.getPlatformClassLoader();
public class ClassLoaderExample {
public static void main(String args[]) {
try {
// 이 클래스의 클래스로더를 출력한다.
System.out.println("ClassLoaderExample.getClass().getClassLoader(): " + ClassLoaderExample.class.getClassLoader());
// 확장 클래스로더를 통해서 이 클래스를 다시 로드한다.
Class.forName("ClassLoaderExample", true, ClassLoaderExample.class.getClassLoader().getParent());
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
}
}
코드 참고: https://blog.hexabrain.net/397
로드된 클래스 파일들을 검증하고, 사용할 수 있게 준비하는 과정을 의미한다. Linking 과정을 세 단계로 구분할 수 있다.
static field(class Variable)
를 위한 메모리를 할당하고, 이를 기본값으로 초기화ClassLoader를 통해 바이트 코드는 JVM의 Run-time Data Area에 로드된다.
Run-time Data Area는 운영체제로 부터 할당받은 자바 응용프로그램 프로세스의 메모리 공간을 의미한다.
그런데 JVM의 메모리 영역은 어떻게 생겼을까?
고놈 참 기똥차게 생겼네
하나씩 살펴보자
Method area에 모든 바이트 코드들이 load & store 된다.
Constant Pool도 이곳에서 관리된다.
JVM 당 하나만 존재 -> 모든 Thread 들이 Method Area를 공유
JVM 의 다른 메모리 영역에서 해당 정보에 대한 요청이 오면, 실제 물리 메모리 주소로 변환해서 전달해줍니다. (?)
JVM 구동 시작 시에 생성이 되며, 종료 시까지 유지
GarbageCollection
의 주 대상참고: JVM stack과 frame
각 Thread 별로 따로 할당되는 영역 -> 동시성 문제에서 자유롭다.
각 Thread 들은 메소드를 호출할 때마다 Frame 이라는 단위를 push
메소드가 마무리되며 결과를 반환하면 해당 Frame 은 Stack 으로부터 pop
Frame 은 메소드에 대한 정보를 가지고 있는 Local Variable
, Operand Stack
그리고 Constant Pool Reference
로 구성이 되어있다.
Local Variable
은 메소드의 지역 변수들을 저장하는 영역
Operand Stack
은 메소드 내 연산을 위한 작업 공간
Constant Pool Reference
는 Constant Pool참조를 위한 공간
PC Registers
영역에 저장, 관리한다.PC Registers
영역을 가진다.참고
JVM - 실행 엔진(Execution Engine)
Understanding Java JIT Compilation with JITWatch, Part 1
JIT Compiler
How the JIT compiler boosts Java performance in OpenJDK
JIT(Just In Time) 컴파일러
기본적으론 JVM은 interpreter를 통해 바이트 코드를 기계어로 interprete하는 방식을 취한다.
JIT: Just in Time = 제 때 = 메소드가 호출될 때
JVM 인터프리터를 통해 모든 플랫폼(운영체제)에서 동일한 바이트 코드를 실행할 수 있다는 장점 이면에는 '성능'이라는 문제점이 있다.
이러한 문제를 해결하기 위해, JVM은 JIT Compiler를 사용한다.
동일한 코드에 대해 interprete하는 횟수가 threshold를 넘으면 JIT compiler가 별도의 스레드(컴파일 스레드)에서 해당 코드를 기계어로 컴파일한다.
threshold는 두가지 측면을 고려하여 결정된다.
package jit;
public class JitTest {
public static void main(String[] args) {
for (int i = 0; i < 500; ++i) {
long startTime = System.nanoTime();
for (int j = 0; j < 1000; ++j) {
new Object();
}
long endTime = System.nanoTime();
System.out.printf("%d\t%d\n", i, endTime - startTime);
}
}
}
java -XX:+PrintCompilation JitTest.java
로 자세히 확인해보자실행시간(ms), 코드블록 번호 | 코드 분류(n,s,!,% 등) | 코드 컴파일 티어 | 실행한 클래스 정보
n : native method
s : synchronized method
! : 이건 모르겠음;
% : 코드 캐싱된 작업이 수행됨 -> Jit compiler가 작동
코드 컴파일 티어: 얼마나 깊게(최적화 되어)컴파일 되었는가
Jit compiler가 작동되어 도출된 최적화된 기계어를 code cache에 저장
Jit compiler는 내부적으로 2개의 하위 컴파일러로 구성
모든 컴파일된 코드를 code cache에 저장할 순 없다. (용량 문제)
참고: Java Garbage Collection
JVM에 관하여 - Part 4, Garbage Collection 기초
공부 못하는 사람 특: 시험에 안 나오는 부분 공부함
Java에서는 개발자가 프로그램 코드로 메모리를 명시적으로 해제하지 않기 때문에 GC가 더 이상 필요 없는 객체를 찾아 지우는 작업을 한다.
GC는 두 가지 가설(전제 조건)을 기반으로 작동한다.
GC는 위 가설의 장점을 최대한 살리기 위해서 HotSpot VM에서는 크게 2개로 물리적 공간을 나누었다.
Yong Generation 영역
새롭게 생성한 객체의 대부분이 여기에 위치한다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다.
이 영역에서 객체가 사라질때 Minor GC
가 발생한다고 말한다.
Old Generation 영역
접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사된다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다.
이 영역에서 객체가 사라질 때 Major GC(혹은 Full GC)
가 발생한다고 말한다.