-
JVM3프로그래밍 언어/JAVA 2020. 5. 2. 18:19
Runtime Data Areas
- Runtime Data Areas의 구조
- PC Registers
- Java Virtual Machine Stacks
- Native Method Stacks
- Method Area
- Java Heap
- Runtime Data Area Simulation
Runtime Data Areas의 구조
Process로서 JVM이 프로그램을 수행하기 위해 OS로부터 할당 받은 메모리 영역
Runtime Data Areas는 각각의 목적에 따라 5개의 영역으로 나뉨
-
- PC Registers Thread 별 생성
- Java Virtual Machine Stacks Thread 별 생성
- Native Method Stacks Thread 별 생성
- Method Area Thread에게 공유
- Heap Thread에게 공유
내용
PC Registers
Java는 Stack-Base로 작동
JVM은 CPU에 직접 Instruction을 수행하지 않고 Stack에서 Operand를 뽑아내어 이를 별도의 메모리 공간(PC Registers)에 저장하는 방식
Java는 플랫봄별 도립적으로 동작, 하지만 JVM도 OS나 CPU 입장에서 머신에서 동작하는 하나의 프로세스
Java도 현재 작업하는 내용을 CPU에 Instruction을 제공해야함 → 이를 위한 버퍼 공간으로 PC Registers라는 메모리 영역 생성
PC Registers는 Thread 마다 하나씩 존재, Thread가 시작할 때 생성
만약 Thread가 Java Method를 수행하고 있으면 PC Register에는 현재 수행 중인 JVM Instruction의 주소를 가지게 됨
반면, C언어 등과 같은 Native Method를 수행하고 있다면 PC Registers는 undefined로 있게됨
Java Virtual Machine Stacks
Thread의 수행정보를 기록하는 Frame을 저장하는 메모리 영역
Java Virtual Machine Stacks은 Thread 별로 존재하며 Thread가 시작할 때마다 생성
다른 Thread는 접근할 수 없기 때문에 동기화 이슈는 발생하지 않음
ex) Local Variables
JVM은 Stack Frame을 Java Virtual Machine Stack에 단순 push 및 pop 작업만을 수행
Stack Frame 중에 현재 수행하는 Method의 정보를 저장하는 것을 Current Frame이라고 하고 현재 수행하고 있는 Method의 클래스를 Current Class라고 함
Stack Frame에는 Method의 Parameter Variables, Local Variables, 연산의 결과 등과 같은 데이터들을 저장
Thread가 Java Method를 하나 수행하게 되면 JVM은 Stack Frame을 하나 생성하여 Java Virtual Machine Stacks에 push 함
그래서 새롭게 들어간 Stack Frame은 Current Frame이 됨
해당 Method가 수행을 마치게 되면 Java Virtual Machine Stacks에서 pop하게 되고 이전의 Stack Frame이 Current Frame이 됨
그리고 만약 비정상 처리가 되어 Exception 처리를 하는 작업을 수행한 후 Java Virtual Machine Stacks에서 사라지게 됨
Stack Frame
Thread가 수행하고 있는 Application를 Method 단위로 기록하는 곳
Stack Frame
-
-
- Local Variable Section
- Operand Stack
- Frame Data
-
Stack Frame은 Class의 메타 정보를 이용하여 적절한 크기로 고정됨 ← compile time에 크기를 결정가능함
Local Variable Section
Method의 Parameter Variable과 Local Variable 들을 저장
Local Variable Section은 0부터 시작하는 인덱스를 가진 Array로 구성, Array의 인덱스를 통해 데이터에 접근
Method의 Parameter Variable는 선언된 순서로 인덱스가 할당, Local Variable는 compiler가 알아서 인덱스를 할당
Parameter Variable과 Local Variable이 int와 같은 Primitive Type인 경우 고정된 크기로 할당
Object나 Array, String과 같은 객체는 가변크기로 reference 현으로 할당 받음
객체정보는 Local Variable Section이나 Stack Frame이나 Java Virtual Machine Stacks에 직접적으로 저장되는 것이 아니라 해당 객체가 존재하는 Heap의 위치를 말해주는 Reference를 저장
char, byte, short, boolean 형으로 선언한 것들이 Local Variable Section에서는 모두 int 형으로 할당
Primitive Type이라 할지라도 JVM의 지원여부와 JVM Stacks의 저장형태에 따라 달라짐
char, byte, short, boolean 는 Local Variable Section이나 Operand Stack에서는 Int 형으로 변환되어 저장/ Heap에서는 원래의 형으로 저장
0번 인덱스의 경우 hidden this로 정의
모든 Local Method 혹은 Instance Method에 무조건 포함, 여기에 저장된 Reference를 통해 Heap에 있는 Class의 instance 데이터를 찾게 됨
Operand Stack
내용
JVM의 작업 공간 - JVM이 프로그램을 수행하면서 연산을 위해 사용되는 데이터 및 그 결과를 Operand Stack에 집어넣고 처리
Operand Stack, Array로 구성
Frame Data
Constant Pool Resolution 정보와 Method가 정상 종료했을 때의 정보들 그리고 비정상 종료했을 시에 발생하는 Exception 관련 정보들을 저장
Constant Pool Resolution
Resolution은 Symbolic Reference로 표현된 Entry를 찾아 Direct Reference로 변경하는 과정을 의미
Class의 모든 Symbolic Reference는 Method Area의 Constant Pool이라는 곳에 저장, Resolution을 Constant Pool Resolution이라고 명함
Frame Data에 저장된 Constant Pool Resolution은 관련 Constant Pool의 Pointer 정보, JVM은 이 Pointer를 이용하여 필요할 때 마다 Constant Pool을 찾아감
보통 상수를 가져올 때 Constant Pool의 Entry 정보를 참조하기도 하지만 다른 Class를 참조하거나 Method를 수행하거나 아니면 특정 변수를 접근할 때에도 Constant Pool를 참조
Java의 모든 Reference는 Symbolic Reference 이므로 Class나 Method 그리고 변수나 상수에 접근할 때에도 이러한 Resolution이 수행
그리고 특정 Object가 특정 Class나 Interface에 의존 관계가 있는지 확인하기 위해서도 Constant Pool의 Entry 참조
JVM Stacks에 Current Frame이 pop이 되고 사라지면 이전에 이 Method를 호출했던 Method의 Stack Frame이 Current Frame이 됨, 여러 Stack Frame 중 어디로 돌아가는가?
Frame Data에는 자신을 호출한 Stack Frame의 Instruction Pointer가 존재함
Method가 종료되면 JVM은 이 정보를 PC Register에 설정하고 Stack Frame을 빠져나감
해당 Method의 반환값이 있다면 이 반환값을 Current Frame 즉, 자신을 호출한 Method의 Stack Frame의 Operand Stack에 Push하는 작업 병행
Native Method Stacks
Java는 Java 외의 언어로 작성된 프로그램, API 툴킷 등과이 통합을 쉽게 하기 위하여 JNI(Java Native Interface)라는 표준 규약을 제공
즉, Native Code 로 되어 있는 Fuction의 호출을 Java 프로그램 내에서 직접 수행할 수도 있고 그 결과 값을 받아 올 수 있게 됨
JNI의 경우 앞서 말한 Stack과 Stack Frame과 유사한 방식을 통해 Method 호출 → Native Method Stacks
Native Method Stacks은 Native Code가 C로 작성되 있으면 C stack으로 C++로 생성되어있으면 C++ stack으로 생성
Native Method Stacks이 끝나면 다시 Thread의 Java Virtual Machine Stacks로 돌아옴
이때, Native Method를 호출한 Stack Frame으로 돌아오는 것이 아닌 새로운 Stack Frame을 생성하여 다시 작업을 수행
Hotspot JVM이나 IBM JVM은 앞서 Java Virtual Machine Stacks과 Native Method Stacks을 구분하지 않음 → Native Stack으로 통합
그렇기 때문에 Java Method를 수행하는지 아니면 Native Method를 수행하는지를 따져 Stack Frame이 Java Stack Frame인지 Native Stack Frame인지 구분할 뿐
Method Area
내용
class area, method area, code area, static area 라고 불려짐
모든 Thread 들이 공유하는 메모리 영역
이 영역은 Load 된 Type(Class 나 Interface)을 저장하는 논리적 메모리 공간
Method Area는 JVM이 기동할 때 생성이 되며 Garbage Collection의 대상
Method Area 벤더마다 다르게 구현
Type Information in Method Area
- Type Information
- Constant Pool
- Field Information
- Method Information
- Class Variables
- Reference to class - ClassLoader
- Reference to class - Class
Type Information
가장 기본이 되는 정보, Type에 대한 전반적인 내용 포함
Full Qualified Name - Package.class 형태를 지니는 Type의 전체 이름
Type의 직계 super class의 전체 이름 (Type이 interface이거나 java.lang.object class이거나 super class가 없는 경우 제외)
Type이 Class인지 Interface인지 여부
Type의 Modifier
Interface인 경우 직접 Link 되고 있는 객체의 리스트로 객체는 전체 이름(package.class)으로 표현됨
Constant Pool
전체 JVM에서 가장 중요한 역할을 수행하는 곳
Type의 모든 Constant 정보를 가지고 있는 부분
여기서 Constant는 상수는 물론 Literal Constant, Type, Field(Member Variable, Class Variable), Method로의 모든 Symbolic Reference까지 확장한 개념
앞서 Symbolic Reference 언급하였는데 Constant Pool 내에 저장됨, JVM은 실행 시 참조하는 객체에 접근할 필요가 있으면 Constant Pool의 Symbolic Reference를 통해 해당 객체가 위치한 메모리 주소를 찾아 동적으로 연결
Field Information
Type에서 선언된 모든 Field의 정보
Field Information에는 Field의 정보가 선언된 순서대로 기록
- Field 이름
- Field의 Data Type, 선언된 순서
- public, private, protected, static, final, volatile, transient 와 같은 Field의 Modifier
추가
Variable
-
- Instance Variable ----- Field (non-static)
- Class Variable --------- Field (static)
- Local Variable --------- method에 속함
- Parameter -------------- method에 속함
Method Information
Type에 선언된 모든 Method의 정보를 의미
- Method 이름
- Method의 반환 값의 Data Type 또는 void
- Method Parameter의 수와 Data Type, 선언된 순서
- public, private, protected, static, final, synchronized, native, abstract 와 같은 Method의 Modifier
만약, Method가 native나 abstract가 아니라면 다음의 정보가 추가됨
-
- Method의 Bytecode
- Method의 Stack Frame의 Operand Stack 및 Local Variable Section의 크기
- Exception Table
Class Variables
Class에서 static으로 선언된 변수
Class에서 하나의 값으로 유지
해당 변수는 Instance에 공유되기 때문에 이를 이용하는데 있어 동기화 이슈 발생
Class Variable을 final로 선언할 경우 이를 변수가 아닌 상수 취급하며 Constant Pool에 Literal Constant로 저장
Reference to class - ClassLoader
Type이 JVM에 Load 될때 항상 이 Type은 어떤 ClassLoader를 경유하여 Loading 되었는지를 추적하게 됨
한 Type이 다른 Type을 참조할 때 같은 ClassLoader를 사용하도록 되어 있음
User-Defined ClassLoader - 해당 ClassLoader의 Reference를 Type의 정보 중 하나로 저장하게 됨
Bootstrap ClassLoader - Reference가 Null로 저장
위 정보는 Dynamic Linking을 할 때 해당 Type과 동일한 ClassLoader를 통해 참조하는 Type을 Loading 하기 위해 사용
Reference to class - Class
Type이 JVM에 Load 되면 항상 java.lang.class Class의 Instance가 하나 생성됨
Method Area에는 Type 정보의 일부로 이 Instance의 Reference를 저장하고 있음
Method Table
Java는 캡슐화된 객체를 이용하는 프로그래밍 언어로 Reference를 통해 객체를 찾아 다니는 일이 프로그램 수행 중 빈번히 일어남
Method Area에서 원하는 정보를 찾는 속도는 성능의 중요한 이슈!
이를 위해 JVM을 설계하는 사람들은 Method Table이라는 데이터구조 사용
Heap
내용
Instance(또는 Object)와 Array 객체 두 가지 종류만 저장되는 공간
모든 Thread 들에 의해 공유되는 공간
같은 Application을 사용하는 Thread 사이에서는 공유된 Heap Data를 이용할 때 동기화 이슈가 발생할 수 있음
JVM은 Java Heap에 Memory를 할당하는 Instruction(Bytecode로 new, newarray, anewarray, multianewarray)만 존재하고 메모리 해제를 위한 어떤 코드도 존재 X
Java Heap의 메모리 해제는 오로지 Garbage Collection을 통해 수행
Opbject Layout
Heap에 저장되는 Object와 Array는 모두 Header와 Data로 나뉘어져 있음
Hotspot JVM의 Opbject Layout
Object는 두 개의 Header
Array는 세 개의 Header
헤더 하나당 1word의 크기를 가지고 있음 (32bit - 4byte / 64bit - 8byte)
First Header - Mark Word, Garbage Collector와 Synchronization 작업을 위해 사용
Hash Code(0)Thread ID(1)AgeBiased(1 bit)Tag
Lock Record Address
Monitor Address
Forwarding Adress 등등
Mark Word의 헤더1
Biased BitTag상태
0
01
Unlocked
0
00
Light-weight locked
0
10
Heavy-weight locked
0
11
Marked for GC
1
01
Biased / Biasable
Mark Word의 헤더2 (하위 3bit)
하위 3bit는 Biased Bit가 1이면 Biased Lock 사용
Synchronization 작업을 수행할 때 Lock을 휙득하기 위한 연산을 수행하지 않고 가볍게 Lock을 휙득한다는 의미
Biased Lock을 사용하게 되면 Biased Lock로 Synchronization을 수행하는지에 대한 상태 값만 존재
Biased Bit가 0 → Biased Lock 사용 X, Tag 별로 4 가지 상태 나타냄
Hash Code 또는 Thread ID는 23 bit 할당 ← Biased Bit와 Tag에 따라 달라짐
Biased Bit가 1이면 Biased Lock을 획득한 Thread의 ID가 기록
Biased Bit가 0이고 이전 Synchronization 작업 때 산출 된 Hash Code가 있다면 Tag에 따라 각각 달리 값이 저장
Light-weight locked - Lock Record Address
Heavy-weight locked - Monitor Address 등의 Lock과 관련된 Pointer 정보
Marked for GC - Forwarding Address의 정보를 Hash Code로 가지고 있게 됨
Age - 6bits로 Young Generation의 Object가 Eden과 Survivor를 넘나든 횟수를 기록
두 번째 Header에는 Method Area의 Class 정보를 가리키는 Reference 정보가 저장 ← 이는 Object와 Array 모두 동일
Array의 경우 Array Size를 위한 Header가 하나 더 추가
Hotspot JVM의 Opbject Layout
VTable
VTable Structure의 pointer를 가지고 있음
VTable pointer는 Object Information(OI) Structure 정보 가짐
OI - Class Name, Debug Data, Object Type, Object Size 등의 정보 포함, GC에 의해 사용됨
Lock Word
Contention bit은 Thread 간에 이 Object 의 lock을 획득하기 위한 경합이 발생하면 On, Base Lock System인 Fat Lock이 사용됨
Lock Reservation을 사용하는 Reserved Mode에서는 Off
Heap 구조
HotspotJVM의 Heap 구조
Generational Heap 구조
- Young Generation - Eden 영역과 Survivor 영역으로 구성
- Old Generation - 비교적 참조가 많이 되는 Object 들을 저정하는 공간
Eden 영역 - Object가 Heap에 최초로 할당 되는 장소
Eden 영역이 가득차게 되면 참조 여부를 따져 Live Object이면 Survivor 영역으로 참조가 끊어지면 Garbage Object면 그냥 남겨놓음
모든 Live Object가 survivor 영역으로 넘어가면 Eden 영역을 청소(Scavenge) 함
survivor 영은은 두개로 구성, Live Object를 대피시킬 때는 하나의 Survivor 영역만 사용 ← 이러한 과정을 Minor GC라고 함
Young Generation에서 Live Object로 오래 살아남으면 Old Generation으로 이동
IBM JVM의 Heap 구조
1.5 이후 Hotspot과 유사
Freetype lib 공부
configure --enable-unlimited-crypto
default restrictions off
--disable-debug-symbols --disable-zip-debug-info
--enable-freetype-bundling
--with-cacerts-file=path/to/cacerts
bash ./configure --help
ccache를 이용하면 빌드 속도가 빨라진다
c/c++ native 소스를 많이 가지고 있음 → 재컴파일하는 것이 빈번함 → ccache는 재컴파일 시 컴파일 해논 것을 불러옴 → 빌드 빨라짐
ccache 패키지 설치
--enable-ccache
JEP
JSR - Java Specification Requests
LTS - Long Term Supports
JVM technology stack
JVM 스펙
결과를 실행 스택에 보관
스택의 맨 위에 쌓인 값들을 가져와서 계산
JVM 인터프리터의 기본로직
평가 스택을 이용해 중간값들을 담아두고 가장 마지막에 실행된 명령어와 독립적으로 프로그램을 구성하는 opcode을 하나씩 순서대로 처리하는 while 루프 안의 switch 문
자바 클래스로딩 메커니즘
- 부트스트랩 클래스 실행, 다른 클래스로더가 나머지 시스템에 필요한 클래스를 로드할 수 있게 최소한의 필수 클래스(java.lang.Object, Class, Classloader)만 로드
- 자바 런타임 코어 클래스 로드, 자바 8이전까지 rt.jar 로딩, 자바9 이후부터는 런타임 모듈화되고 클래스 로딩 로직 바뀜
- 확장 클래스 로더, 부트스트랩 클래스로더를 자기 부모로 설정하고 필요할 때 클래스로딩 작업을 부모에게 넘김, 자바8에서 탑재된 자바스크립트 런타임 내시혼이 확장 클래스 로드
- 애플리케이션 클래스 로더, 지정된 클래스패스에 위치한 유저 클래스를 로드
바이트코드 실행
javac를 이용한 컴파일
자바 소스 코드 --> .class 파일
바이트코드는 특정 컴퓨터 아키텍처에 특정하지 않은 중간표현형(IR)
클래스 파일이라는 것을 뜻하는 매직 넘버(4 바이트)
클래스 파일을 컴파일할 때 꼭 필요한 메이저/마이너 버전 숫자(4 바이트)
constant pool에는 코드 곳곳에 등장하는 상숫값이 있음, JVM은 코드를 실행할 때 런타임에 배치된 메모리 대신, 이 constatn pool 테이블을 찾아보고 필요한 값을 참조
access flag는 클래스에 적용한 수정자를 결정, 플래그 첫 부분은 일반 프로퍼티(public, final ,,,), 해당 클래스 파일이 인터페이스인지 추상 클래스인지도 표시, 끝 부분에는 클래스 파일이 소스코드에 없는 합성 클래스인지, annotation인지 enum인지 나타냄
this 클래스, super 클래스, 인터페이스 엔트리는 클래스에 포함된 타입 계층 나타내며, 각각 상수 풀을 가리키는 인덱스로 표시
생성된 바이트코드를 java -c Helloworld
this 레퍼런스를 스택 상단에 올려놓는 aload_0 명령 실행
invokespecial 호출, 슈퍼생성자들을 호출하고 객체를 생성하는 등 특정 작업을 담당하는 인스턴스 메서드를 실행, 디폴트 생성자를 오버라이드한 코드가 없으므로 Object 디폴트 생성자가 매치
main 메서드 실행
iconst_0, 정수형 상후 0을 스택에 push하고 istore_1으로 이 상수값을 오프셋 1에 위치한 지역 변수(루프의 i)에 저장
지역 변수 오프셋은 0부터 시작, 인스턴스 메서드에서 0번째 엔트리는 무조건 this
오프셋 1의 변수를 스택으로 다시 로드(iload_1)한 뒤, 상수 10을 push(bipush 10)한 다음 if_cmpge로 둘을 비교
8번 명령으로 넘어가, System.out의 정적 메서드를 해석(getstatic #2)
constant pool에서 "hello world"라는 스트링을 로드(ldc #3)
invokevirtual 명령으로 이 클래스에 속한 인스턴트 메서드 실행
정수값은 하나 증가(iinc 1,1), goto을 만나 2번 명령으로 되돌아감
if_icmpge 테스트가 성공할 때까지 반복하다가 22번 명령으로 제워권이 넘어가 메서드 반환
핫스팟 입문
제로 코스트 추상화 (zero-cost abstraction)
제로-오버헤드 원칙 - 사용하지 않는 것에는 대가를 치르지 않음, 사용하는 코드보다 더 나은 코드를 건네 줄 수 없음
C/C++은 컴뷰터와 OS가 실제로 어떻게 작동해야 하는지 언어 유저가 아주 세세한 저수준까지 일러주어야 한다는 것
자바는 이러한 제로 오버헤드 원칙을 따르지 않음
이런 언어로 작성한 코드 소스를 빌드하면 해당 플랫폼에 특정한 기계어로 컴파일 됨
(Ahead of Time(AOT) 컴파일)
JIT 컴파일이란
기본적인 인터프리터 대신 프로그램 성능을 최대로 내기 위해 네이티브 기능을 활용해 CPU에서 직접 프로그램을 실행하도록 하는 기술
핫스팟은 인터프리티드 모드로 실행하는 동안 애플리케이션을 모니터링하면서 가장 자주 실행되는 코드 파트를 발견해 JIT 컴파일을 수행
특정 메서드가 어느 한계치 threshold을 넘어가면 프로파일러가 특정 코드 섹션을 컴파일 및 최적화 수행
JVM 메모리 관리
자바는 Garbage Collection이라는 프로세스를 이용해 힙 메모리를 자동 관리하는 방식으로 해결
JVM이 더 많은 메모리를 할당해야 할 때 불필요한 메모리를 회수하거나 재사용하는 불확정적 프로세스
스레딩과 자바 메모리 모델(JMM)
자바 환경 자체가 JVM처럼 멀티스레드 기반
성능 측정 및 작업 분석하기가 한층 더 까다로워짐
주류 JVM 구현체에서 자바 애플리케이션 스레드는 각각 정확히 하나의 전용OS 스레드에 대응됨
JVM 구현체 종류
OpenJDK
Oracle
줄루 - 아줄 시스템이 제작한 자바 풀 인증 받은 OpenJDK 구현체
징 - 아줄 시스템이 제작한 고성능 상용 JVM, 자바 풀 인등, 64비트 리눅스에서만 동작
아이스티 - 레드헷
J9 - IBM이 만든 사용 JVM --> 오픈소스화 됨
애비안 - 100% 자바 인증을 받지 않았지만 JVM 학습을 위한 훌륭한 도구, 오픈소스
JVM 모니터링과 툴링
자바 관리 확장(JMX)
자바 에이전트(Java Agent)
JVM 툴 인터페이스(JVMTI)
서비서빌리티 에이전트(SA)
VisualVM
Garbage Collection 기초
시스템에 있는 모든 객체의 수명을 정확히 몰라도 런타임이 대신 객체를 추적하여 쓸모없는 객체를 알아서 제거하는 것
자동으로 회수한 메모리는 깨끗이 비우고 재활용할 수 있음
Garbage Collection 기본 원칙
- 알고리즘은 반드시 모든 가비지를 수집
- 살아 있는 객체는 절대로 수집해선 안됨
두번째 원칙이 중요. 살아있는 객체를 수집하게 되면 세그멘티이션 결함이 발생
1 마크 앤 스위프
GC 알고리즘의 기본 개념과 이를 응용해 메모리를 어떻게 자동 회수하는 지 알아봄
기초
할당 됐지만 아직 회수되지 않은 객체를 가리키는 포인터를 포함한 allocated list를 사용
- 할당 리스트를 순회하면서 마크 비트를 지움
- GC 루트부터 살아 있는 객체를 찾음
- 이렇게 찾은 객체마다 마크 비트를 셋팅
- 할당 리스트를 순회하면서 미크 비트가 세팅되지 않은 객체를 찾음
- 힙에서 메모리를 회수해 free list에 되돌림
- allocated list에서 객체를 삭제
깊이 우선 방식을 통해 살아있는 객체를 찾음, 이렇게 생성된 객체 그래프를 live object graph라고 하며 접근 가능한 객체의 전이 폐쇄라고도 함
GC 용어
STW - GC 사이클이 발생하여 가비지를 수집하는 동안 모든 애플리케이션 스레드가 중단, 따라서 애플리케이션 코드는 GC 스레드가 바라보는 힙 상태를 무효환할 수 없음. 이럴때 GC 알고리즘은 대부분 이럴때 STW 일어남
동시 (concurrency)
GC 쓰레드는 애플리케이션 스레드와 동시 실행될 수 있음
이는 굉장히 어렵고 비싼 작업이며 100% 동시 실행을 보장하지 않음
CMS(Councurrent Mark and Sweap) 사실 상 '준 동시 수집기'라고 해야함
병렬
여러 쓰레드를 동원해서 가비지 수집함
객체를 런타임에 표현하는 방법
oop = Ordinary Object Pointer, 평범한 객체 포인터
핫스팟 런타임에 oop라는 구조체로 자바 객체를 나타냄
oop는 참조형 지역 변수 안에 위치, 여기서 자바 메서드의 스택 프레임으로부터 자바 힙을 구성하는 메모리 영역 내부를 가리킴
oop를 구성하는 자료구조는 여러가지
instanceOop라는 자바 클래스의 인스턴스를 나타냄
instanceOop의 메모리 레이아웃은 모든 객체에 대해 기계어 워드 2개로 구성된 헤더로 시작
Mark 워드, Klass 워드 나옴
(klassOop는 JVM 클래스로더가 로드한 Class 객체를 JVM 수준에서 나타낸 구조체)
자바7까지 instanceOop의 Klass 워드가 자바 힙의 일부인 PermGen이라는 메모리 영역을 가리켰음, 자바 힙에 있는 건 예외없이 객체 해더를 갖고 다녀야 한다는게 기본 원칙이었고 실제로 자바 옛 버전은 메타데이터를 KlassOop으로 참조
자바8부터는 Klass가 자바 힙의 주 영역 밖으로 빠지게 됨. 그래서 최신 버전의 자바는 Klass 워드가 자바 힙 밖을 가리키므로 객체 헤더가 필요 없음
klassOop에는 클래스용 가상함수테이브(virtual function table) vtable이 있지만, Class 객체에는 리플렉션으로 호출할 Method 객체의 레퍼런스 배열이 담겨 있음
oop는 대부분 기계어 워드, 메모리 낭비를 막기 위해 압축 oop 기법 제공
-XX:+UseCompressedOops
그러면 힙에 있는 다음 oop가 압축됨
- 힙에 있는 모든 객채의 Klass 워드
- 참조형 인스턴스 필드
- 객체 배열의 각 원소
핫스팟 객체 헤더는 일반적으로 다음과 같이 구성
- Mark word
- Klass word
- 객체가 배열이면 length 워드
- 32bit 여백
객체 인스턴스 필드는 헤더 바로 다음 나열됨, klassOop는 Klass 워드 다음에 메소드 vtable이 나옴
JVM 환경에서 자바 레퍼런스는 instanceOop (또는 null)를 제외한 어떤 것도 가리킬 수 없음
자바 값은 기본형 값 또는 instanceOop 주소(레퍼런스)에 대응되는 비트 패턴
모든 자바 레퍼런스는 자바 힙의 주 영역에 있는 주소를 가리키는 포인터라고 볼 수 있음
자바 레퍼런스가 가리키는 주소에는 Mark 워드 + Klass 워드가 들어있음
klassOop와 Class<?> 인스턴스는 다르며 klassOop을 자바 변수 안에 넣을 수 없음
핫스팟의 oop 체계는 (jdk8 hotspot/src/share/vm/oops 디렉터리) .hpp 파일에 정의되어 있음
아래는 oop의 전체 상속 구조
GC 루트 및 아레나
GC 루트는 메모리의 '고정점' (=anchor point)로 메모리 풀 외부에서 내부를 가리키는 포인터
메모리 풀 내부에서 같은 메모리 풀 내부의 다른 메모리 위치를 가리키는 내부포인터와 외부 포인터
GC 루트는 다음과 같이 종류가 다양함
스택 프레임
JNI
레지스터
(JVM 코드 캐시에서) 코드 루트
전역 객체
로드된 클래스의 메타데이터
힙에 있는 객체를 가리키는 참조형 지역 변수도 말하자면 가장 단순한 형태의 GC 루트
핫스팟 GC는 Arena(무대)라는 메모리 영역에서 작동
핫스팟은 자바 힙을 관리할 때 시스템 콜을 하지 않음
할당과 수명
자바 애플리케이션에서 가비지 수집이 일어나는 주된 원인은 다음 두 가지
할당률
객체 수명
할당률은 일정 기간 (단위 MB/s) 새로 생성된 객체가 사용한 메모리 양
객체 수명은 측정이 어려움, 그렇기 때문에 객체 수명이 할당률보다 더 핵심적인 용인
약한 세대별 가설 Weak Generational Hypothesis
소프트웨어 시스템의 런타임 작용을 관찰한 결과 알게 된 경험 지식으로 JVM 메모리 관리의 이론적 근관을 형성함
"JVM 및 유사 소프트웨어 시스템에서 객체 수명은 이원적 분포 양상을 보인다. 거의 대부분의 객체는 아주 짧은 시간만 살아 있지만 나머지 객체는 기대 수명이 훨씬 길다"
→ 가비지를 수힙하는 힙은 단명 객체를 쉽고 빠르게 수집할 수 있게 설계해야 하며 장수 객체와 단명 객체를 완전히 떼어 놓는게 가장 좋다
핫스팟은 이러한 가설을 바탕으로 십분 활용함
- 객체마다 '세대 카운트'(객체가 지금까지 무사 통과한 가비지 수집 횟수)를 센다
- 큰 객체를 제외한 나머지 객체는 Eden 공간에 생성, 여기서 살아남은 객체를 다른 곳으로 옮긴다
- 충분히 오래 살아남은 객체들은 별도의 메모리 영역(old 또는 Tenured)에 보관
세대별 수집 목적에 따라 메모리를 상이한 영역으로 나누면 핫스팟의 마크 앤 스위프 수집의 구현에 따라서 그 결과가 더 세분화 됨
여기서 중요한 것은 외부에서 young 세대 내부를 가리키는 포인터를 계속 추적하는 기법
핫스팟은 card table이라는 자료 구조에 늙은 객체가 젊은 객체를 참조하는 정보를 기록
card table은 JVM이 관리하는 바이트 배열로, 각 원소는 old 세대 공간의 512 바이트 영역을 가리킴
card table logic:
늙은 객체 0에 있는 참조형 필드값이 바뀌면 0에 해당하는 instanceOop가 들어 있는 카드를 찾아 해당 엔트리를 dirty 마킹함
핫스팟은 레퍼런스 필드를 업데이트할 때마다 단순 write barrier를 이용함
필드 저장이 끝나면 결국 어단가에서 다음 코드 조각이 실행됨
card[*instanceOop >> 9] = 0;
여기서 카드에 dirty하다고 표시한 값이 0이고, 카드 테이블이 512바이트라서 9비트 우측 시프트함
자바의 수집기는 오래전부터 힙을 young/old 영역으로 나누어 관리해왔는데 자바 8u40 버전부터 새로운 수집기(G1)의 품질이 완성단계에 이름
핫스팟의 가비지 수집
스레드 로컬 할당
에덴은 대부준의 객체가 탄생하는 장송이고 단명객체는 다른 곳에는 위치할 수 없으므로 특별히 관리를 잘해야 하는 영역
JVM은 에덴을 여러 버퍼로 나누어 각 에플리케이션 스래드가 새 객체를 할당하는 구역으로 활용하도록 배포
스래드마다 새 객체를 할당하기 때문에 다른 스레드가 영역을 침범할 우려가 있음 → Thread-Local Allocation Buffer(TLAB) 설정
애플리케이션 스레드가 자신의 TLAB을 배타적으로 제어한다는 건 JVM 스레드의할당 복잡도가 O(1)
반구형 수집 hemisphere evacuating collector
보통 크기가 같은 두 공간을 사용하는 독특한 방출 수집기
장수하지 못한 객체를 임시 수용소에 담아 두자는 아이디어
덕분에 단명 객체가 테뉴어드 세대를 어지럽히지 않게 하고 full GC 발생 빈도를 줄일 수 있음
수집기가 라이브 반구를 수집할 때 객체들은 다른 반구로 압착시켜 옮기고 수집된 반구는 비워서 재사용함
절반의 공간은 항상 완전히 비움
실제로 보관 가능한 메모리 공간보다 2배를 더 사용하게 되어 낭비, 그러나 공간이 너무 크지 않다면 유용한 기법
핫스팟은 반구형 기법과 에덴 공간을 접목시켜 영 세대 수집
핫스팟은 young heap의 반구부를 survivor 공간이라고 함
survivor 공간은 에덴보다 작으며 이 공간의 역할은 각 영 세대 수집을 교환하는 것
병렬 수집기 parallel collector
자바8 이전까지 JVM 디폴트 가비지 수집기는 병렬 수집기
병렬 수집기는 처리율에 최적화되어 있고 young GC, full GC 모두 full STW를 일으킴
애플리케이션 스레드를 모두 중단시킨 다음 가용 CPU 코어를 총동원해 가능한 한 재빨리 메모리를 수집
Parallel GC
가장 간단한 young 세대용 병렬 수집기
ParNew GC
CMS 수집기와 함께 사용할 수 있게 Parallel GC를 조금 변형한 것
ParallelOld GC
old 세대용 병렬 수집기
young 세대용 병렬 수집
동작 방식
스레드가 에덴에 객체를 할당하려는데 자신이 할당받은 TLAB 공간은 부족하고 JVM은 새 TLAB를 할당할 수 없을 때 영세대 수집이 발생
영세대 수집이 일어나면 JVM은 어쩔 수 없이 전체 애플리케이션 스레드를 중단함
전체 애플리케이션 스레드가 중단되면 핫스팟은 영세대을 뒤져서 가비지 아닌 객체를 골라냄
이때 GC 루트와 오들 세대에서 출발하는 GC 루트를 식별하기 위한 카드 테이블을 병렬 마킹 스캔 작업의 출발점으로 삼음
그리고 나서, Parallel GC는 살아남은 객체를 현재 비어 있는 서바이버 공간으로 모두 방출한 후 세대 카운트를 늘려 한 차례 이동했음을 기록
마지막으로 에덴과 이제 막 객체들을 방출시킨 서바이버 공간을 재사용 가능한 빈 공간으로 표시하고 애플리케이션 스레드를 재시작해 TLAB을 애플리케이션 스레드에 배호하는 프로세스를 재개함
올드 세대 병렬 수집
ParallelOld GC는 자바8 기준으로 디폴트 올드 세대 수집기
Parllel GC와 상당히 비슷하지만 근본적인 차이점이 있음
Parllel GC는 반구형 수집기 이지만 ParallelOld GC는 하나의 연속된 메모리 공간에서 압착하는 수집기
올드 세대에 더 이상 방출할 공간이 없으면 병렬 수집기는 올드 세대 내부에서 객체들을 재비치해서 늙은 객체가 죽고 빠져 버려진 공간을 회수함
메모리 사용 면에서 아주 효율적이고 메모리 단편화가 일어날 일이 없음
병렬 수집기의 한계
병렬 수집기는 세대 전체 콘텐츠를 대상으로 한번에, 가능한 한 효율적으로 가비지를 수집함
하지만 단점 또한 존재 - full STW 유발
young GC에서는 문제가 작음, 그러나 Old 영역에서는 상황이 다름
올드 세대는 디폴트 크기 자체가 영세대의 7배
영역 내 살아 있는 객체 수만큼 마킹 시간이 늘어남 (영세대는 살아남는 객체가 작고 살아 있는 대상으로 마킹 작업이 일어남)
할당의 역할
자바의 가비지 수집 프로세스는 보통 유입된 메모리 할당 요청을 수용하기에 메모리가 부족할 때 작동하며 필요한 만큼 메모리를 공급 (예측 불가, 그때 그때 발생)
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
JAVA 버전별 history (0) 2020.05.03 JVM2 (0) 2020.04.26 JVM (0) 2020.04.26 NIO - Buffer (0) 2020.04.03 추상 클래스와 인터페이스 (0) 2020.03.29