-
Notifications
You must be signed in to change notification settings - Fork 0
김규원, 김민주, 오민석, 최세민 ‐ JVM 학습
작성 중 입니다.
“Write Once, Run EveryWhere”
플랫폼마다 다른 기계어를 작성할 고민 없이 JVM이 이걸 해결해준다?
추상화된 인터페이스를 제공해주고, 개발자는 그 인터페이스에 맞추어 개발하면 된다고 이해하면 되려나????
바이트코드 형태?
!https://velog.velcdn.com/images/ddangle/post/f3d3383b-2883-4840-98e9-a9c4b08d8c49/image.png
Loading → Linking → Initializtion
- 자바의 바이트코드를 메소드 영역에 저장합니다.
- 클래스로더의 계층 구조
- Bootstrap Class Loader
- JAVA SE 핵심 클래스를 로드
- ex) java.base(Java.lang, java.util …), java.logging
- ex) java.lang.Object, java.lang.String …
- JAVA SE 핵심 클래스를 로드
- Platform ClassLoader
- 자바 SE 추가적인 클래스로드
- java.sql, java.xml
- 자바 SE 추가적인 클래스로드
- Application Class Loader
- 사용자가 정의 한 클래스와 서드파티 라이브러리 클래스를 로드합니다.
- Custom ClassLoader
- Bootstrap Class Loader
클래스로더의 중요 특징
Visibility - 상위 클래스로더는 하위 클래스로더가 알고있는 클래스를 알 수 없다. 하위 클래스로더는 상위 클래스로더가 알고 있는 클래스를 알 수 있다.
Uniqness - 클래스는 정확히 하나만
Unload Impossiblity - 클래스로더
- 검증: 클래스 파일의 명세를 검증한다
- 준비: 클래스가 필요로하는 메모리를 할당하고, 클래스에서 정의된 필드, 메소드, 인터페이스를 나타내는 데이터 구조를 준비한다.
- 분석: 심볼릭 레퍼런스를 메소드 영역에 있는 실제 메모리 레퍼런스로 교체합니다.
심볼릭 레퍼런스
바이트 코드를 확인하면 다음과 같이 심볼릭 레퍼런스를 확인할 수 있습니다.
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.myMethod();
}
}
class MyClass {
public void myMethod() {
System.out.println("Hello, World!");
}
}
// Main.class
0: new #2 // class MyClass
3: dup
4: invokespecial #3 // Method MyClass."<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method MyClass.myMethod:()V
12: return
런타임 다형성
만약 MyClass를 상속받은 ParentClass가 있다면?
public class Main {
public static void main(String[] args) {
MyClass obj = service.getObj(); // service는 MyClass 또는 ParentClass의 인스턴스를 반환할 수 있다.
obj.myMethod();
}
}
class MyClass {
public void myMethod() {
System.out.println("MyClass method");
}
}
class ParentClass extends MyClass {
@Override
public void myMethod() {
System.out.println("ParentClass method");
}
}
// Main.class
0: aload_1 // obj를 로드
1: invokevirtual #4 // Method MyClass.myMethod:()V
4: return
런타임에 객체 타입을 확인하고 해당 클래스의 vtable을 조회합니다.
vtable에서 실제 타입의 실제 메소드 메모리 주소를 찾아서 메소드를 호출합니다.
실행 엔진은 메모리에 적재된 클래스 (.class를? 바이트코드, 클래스로딩 시점에 메모리에 적재,) 들을 기계어로 변경하여 명령어 단위로 실행함
→ 이게 JVM 설계철학의 핵심, 자바가 플랫폼에 독립적이고, 이식성이 높은 언어인 이유는 인터프리터 덕분임
.java로 만든 걸 .class로 컴파일 하는 게 Execution Engine이 해석할 수 있는 내용으로 만드는 것
.class
로 만들어진 게 각각의 OS에 맞는 실행 엔진 (Interpretor)에 의해 수행된다
.class 파일(바이트 코드)
는 Runtime Data Areas
https://www.javatpoint.com/java-interpreter
JVM 인터프리터는 런타임 중에 바이트 코드를 한 줄씩 읽으면서 실행함
전체 소스코드의 명령어를 수집하여 재구성하는 컴파일러 언어와 달리
인터프리터는 소스코드의 각 행을 연속적으로 분석하며 실행하기 때문에 일반적으로 느림
바이트코드를 인터프리터가 실행하는 과정
-
상수 풀 초기화 (클래스 로딩 시점)
-
클래스 준비
- 클래스 변수(static 변수)의 메모리를 할당하고 기본값으로 초기화합니다.
-
Class Resolution
- 심볼릭 레퍼런스를 실제 메모리 레퍼런스로 변환합니다.
→ 심볼릭 레퍼런스란
-
클래스 초기화
- 클래스 변수의 초기 값 할당과
static
블록 실행.
- 클래스 변수의 초기 값 할당과
여기까지는 클래스로딩시점?
-
메서드 디스패치 테이블을 생성함
- 메서드 디스패치 테이블을 생성하여 메서드 호출 시 빠른 접근을 가능하게 함
-
메서드 엔트리 포인트 **(프로그램이 시작되는 main메서드)**를 찾아서 실행
-
인터프리터는 메서드의 바이트코드를 한 줄씩 읽고 실행
이 과정에서 바이트 코드를 해석하는데
- Fetch
- Decode
- Execute
명령어를 통해 실행함
하지만 성능이 느림
실제 기계어로 변환하여 코드캐싱
코드 컴파일을 수행할 기준을 의미함
컴파일 임계치를 만족하는 코드는 JIT 컴파일러에 의해 컴파일이 수행됨
컴파일 임계치는 아래 두 가지 횟수를 합친 것을 의미함
- method entry counter - JVM 내에 있는 메소드가 호출된 횟수
- back-edge loop counter - 메소드가 루프를 빠져나오기까지 회전한 횟수
객체를 저장하는 공간으로 GC의 대상이 되는 공간이다. (ex. new String("abc")) String Constant Pool 은 Heap 영역에 존재한다.
(new String("abc") 와 String s = "abc" 의 차이 참고)
Heap 영역
-
Permanent : 생성된 객체들의 정보의 주소값이 저장되는 공간. 클래스 로더에 의해 load되는 class, method 등에 대한 메타 정보가 저장되며 JVM에 의해 사용된다. Reflection 을 사용하여 동적으로 클래스가 로딩 되는 경우에도 사용된다.
- Reflection이란? 객체를 통해 클래스의 정보를 분석해 내는 프로그래밍 기법구체적인 클래스 타입을 알지 못해도, 컴파일된 바이트 코드를 통해 역으로 클래스의 정보를 알아내어 사용할 수 있다는 뜻이다.
-
New/Young
- Eden : 객체들이 최초로 생성되는 공간. Eden 영역이 가득차게 되면 첫 번째 GC가 발생한다. (Eden 영역에 있는 값들을 Survivor 1 영역에 복사하고 이 영역을 제외한 나머지 객체를 삭제)
- Survivor 0/1 : Eden 에서 참조되는 객체들이 저장되는 공
-
왜 Survivor 영역은 2개일까?
메모리 (외부) 단편화 문제를 해결하기 위함이다. 메모리 할당-해제 과정에서 생기는 메포리 파편화를 해결하기 위해 다른 Survivor 영역으로 옮길 때 외부 fragmentation 을 제거해줍니다.
-
-
Old : Young 에서 가비지 컬렉션으로 마지막까지 살아남은 객체가 Old 로 오게된다. 즉, 오래된 데이터가 저장되는 영역
GC 절차
- 새로 생성한 대부분의 객체는 Eden 영역에 위치
- Eden 영역이 차버리는 시점이 되면 Minor GC 발생 - Eden 영역에서 살아남아있던 객체가 Survivor 영역 중 하나로 이동
- 이후에도 Eden 영역에서 GC가 발생하면 차고 있는 Survivor 영역으로 이동
- Survivor 영역 중 하나가 차버리는 시점이 되면 Minor GC 발생 - 원래의 영역을 비우고 다른 Survivor 영역으로 이동
- 이 과정을 반복하다가 계속해서 살아 남은 객체 - Old 영역으로 이동
Eden 영역에서 객체를 할당하는 방법
- bump-the-pointer : Eden 영역에 제일 마지막으로 할당된 객체를 통해 다음에 할당된 메모리에 대한 next pointer를 가져온다. 이 포인터를 통해 빠르게 메모리 할당을 한다. 그러나 Thread-safe하지 못하여 여러 쓰레드를 사용할 때 lock 이 걸린다.
- TLABs (Thread-Local Allocation Buffers) : Eden 영역의 작은 덩어리에 대해 쓰레드별로 접근하도록 한다. 이를 통해 Thread-safe 하게 객체를 할당할 수 있다.
Stop the World
GC 를 수행하기 위해 JVM 프로그램이 멈추는 현상을 의미한다. 멈추는 상황이 오래 발생하면 성능이 좋지 못하기 때문에 이 시간을 줄이는 것이 핵심이다.
Mark and Sweep 알고리즘
- mark : 사용되는 메모리와 사용되지 않는 메모리를 식별하는 작업
- sweep : mark 단계에서 사용되지 않음으로 식별된 메모리를 해제하는 과정
Old 영역에서 Young 영역의 객체 참조
Old 영역에 카드 테이블이 존재한다. 이 카드 테이블에는 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때마다 정보가 표시된다.
Young 영역에서 GC가 발생할 때 해당 객체를 Old 영역에서 참조한다면 없애서는 안된다. 이 과정에서 Card table를 확인하면서 Old 영역에서 참조를 하는지 확인한다.
Old 영역의 GC
- Serial GC : mark-sweep-compact 알고리즘. cpu 코어 하나인 상황을 위함
- Parallel GC : Serial GC와 달리 멀티 쓰레드
- Parallel Old GC(Parallel Compacting GC) : mark-sumary-compact 알고리즘. sweep 는 단일 쓰레드, sumarry 는 여러 쓰레드가 old 영역을 분리하여 스캔. Summary 단계에서는 앞서 GC 를 수행한 영역에 대해서 별도로 살아있는 개체를 색별한다는 점에서 다르며 좀더 복잡함
- Concurrent Mark Sweep GC: Parallel GC 와 마찬가지로 여러 개의 쓰레드를 이용. 하지만 기존의 Serial GC 와 Parallel GC 와는 다르게 mark sweep 알고리즘을 concurrent 하게 진행
- G1 GC : Heap 를 동일한 크기의 Region 으로 나누고, 가비지가 많은 region 에 대해 우선적으로 GC 수행1. Minor GC한 지역에 객체를 할당하다가 해당 지역이 꽉 차면 다른 지역에 객체를 할당하고, Minor GC가 실행된다. G1 GC는 각 지역을 추적하고 있기 때문에, 가비지가 가장 많은(Garbage First) 지역을 찾아서 Mark and Sweep를 수행한다.Eden 지역에서 GC가 수행되면 살아남은 객체를 식별(Mark)하고, 메모리를 회수(Sweep)한다. 그리고 살아남은 객체를 다른 지역으로 이동시키게 된다. 복제되는 지역이 Available/Unused 지역이면 해당 지역은 이제 Survivor 영역이 되고, Eden 영역은 Available/Unused 지역이 된다.
- Major GC(Full GC)시스템이 계속 운영되다가 객체가 너무 많아 빠르게 메모리를 회수 할 수 없을 때 Major GC(Full GC)가 실행된다. 그리고 여기서 G1 GC와 다른 GC의 차이점이 두각을 보인다.기존의 다른 GC 알고리즘은 모든 Heap의 영역에서 GC가 수행되었으며, 그에 따라 처리 시간이 상당히 오래 걸렸다. 하지만 G1 GC는 어느 영역에 가비지가 많은지를 알고 있기 때문에 GC를 수행할 지역을 조합하여 해당 지역에 대해서만 GC를 수행한다. 그리고 이러한 작업은 Concurrent하게 수행되기 때문에 애플리케이션의 지연도 최소화할 수 있는 것이다.
🔥 JVM 메모리는 실제 가상 메모리 시스템 위에서 동작합니다. JVM이 할당한 힙, 스택, 메타스페이스 등의 메모리 영역은 가상 주소 공간 내에서 관리되며, 이 가상 주소 공간은 운영 체제의 가상 메모리 시스템에 의해 물리 메모리로 매핑됩니다.

-
메소드 영역
: 클래스 정보 저장, Runtime Constant Pool도 여기에 해당됨 -
힙 영역
: new로 생성되는 객체들을 저장 -
스택
: 각각의 스레드마다 하나씩 생성 -
PC 레지스터
: 스레드마다 생성, PC 값 저장


- 정적 필드와 클래스 구조만을 가지고 있음
- 이는 JVM 시작시 생성되고 명시적으로 null이 선언되거나 프로그램 종료 시까지 유지됨
-
Young Generation
: 생명 주기가 짧은 객체를 GC 대상으로 하는 영역. -
Eden
: new를 통해 새로 생성된 객체가 위치. 정기적인 쓰레기 수집 후 살아남은 객체들은 Survivor로 이 -
Survivor 0
/Survivor 1
: 각 영역이 채워지게 되면, 살아남은 객체는 비워진 Survivor로 순차적으로 이 -
Old Generation
: 생명 주기가 긴 객체를 GC 대상으로 하는 영역. Young Generation에서 마지막까지 살아남은 객체가 이동