JVM Performance Engineering: Inside OpenJDK and the HotSpot Java Virtual Machine - Chapter2

김경환·2024년 5월 20일

Java 시스템 진화

  • 자바는 정적 타입검사 언어

    • 컴파일시 타입 확인
  • 자바는 강타입언어

    • 타입이 안맞을시 컴파일 에러

    하지만 버전이 넘어가면서 여러가지 타입에 대한 기능 제공

    J2SE 5.0 이전

    primitve Type: int, short, long, byte, float, double boolean, char (+String)
    reference type: interface/class/array
    -> array 가 있다..!

    to JAVA SE 8

enum

Annotations

  • predefined: @SuppressWarnings, @Override, @Deprecated
  • SE7: @SafeVarargs
  • SE8: @FunctionalInterface
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_PARAMETER)
public @interface Pizza {
}

SE8: 'target type' inference

In the context of lambda expressions, target typing refers to the ability of the compiler to infer the type of a lambda expression from its target context.

Map<String, Map<String, String>> mapOfMapsInferred = new HashMap<>();

List<Allergens> sortedAllergens = Arrays.stream(Allergens.values())
    .sorted(Comparator.comparing(Allergens::getSeason))
    .collect(Collectors.toList());

이 예제에서는 람다 식을 Allergens::getSeason lambda 식과 동일한 메서드 참조입니다. allergen -> allergen.getSeason(). 컴파일러는 대상 유형을 사용합니다. Comparator 람다 식의 유형을 추론합니다.

JAVA SE7 SE8

  • '_' 숫자 리터럴
    100_000
  • int unsigned 화
  • interface: static method + default method

JAVA 9, 10

VarHandles

잠금없이 변수를 Atomic 하게 제어할 때 사용

  • 안정성:  유효한 메모리 바운더리안에서 사용
  • 무결정: final 필드 값을 업데이트 할 수 없음
  • 성능: sun.misc.Unsafe보다 성능이 비슷하거나 좋음
  • 사용성: sun.misc.Unsafe API, java.util.concurrent.atomic API보다 사용하기 좋음

AtomicInteger 와 같은 Atomic 객체 대신 상용

모드

  • getVolatile: 데이터 읽기
  • serVolatile: 데이터 쓰기
  • 읽기-수정-쓰기 : compareAndExchange, getAndAddRelease, getAndBitwiseXorRelease
  // Declaration and Initialization Example
class SafePin {
    int pin;
}

class ModifySafePin {
    // Declare a static final VarHandle for the "pin" field
    private static final VarHandle VH_PIN;

    static {
         try {
            // Initialize the VarHandle by finding it for the "pin" field of SafePin class
            VH_PIN = MethodHandles.lookup().findVarHandle(SafePin.class, "pin", int.class);
         } catch (Exception e) {
            // Throw an error if VarHandle initialization fails
            throw new Error(e);
         }
    }
}
  
  // Atomic Update Example
class ModifySafePin {
    private static final VarHandle VH_PIN = ModifySafePin.getVarHandle();

    private static VarHandle getVarHandle() {
          try {
              // Find and return the VarHandle for the "pin" field of SafePin class
              return MethodHandles.lookup().findVarHandle(SafePin.class, "pin", int.class);
          } catch (Exception e) {
              // Throw an error if VarHandle initialization fails
              throw new Error(e);
          }
    }
    public static void atomicUpdatePin(SafePin safePin, int newValue) {
         int prevValue;
         do {
              prevValue = (int) VH_PIN.getVolatile(safePin);
         } while (!VH_PIN.compareAndSet(safePin, prevValue, newValue));
    }
}

계속해서 atomic 하게 업데이트

ref: https://docs.oracle.com/javase/9/docs/api/java/lang/invoke/MethodHandles.html

JAVA 11 to JAVA 17

switch (JDK12)

  
  public enum Day {
   MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

public String getDayName(Day day) {
   return switch (day) {
      case MONDAY -> "Monday";
      case TUESDAY -> "Tuesday";
      case WEDNESDAY, THURSDAY -> {
         String s = day == Day.WEDNESDAY ? "Midweek" : "Almost Weekend";
         yield s; // yield is used to return a value from a block of code
      }
      // Other cases …
      default -> throw new IllegalArgumentException("Invalid day: " + day);
   };
}

yeild 와 -> arrow statement

Sealed Class (JDK 17)

sealed abstract class Pet permits Mammal, Reptile {}

sealed abstract class Mammal extends Pet permits Cat, Dog, PygmyPossum {
   abstract boolean isAdult();
}

final class Cat extends Mammal {
    boolean isAdult() {
      return true;
    }
    // Cat-specific properties and methods
}

// Similar implementations for Dog and PygmyPossum

sealed abstract class Reptile extends Pet permits Snake, BeardedDragon {
    abstract boolean isHarmless();
}

final class Snake extends Reptile {
    boolean isHarmless() {
        return true;
    }
    // Snake-specific properties and methods
}


// Similar implementation for BeardedDragon

sealed abstract class 의 permits 를 미리 지정 (약간 귀찮...?)

record class (JDK 17)

kotlin data class 와 같음

public record Pet (String name, int age, String breed) {}
----
List<Pet> pets = List.of(
    new Pet("Ruby", 2, "Great Pyrenees and Red Heeler Mix", true),
    new Pet("Bash", 1, "Maltese Mix", false),
    new Pet("Perl", 10, "Black Lab Mix", true)
);

List<Pet> adultPets = pets.stream()
    .filter(Pet::isAdult)
    .collect(Collectors.toList());

adultPets.forEach(pet -> System.out.println(pet.name() + " is an adult " + pet.breed()));

Beyond Java 17: Project Valhalla

Problem

  • primitive type 과 reference type(object) 구분
  • primitive type 은 identiy 가 없고 순수 데이터 값
  • JVM 는 class/array 와 같은 aggregate type 에만 ID 할당 추적 Identity 는 비용이 수반됨. 헤더와 바디가 필요

###Java Object Layout(JOL)을 이용한 Object Memory Layout 분석

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
import static java.lang.System.out;

public class ArrayLayout {
    public static void main(String[] args) throws Exception {
        out.println(VM.current().details());
        byte[] ba = new byte[8];
        out.println(ClassLayout.parseInstance(ba).toPrintable());
    }
}
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
[B object internals:
OFFSET SIZE TYPE DESCRIPTION     VALUE
   0     4       (object header) 01 00 00 00 (00000001 …) (1)
   4     4       (object header) 00 00 00 00 (00000000 …) (0)
   8     4       (object header) 48 68 00 00 (01001000 …) (26696)
  12     4       (object header) 08 00 00 00 (00001000 …) (8)
  16     8 byte  [B.<elements>          N/A
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

차례차례 분석

# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
  • 64비트 Java VM 에 있고, compressed 되어있음. (kclass 또한 compressed 되어있음)
    (OOP 에는 instanceOOP 와 Klass 로 존재)
  • Mark Word: 인스턴스의 메타데이터를 가리키는 포인터
  • Klass 는 클래스 메타 데이터를 가르키는 포인터
  [B object internals:
OFFSET SIZE TYPE DESCRIPTION     VALUE
   0     4       (object header) 01 00 00 00 (00000001 …) (1)
   4     4       (object header) 00 00 00 00 (00000000 …) (0)
   8     4       (object header) 48 68 00 00 (01001000 …) (26696)
  12     4       (object header) 08 00 00 00 (00001000 …) (8)
  16     8 byte  [B.<elements>          N/A
  • 위에 2줄 OOP 헤더
  • 그 아래 1줄 Klass
  • 그 아래 araay길이
  • 그아래 Batye 요소

복잡한 객체

public class MorningPeopleArray {
    public static void main(String[] args) throws Exception {
        out.println(VM.current().details());

        // Create an array of MorningPeople objects
        MorningPeople[] mornpplarray = new MorningPeople[8];

        // Print the layout of the MorningPeople class
        out.println(ClassLayout.parseClass(MorningPeople.class).toPrintable());

        // Print the layout of the mornpplarray instance
        out.println(ClassLayout.parseInstance(mornpplarray).toPrintable());
    }
}
class MorningPeople {
    String name;
    Boolean type;


    public MorningPeople(String name, Boolean type) {
        this.name = name;
        this.type = type;
    }
}
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

MorningPeople object internals:
 OFFSET  SIZE   TYPE               DESCRIPTION                VALUE
      0    12                      (object header)            N/A
     12     4   java.lang.String   MorningPeople.name         N/A
     16     4   java.lang.Boolean  MorningPeople.type         N/A
     20     4                      (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[LMorningPeople; object internals:
 OFFSET  SIZE   TYPE              DESCRIPTION               VALUE
      0     4                     (object header)           (1)
      4     4                     (object header)           (0)
      8     4                     (object header)           (13389376)
     12     4                     (object header)           (8)
     16    32   MorningPeople     MorningPeople;.<elements> N/A
Instance size: 48 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

문제점

불변객체

// An immutable class by declaring it final
final class MorningPeople {
    private final String name;
    private final Boolean type;

    public MorningPeople(String name, Boolean type) {
        this.name = name;
        this.type = type;
    }
    // Getter methods for name and type
    public String getName() {
         return name;
    }
    public Boolean getType() {
        return type;
    }
}
  • 불변객체를 JOL 로 출력해보면 일반객체와 같음
  • MorningPeople 메모리 독립객체, 그리고 불변
  • 이 참조는 힙 전체에 흩어져있고, 캐시활용도가 떨어짐 (사실 무슨 이야기인지..)

Value Class

  • 특정한 경우 개체 ID의 필요성 X
  • GC 작업량을 가볍게, 그리고 GC 사이클을 좀더 효율적으로

객체헤더를 제거, primitive type 과 유사한 인라인 스토리지

  • 메모리 사용량 감소 : 인라인 스토리지는 필요한 메모리 사용량 감소
  • 인라인 스토리지는 값 클래스의 모든 필드가 메모리에서 서로 가깝도록 보장. 참조지역성 확보로 CPU 캐시 효율 증가
  • primitive, reference, nested value class 필드를 지원해서 다용도로 사용 가능

primitive 를 지원한 generic 정의

원시유형도 제네릭 지원에 포함

public class FreshmenAdmissions<K, any V> {
    K key;
    V boolornumvalue;

    public void admissionInformation(K name, V value) {
        key = name;
        boolornumvalue = value;
    }
}

any V 는 primitive type 도 지원하는 generic 이 됨

현재 상태

java 에 value class, primitive class, generic 향상

  • value class 는 == 을 사용하는 경우 ID 가 아닌 콘텐츠 동일성 확인 (값 클래스는 id 가 없음)
  • primitive class 는 null 일 수 없음 (참조 유형 x, 값을 할당하지 않으면 0)
    • 별도의 힙 할당이 필요하지 않고 메모리에 직접 저장. primitive class array 는 힙에서 연속성을 가짐
    • primitive class <-> value class 는 box/unbox 지원 (https://openjdk.org/jeps/401)

이제 primitive type 을 다른유형하고 다르게 취급할 필요없기 때문에 type 시스템이 우아해짐

성능

Project Valhalla는 가치 클래스와 프리미티브 클래스를 메모리에 직접 저장할 수 있도록 하고 제네릭을 개선하여 이러한 유형과 더 잘 작동하도록 함으로써 메모리 사용량을 줄이고 Java 애플리케이션의 성능을 향상시키는 것을 목표

JEP 401: Value Class and Objects 는 얼리 억세스로 릴리즈됨 (EA)

  • value class implementatinos
  • .ref 기본클래스의 참조유형

사용 사례

  • 대량의 불변 객체를 처리하는 금융 시뮬레이션은 메모리 효율성과 가치 클래스의 속도를 활용하여 보다 효율적인 처리를 수행
  • 향상된 제네릭을 통해 API 를 우아하고 쉽게 구현
  • value class 로 thread safe 한 환경 구성

다른 언어랑 비교

  • c# 의 value type 과 비슷하게 성능 최적화
    • C# 과는 다른점은 메서드/인터페이스에 대한 향상된 기능
    • 참조유형으로 사용될떄 boxing 성능 비용 작면
  • kotlin data class 는 record 와 유사하여 일반적인 방법을 자동화
  • scala case class 는 불변 구조, 그리고 패턴배칭 지원
profile
이것저것하는 잡부

0개의 댓글