brunch

You can make anything
by writing

C.S.Lewis

by myner Nov 25. 2019

JVM Internal

나를 다시 붙잡는 심정으로, 마음을 다잡는 심정으로 - 브런치 3

부제에서도 보이지만 당분간 쓸 글은 '나를 다시 붙잡는 심정으로, 마음을 다잡는 심정으로' 시리즈가 될 듯싶다. 기본을 돌아보고 싶을때, 마음을 다잡고 싶을때 가볍게 보는 그런 기술 브런치를 작성해 나가려고 한다.


인트로


자바 바이트코드가 JRE 위에서 동작하고 JRE는 자바 API와 JVM으로 구성되며, JVM의 역할은 자바 애플리케이션을 클래스 로더(Class Loader)를 통해 읽어 들여서 자바 API와 함께 실행하는 것


JVM


스택기반의 가상 머신 

심볼릭 레퍼런스: 프리미티브 타입을 제외한 모든 타입(클래스와 인터페이스)을 명시적인 메모리 주소 기반의 레퍼런스가 아니라 심볼릭 레퍼런스를 통해 참조

GC

기본 자료형을 명확하게 정의하여 플랫폼 독립성 보장

네트워크 바이트 오더: 빅 엔디안 


자바 바이트코드


자바 바이트코드에서 메서드를 호출하는 명령어 OpCode는 invokeinterface, invokespecial, invokestatic, invokevirtual, invokedynamic의 5가지가 있으며 각각의 의미는 다음과 같다.


invokeinterface: 인터페이스 메서드 호출

invokespecial: 생성자, private 메서드, 슈퍼 클래스의 메서드 호출

invokestatic: static 메서드 호출

invokevirtual: 인스턴스 메서드 호출

invokedynamic: 동적 타입 언어를 위한 opcode, Jruby, Jython, Groovy 같은 JVM에서 돌아가는 동적 타입언어를 지원하기 위해 추가. Java8 부터 default method, lambda compile시에 사용


invokedynamic


Java8에서는 두가지 목표를 전부 달성하기 위하여 아래와 같은 전략을 취합니다. compile시에 bytecode에서는 람다를 구현하는 객체를 생성하지 않음 -> runtime에 실제 생성을 위함하는 방법만(recipe)만 표기

해당 recipe는 invokedynamic instruction에 동적/정적 인수 목록으로 encoding 된다.


1. invokedynamic이 호출되면 bootstrap영역의 lambdafactory.metafactory()를 수행

    - lambdafactory.metafactory(): java runtime library의 표준화 method

    - 어떤 방법으로 객체를 생성할지 dynamically 결정: 클래스 생성, 재사용, proxy, wrapper class, VM전용 API사용등등 성능 향상을 위한 최적화된 방법 사용


2. java.lang.invoke.CallSite 객체를 return함.

    - 해당 lambda의 lambda factory

    - MethodHandle을 멤버변수로 가짐

    - 람다가 변환되는 함수 인터페이스의 인스턴스를 반환

    - 한번만 생성되고 재 호출시 재 사용함. 


클래스 파일 포맷


한 메서드의 크기가 65535바이트를 넘을 수 없다는 JVM 명세 자체의 제한 


자바 바이트코드에서 일반적으로 사용하는 branch/jump 명령은 "goto"와 "jsr" 두 가지이다.


goto [branchbyte1] [branchbyte2]  

jsr [branchbyte1] [branchbyte2]


이들은 모두 2바이트의 signed branch offset을 피연산자로 받으므로 최대 65535번 인덱스까지 이동할 수 있다. 그러나, 자바 바이트코드는 좀 더 여유 있는 branch를 지원하기 위해 4바이트의 signed branch offset을 받는 "goto_w"와 "jsr_w"를 이미 준비하고 있다.


goto_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4]  

jsr_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4]  


즉, 이들을 이용하면 65535 이상의 인덱스로도 branch 가능하므로, 자바 메서드의 65535바이트 제한을 넘을 수 있을 것 같다. 그러나, 자바 클래스 파일 포맷의 다른 여러 제한 때문에 여전히 자바 메서드는 65535바이트를 넘을 수 없다.


자바 클래스 파일의 큰 골격은 다음과 같다.


ClassFile {  

u4 magic;  

u2 minor_version;  

u2 major_version;  

u2 constant_pool_count;  

cp_info constant_pool[constant_pool_count-1];  

u2 access_flags;  

u2 this_class;  

u2 super_class;  

u2 interfaces_count;  

u2 interfaces[interfaces_count];  

u2 fields_count;  

field_info fields[fields_count];  

u2 methods_count;  

method_info methods[methods_count];  

u2 attributes_count;  

attribute_info attributes[attributes_count];  

}


메서드 크기 65535바이트 제한은 여기서 method_info 구조체의 내용과 관련이 있다. method_info 구조체에는 Code, LineNumberTable, LocalVariableTable attribute가 있다. LineNumberTable의 길이에 해당하는 값, LocalVariableTable의 길이에 해당하는 값, Code attribute에 포함된 exception_table의 길이에 해당하는 값은 모두 2바이트로 고정되어 있다. 


따라서 메서드의 크기는 LineNumberTable, LocalVariableTable, exception_table의 길이를 넘지 못하기 때문에 65535바이트로 제한되는 것이다.


JVM 구조


이미지 참조: https://d2.naver.com/helloworld/1230
이미지 참조: https://sehun-kim.github.io/sehun/JVM/

클래스로더


자바는 동적 로드, 즉 컴파일타임이 아니라 런타임에 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크하는 특징이 있다.

계층구조

위임모델

가시성 제한

언로드 불가


클래스로더 위임모델



이미지 참조: https://d2.naver.com/helloworld/1230

부트스트랩 클래스 로더: JVM을 기동할 때 생성되며, Object 클래스들을 비롯하여 자바 API들을 로드한다. 다른 클래스 로더와 달리 자바가 아니라 네이티브 코드로 구현되어 있다.

익스텐션 클래스 로더(Extension Class Loader): 기본 자바 API를 제외한 확장 클래스들을 로드한다. 다양한 보안 확장 기능 등을 여기에서 로드하게 된다.

시스템 클래스 로더(System Class Loader): 부트스트랩 클래스 로더와 익스텐션 클래스 로더가 JVM 자체의 구성 요소들을 로드하는 것이라 한다면, 시스템 클래스 로더는 애플리케이션의 클래스들을 로드한다고 할 수 있다. 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드한다.

사용자 정의 클래스 로더(User-Defined Class Loader): 애플리케이션 사용자가 직접 코드 상에서 생성해서 사용하는 클래스 로더이다.


클래스 로드 단계


이미지 참조: https://d2.naver.com/helloworld/1230

로드: 클래스를 파일에서 가져와서 JVM의 메모리에 로드한다.

검증(Verifying): 읽어 들인 클래스가 자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사한다. 

준비(Preparing): 클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조를 준비한다.

분석(Resolving): 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.

초기화: 클래스 변수들을 적절한 값으로 초기화한다. 즉, static initializer들을 수행하고, static 필드들을 설정된 값으로 초기화한다.


런타임 데이터 영역


이미지 참조: https://d2.naver.com/helloworld/1230


메서드 영역: 메서드 영역은 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다. JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, Static 변수, 메서드의 바이트코드 등을 보관한다. 메서드 영역은 JVM 벤더마다 다양한 형태로 구현할 수 있으며, 오라클 핫스팟 JVM(HotSpot JVM)에서는 흔히 Permanent Area, 혹은 Permanent Generation(PermGen)이라고 불린다. 메서드 영역에 대한 가비지 컬렉션은 JVM 벤더의 선택 사항이다. 

- 이 영역에 있는 클래스 정보로 힙 영역에 객체를 생성


런타임 상수 풀: 클래스 파일 포맷에서 constant_pool 테이블에 해당하는 영역이다. 각 클래스와 인터페이스의 상수뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다. 즉, 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아서 참조한다.


자바8에서 JVM메모리 모델 변경사항


- PermGen영역이 없어지고 MetaSapce공간으로 변경

- 별도의 사이즈를 지정하지 않으면 J

VM이 동적으로 알아서 조절 

- PermGen 관련 JVM옵션(ex: -XX: PermSize, -XX: MaxPermSize)는 무시됨


MetaSapce


- 네이티브 힙 메모리의 일부

- XX를 사용하여 조정가능 MetaspaceSize 및 -XX: MaxMetaSpacesize

- java.lang.OutOfMemoryError: 네이티브 공간이 고갈되면 메타 데이터 공간이 수신 

- dead class와 classloader의 GC는 MaxMetaspaceSize에 도달하면 Metaspace garbage collection 발생합니다.

- Metaspace의 tuning과 monitoring은 GC의 횟수와 지역을 제한하기 위해 필요한 작업입니다. 과도한 Metaspace GC는 classloader의 memory leak 또는 적합하지 않은 Application의 memory size를 초래합니다.

이미지 참조: https://equj65.net/tech/java8hotspot/


PermGen영역에 저장되던 것들은 어디로? 

- Method of a class -> Native 영역으로 

- Names of the Class -> Native 영역으로

- Constant pool Information(String 포함) -> Heap 영역으로 

- Static Variable -> Heap영역으로 



실행 엔진


실행 엔진은 자바 바이트코드를 명령어 단위로 읽어서 실행한다. CPU가 기계 명령어을 하나씩 실행하는 것과 비슷하다. 바이트코드의 각 명령어는 1바이트짜리 OpCode와 추가 피연산자로 이루어져 있으며, 실행 엔진은 하나의 OpCode를 가져와서 피연산자와 함께 작업을 수행한 다음, 다음 OpCode를 수행하는 식으로 동작


인터프리터: 바이트코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하나씩 해석하고 실행하기 때문에 바이트코드 하나하나의 해석은 빠른 대신 인터프리팅 결과의 실행은 느리다는 단점을 가지고 있다. 바이트코드라는 '언어'는 기본적으로 인터프리터 방식으로 동작한다.


JIT(Just-In-Time) 컴파일러: 인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고, 이후에는 해당 메서드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행하는 방식이다.

이미지 참조: https://d2.naver.com/helloworld/1230

AOT(Ahead-Of-Time) 컴파일러: 한번 컴파일된 네이티브 코드를 여러 JVM이 공유 캐시를 통해 공유해서 사용하는 것을 의미한다. 즉, AOT 컴파일러를 통해 이미 컴파일된 코드는 다른 JVM에서도 컴파일하지 않고 사용할 수 있게 하는 것이다. 








출처 

https://d2.naver.com/helloworld/1230

https://tourspace.tistory.com/12

https://atin.tistory.com/625

https://brunch.co.kr/@heracul/1

https://sehun-kim.github.io/sehun/JVM/

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari