-XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation
MDO?
바이트코드 인터프리터와 C1 컴파일러에서 JIT 컴파일러가 언제, 무슨 최적화를 할지 결정하는데 필요한 정보를 기록하는 것
컴파일러 최적화 기법
C1, C2 컴파일러
// 인라이닝 전
int result = add(a, b);
private int add(int x, int y) {
return x + y;
}
// 인라이닝 후
int result = a + b;
다음과 같은 오버헤드를 줄일 수 있다
다른 최적화(탈출 분석, 죽은 코드 제거, 루프 펼치기, 락 생략) 의 범위를 확장시키는 역할을 한다
아래 항목을 따져보며 어떤 메서드를 인라이닝 할 지 결정한다
JIT Watch를 통해서도 인라이닝 거부된 메서드를 확인할 수 있다.
-XX:MaxInlineSize=<n>: 메서드를 이 크기 이하로 인라이닝 한다-XX:FreqInlineSize=<n>: 자주 호출되는 메서드를 이 크기 이하로 인라이닝 한다-XX:InlineSmallCode=<n>-XX:MaxInlineLevel=<n>: 이 수준 보다 더 깊이 호출프레임을 인라이닝 하지 않는다중요메서드가 최대 허용 크기를 살짝 초과해 인라이닝 되지 않는 경우, 위의 스위치로 JVM 매개변수를 조정할 수 있다(-XX:MaxInlineSize, -XX:FreqInlineSize)
컴파일러는 매번 순회할 때마다 루프 처음으로 되돌아가는 횟수를 줄이기 위해 루프를 펼칠 수 있다
루프 처음으로 돌아가는 작업을 백 브랜치 라고 하며, 이 비용은 높다
➡️ 따라서 루프 바디가 짧으면 백 브랜치 비용은 상대적으로 높다
따라서 핫스팟은 아래 기준에 따라 루프 펼치기 여부를 결정한다
루프 펼치기는 핫스팟 버전별로 로직이 상이하고, 아키텍처마다 많이 다르다
탈출할 객체의 세가지 유형
hotspot/src/share/vm/opto/escape.hppNoEscape 예시
for (int i = 0; i < 1_000_000; i++){
MyObj foo = new MyObj(i); // foo 메서드 탈출 안함
sum += foo.bar();
}
ArgEscape 예시
for (int i = 0; i < 1_000_000; i++){
MyObj foo = new MyObj(i);
sum += extBar(foo); // foo 는 메서드의 인수로 전달됨
}
GlobalEscape 예시
(by chatgpt)
class MyGlobal {
static MyObj globalObj = null; // 전역 변수
public static void setGlobalObj(MyObj obj) {
globalObj = obj; // 전달받은 객체를 전역 변수에 저장
}
}
public class EscapeExample {
public static void main(String[] args) {
long sum = 0;
for (int i = 0; i < 1_000_000; i++) {
MyObj foo = new MyObj(i);
MyGlobal.setGlobalObj(foo); // foo 객체가 Global Escape
sum += foo.bar();
}
}
}
NoEscape라면, 스칼라 치환 이라는 최적화를 적용해 지역변수였던 것 처럼 스칼라 값으로 바꾼다class Animal {
void speak() {
System.out.println("Some generic noise");
}
}
class Dog extends Animal {
@Override
void speak() {
System.out.println("Bark");
}
}
class Cat extends Animal {
@Override
void speak() {
System.out.println("Meow");
}
}
public void makeItSpeak(Animal animal) {
animal.speak();
}
요런 코드가 있다고 하자.
makeItSpeak 메서드를 호출한다고 하면, Input에는 어떤게 들어올까?
Animal 타입의 구체적인 객체인 Dog or Cat이 들어올 것 이다
(➡️ 각 호출부 마다 딱 한가지 런타임 타입이 수신자 객체 타입이 된다)
즉, 어떤 객체에 있는 메서드를 호출할 때, 그 메서드를 최초로 호출한 객체의 런타임 타입(Dog or Cat)을 알아내면 그 이후 모든 호출도 동일한 타입일 가능성이 크다
그렇다면 해당 호출부 메서드 호출을 최적화할 수 있다
vtable에서 메서드를 찾을 필요가 없고, 항상 타입이 같다면 호출 대상을 계산하여 invokevirtual 명령어를 가드 후 컴파일드 메서드 바디로 분기하는 코드로 치환할 수 있다
좀 더 자세하게!💡
객체의 메서드 호출 시, JVM은 객체의 가상 메서드 테이블인 vtable을 조회하여 실제 실행할 메서드를 찾는다.
이 과정은 invokevirtual JVM 명령어를 통해 이루어 진다.
invokevirtual는 객체의 런타임 타입을 확인하고, 해당 타입의 메서드 구현을 vtable에서 찾는다.
하지만 동일한 메서드를 반복적으로 호출하고, 호출되는 객체의 타입이 변경되지 않는다면?
➡️ 매번 vtable 을 조회하지 않고 호출될 메서드의 주소를 캐시해두고 캐시된 주소를 사용해 메서드를 직접 호출할 수 있다
.ad(architect dependent 인 파일이 인트린직 템플릿이다@HotSpotIntrinsicCandidate annotation을 붙여 인트린직을 사용할 수 있다자주 사용되는 인트린직
java.lang.System.arraycopy(): CPU 벡터 지원 기능으로 배열을 빨리 복사한다java.lang.System.currentTimeMillis(): 대부분 OS가 제공하는 구현체가 빠르다java.lang.Math.min(): 일부 CPU에서 분기 없이 연산이 가능하다main())세이브포인트(Safepoint)
- 정의: 세이브포인트는 JVM이 안전하게 시스템 작업을 수행할 수 있는 지점을 의미합니다. 이 지점에서는 모든 스레드가 일정한 상태에 있으며, 가비지 컬렉션과 같은 작업을 안전하게 수행할 수 있습니다. 즉, 스레드가 실행 중인 코드의 특정 지점에서만 가비지 컬렉터나 다른 시스템 작업이 실행될 수 있도록 합니다.
- 용도: 세이브포인트는 가비지 컬렉션, 코드의 핫스팟 컴파일, 스레드 덤프 생성, 클래스 리로딩 등 다양한 시스템 작업을 안전하게 수행하기 위해 사용됩니다.
세이브 포인트에 걸리는 경우
메서드를 역최적화
힙 덤프를 생성
바이어스 락을 취소
클래스 재정의
세이브 포인트 체크(세이브포인트에 도달했는지 확인하는 과정) 발급은 JIT 컴파일러가 담당한다
핫스팟에서는 다음 지점에 세이브포인트 체크 코드를 넣는다
메서드를 작게 하면 좋은점