Skip to main content

Command Palette

Search for a command to run...

HotSpot Object Header 내부 구조 — Mark Word, Compressed Class Pointer, 그리고 JEP 519 Compact Object Headers

Updated
14 min read

자바에서 new Object() 한 줄로 만들어지는 객체는 단 한 필드도 없어도 16 바이트를 차지합니다. 이 글은 그 16 바이트가 어디서 왔는지를 HotSpot JVM 소스와 JEP 문서 기준으로 분해하고, JDK 24에서 experimental, JDK 25에서 final로 승격된 Compact Object Headers(JEP 519)가 어떻게 헤더 4 바이트를 깎아내는지를 같은 그림 위에 풀어냅니다. JVM 메모리 레이아웃, GC, 동기화의 교차점을 한 번에 보고 싶은 자바 개발자를 대상으로 합니다.

도입 — 16 바이트의 출처

HotSpot에서 객체 하나의 비용을 헤아릴 때 가장 먼저 만나는 사실은 다음입니다.

  • 64-bit JVM에서 new Object()는 16 바이트를 점유합니다.
  • 필드가 0개여도 16 바이트입니다.
  • 헤더 자체는 12 바이트지만 8 바이트 정렬 padding이 붙어 16이 됩니다.

이 12 바이트의 정체가 본문 내내 풀어낼 두 조각, Mark Word(8 B)Klass Pointer(4 B) 입니다. Mark Word는 lock 상태·identity hash·GC age를 한 슬롯에 담은 정밀하게 짜인 비트맵이고, Klass Pointer는 metaspace 안의 클래스 메타데이터를 가리키는 압축된 포인터입니다. JDK 25부터 두 조각을 64 비트 한 워드로 융합하는 길이 product feature로 열렸습니다.

[Object @ 16-byte aligned address]
+---------------------------------------+ 0
| Mark Word (8 bytes)                   |
+---------------------------------------+ 8
| Klass Pointer (4 bytes, compressed)   |
+---------------------------------------+ 12
| Padding (4 bytes)                     |
+---------------------------------------+ 16
| Fields...                             |
+---------------------------------------+

1. Mark Word 비트 레이아웃

src/hotspot/share/oops/markWord.hpp는 Mark Word의 비트 의미를 객체 상태별 ASCII 그림으로 정의합니다. 64-bit 빌드의 정상(unlocked) 객체 레이아웃은 다음과 같습니다 (JDK 17 시점 기준, biased locking이 disable되기 직전 형태).

 64                     39                              7    2  1 0
 |                       |                              |    |  | |
 |  unused : 25          | hash : 31                    |unused:1|age:4|unused:1|lock:2|
필드 비트 폭 용도
lock 2 락 상태 4단계 인코딩
biased_lock 1 편향 락 활성 플래그 (JDK 15+에서 비활성, 코드는 제거됨)
age 4 GC가 사용하는 객체 생존 횟수 (Young → Old 승격 판단)
unused 1 예약
hash 31 Object.hashCode() 호출 시 계산된 identity hash
unused 25 미래 확장용

핵심은 lock 상태에 따라 상위 비트의 의미가 통째로 뒤바뀐다는 점입니다. Mark Word는 다목적 슬롯이지 단일 스키마가 아닙니다.

Lock 상태 4 가지

lock 2 비트가 가리키는 4 상태는 다음과 같이 인코딩됩니다.

 [....| age | 0 | 01]  Unlocked       (regular header — 위 표 그대로)
 [stack-ptr        |00]  Thin-locked  (mark word displaced to stack)
 [monitor-ptr      |10]  Inflated     (Object Monitor에 의해 관리)
 [forwarding-ptr   |11]  GC marked    (GC copy/forwarding 중)
  • Unlocked: 위 표 그대로. 01은 "헤더 자체가 헤더"라는 의미.
  • Thin-lock (Stack-locked): synchronized 진입 시 JVM은 현재 스레드의 스택 프레임에 BasicLock 슬롯을 잡고, 객체의 Mark Word 8 바이트를 그 슬롯에 복사한 뒤(displaced header), 객체의 Mark Word에는 스택 슬롯 주소를 써넣습니다. 락 비트가 00인 이유는 자연스러운 8-byte align 덕에 stack pointer의 하위 2비트가 항상 0이라 충돌하지 않기 때문입니다.
  • Inflated: 락 경합이 생기면 OS-level synchronization이 필요하므로 JVM은 ObjectMonitor를 별도 할당하고, Mark Word를 그 monitor 주소로 교체합니다. 락 비트는 10.
  • GC forwarding: copying GC(예: Parallel, G1 young)가 객체를 새 위치로 옮기는 동안 원래 Mark Word를 forwarding pointer로 교체. 락 비트 11.

Identity Hash가 사라지는 순간

hash : 31 31 비트는 객체별로 한 번 계산됩니다. System.identityHashCode(obj)를 처음 부른 순간 JVM은 (보통 thread-local random에서) 값을 뽑아 Mark Word에 박아넣습니다. 이후 그 객체의 동일성 해시는 변하지 않습니다.

문제는 synchronized가 동시에 작동할 때입니다. Thin-locked 상태에서는 Mark Word가 stack pointer로 덮여 있어 hash 비트가 사라집니다. 그래서 JVM은 thin lock 진입 시 원래 헤더를 displaced header로 스택에 백업해두고, hashCode가 호출되면 displaced header에서 읽어옵니다. 락이 풀릴 때 displaced header를 다시 객체 헤더에 복사합니다.

즉, "Mark Word의 의미"는 lock 비트가 가리키는 컨텍스트와 함께 해석해야 한다는 것이 HotSpot 동기화 설계의 핵심 가정입니다.

2. Klass Pointer와 Compressed Class Pointers

Mark Word 다음 4 바이트는 객체의 Klass Pointer, 즉 metaspace에 있는 Klass 구조체를 가리키는 포인터입니다. KlassObject.getClass()가 반환하는 Class<?>의 native side, 즉 vtable·field layout·인터페이스 정보를 모두 들고 있는 메타데이터입니다.

64-bit 시스템에서 raw pointer라면 8 바이트여야 정상이지만, HotSpot은 기본적으로 -XX:+UseCompressedClassPointers를 켜서 32 비트로 압축합니다. 이를 위해 JVM은 Compressed Class Space라는 특별한 metaspace 영역을 따로 할당합니다.

Compressed Class Space

                    ┌────────────────────────────┐
 narrow klass ──→   │ Compressed Class Space     │ ← klass 메타데이터 전용
 (32 bits)          │   (default 1 GB, max 3 GB) │
                    │                            │
                    │ Klass[A]                   │
                    │ Klass[B]                   │
                    │ ...                        │
                    └────────────────────────────┘
                                │
                                └─→ rest of Metaspace (constant pool, methods, ...)
  • -XX:CompressedClassSpaceSize 기본 1 GiB, HotSpot이 인위적으로 3 GiB로 상한.
  • Klass 구조체만 이 영역에 모여 있어서 32-bit narrow pointer로 충분히 도달합니다.
  • 디코딩 식은 klass = narrow_klass_base + (narrow << shift). 보통 shift = 0이면 그대로 32 비트로 1 GiB까지 직지정합니다.

UseCompressedClassPointersUseCompressedOops가 켜져 있을 때만 의미가 있고, 둘은 짝으로 동작합니다. UseCompressedOops를 끄면 UseCompressedClassPointers도 자동으로 꺼집니다.

Compressed Oops와의 비교

Compressed Oops는 Java heap 안의 객체 참조(필드, 배열 원소)를 32 비트로 압축하는 별개 기능입니다. 다음 두 가지로 구분해야 헷갈리지 않습니다.

기능 대상 압축 식 한계
Compressed Oops Java heap의 객체 참조 addr = base + (oop << 3) (alignment shift) 8-byte alignment에서 32 GiB 힙
Compressed Class Pointers 객체 헤더의 Klass 포인터 klass = base + (narrow << shift) (shift는 보통 0) Compressed Class Space 3 GiB

-XX:ObjectAlignmentInBytes=16으로 정렬을 늘리면 Compressed Oops가 64 GiB까지 닿지만, alignment padding이 늘어 헤더 외에도 객체 한 개당 평균 8 바이트 손해를 봅니다.

3. 배열 헤더는 4 바이트가 더 붙는다

배열도 같은 Mark Word + Klass Pointer 구조를 갖되, 그 뒤에 length 4 바이트가 추가됩니다.

[Array @ aligned address]
+---------------------------------------+ 0
| Mark Word (8 bytes)                   |
+---------------------------------------+ 8
| Klass Pointer (4 bytes, compressed)   |
+---------------------------------------+ 12
| Array Length (4 bytes)                |
+---------------------------------------+ 16
| Elements...                           |
+---------------------------------------+

길이가 0인 new int[0]이라도 16 바이트를 차지합니다. int[]이라면 원소 한 개가 4 바이트이므로 new int[1]은 16 (헤더) + 4 (원소) + 4 (padding) = 24 바이트입니다.

Object.length 같은 필드가 없는데 길이가 어디 저장되느냐는 물음의 답이 이 자리입니다. 자바 바이트코드 arraylength 명령은 객체 주소 + 12에서 4 바이트를 직접 읽습니다.

4. 객체 크기를 JOL로 확인하기

OpenJDK가 제공하는 JOL (Java Object Layout) 도구로 위의 모든 비트를 실측할 수 있습니다.

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.17</version>
</dependency>
import org.openjdk.jol.info.ClassLayout;

public class JolDemo {
    static class Empty {}
    static class WithFields {
        int i;
        long l;
        String s;
    }

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseClass(Empty.class).toPrintable());
        System.out.println(ClassLayout.parseClass(WithFields.class).toPrintable());
    }
}

JDK 21 + Compressed Oops 환경의 출력은 다음과 같은 형태입니다.

JolDemo$Empty object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                  VALUE
      0     4        (object header: mark)        0x0000000000000001
      8     4        (object header: class)       0x000ac1f8
     12     4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

(object alignment gap) 4 bytes external — 12 → 16으로 끌어올린 padding이 명시적으로 잡힙니다. 이 4 바이트가 바로 JEP 519가 노리는 자리입니다.

WithFields도 같이 분석하면 다음과 같습니다.

JolDemo$WithFields object internals:
 OFFSET  SIZE                TYPE DESCRIPTION                  VALUE
      0     4                     (object header: mark)        0x0000000000000001
      8     4                     (object header: class)       0x000ac218
     12     4                 int WithFields.i                  0
     16     8                long WithFields.l                  0
     24     4   java.lang.String  WithFields.s                  null
     28     4                     (object alignment gap)
Instance size: 32 bytes

JVM은 필드를 선언 순서가 아니라 사이즈 내림차순으로 재배치합니다. long(8) → int(4) → reference(4) 순서로 모이면 padding이 28 바이트에 한 번만 들어갑니다. 만약 선언 순서대로 i(4), l(8), s(4)를 그대로 배치하면 i 뒤에 4 바이트 padding을 넣어 l을 8-byte align해야 하므로 총 40 바이트가 됩니다. HotSpot은 이 재배치를 자동으로 해서 객체 크기를 최소화합니다.

객체 크기 표

자주 보이는 자료구조의 64-bit JVM + Compressed Oops 환경 크기는 다음과 같습니다.

자료 크기 메모
new Object() 16 B header 12 + padding 4
new Integer(42) 16 B header 12 + int 4
new Long(42L) 24 B header 12 + padding 4 + long 8
new int[0] 16 B header 12 + length 4
new int[1] 24 B header 16 + int 4 + padding 4
new String("") (JDK 9+) 24 B header 12 + byte[] ref 4 + int 4 + byte 1 + padding 3
new HashMap.Node 32 B header 12 + hash 4 + key 4 + value 4 + next 4 + padding 4

JEP 519가 모든 줄에서 4 바이트씩 깎아내는 그림이 보입니다. HashMap 100만 노드라면 4 MB 절감입니다.

5. JEP 374 — Biased Locking 폐기와 비트 한 자리

Biased locking은 2006년경 HotSpot이 도입한 최적화로, 단일 스레드가 반복적으로 같은 객체에 락을 거는 경우 CAS조차 생략하려는 시도였습니다. Mark Word의 biased_lock : 1 비트와 thread id 23 비트로 "이 객체는 이 스레드에 편향됨" 상태를 표시했습니다.

JEP 374: Deprecate and Disable Biased Locking은 JDK 15에서 다음과 같이 결론을 내렸습니다.

  • 현대 CPU에서 CAS 비용이 충분히 낮아져 biased locking의 이득이 사라졌습니다.
  • 반면 코드 복잡도와 safepoint 의존성(revocation safepoint)이 너무 컸습니다.
  • j.u.concurrent(ReentrantLock, AtomicXxx 등) 사용 비중이 늘어 synchronized 자체의 핫 패스 빈도가 감소했습니다.

따라서 JDK 15부터 UseBiasedLocking이 기본 false가 되었고, 이후 릴리스에서 관련 코드가 점진적으로 제거되어 현재 OpenJDK mastermarkWord.hpp에는 biased_lock 비트 자리가 self_fwd 같은 다른 용도로 재배치되었습니다.

이 한 비트는 사소해 보이지만, JEP 519가 헤더 자체를 통째로 압축할 때 "lock 인코딩에 쓸 비트를 얼마나 줄일 수 있는가"의 핵심 자원입니다. Biased locking이 살아 있었다면 Compact Object Headers는 비트 가계부 자체가 성립하지 않았을 가능성이 큽니다.

6. JEP 450 / JEP 519 — Compact Object Headers

Project Lilliput의 일환으로 출발한 Compact Object Headers는 헤더 12 바이트를 8 바이트로 줄이는 작업입니다. 64-bit Mark Word와 32-bit Klass Pointer를 단일 64-bit 헤더로 합치되, 합쳐도 Mark Word의 모든 기능(락 인코딩, identity hash, GC age, forwarding)을 보존해야 합니다.

타임라인:

  • JEP 450 (JDK 24, 2025-03): Compact Object Headers를 experimental feature로 도입. -XX:+UseCompactObjectHeaders + -XX:+UnlockExperimentalVMOptions 조합 필요.
  • JEP 519 (JDK 25, 2025-09): Final feature로 승격. experimental 플래그 없이도 -XX:+UseCompactObjectHeaders로 켤 수 있되 기본값은 여전히 off.
  • JEP draft (8361187): 향후 default on으로 전환 제안.

압축 후 헤더 비트 배치

JDK 25 final 형태의 64-bit 단일 헤더는 다음과 같이 배치됩니다.

 63                  41                              10  6  4 2 1 0
 |                    |                              |   |  | | | |
 | Compressed Klass : 22 | Hash : 31 | unused : 4 | age : 4 | self-fwd : 1 | lock : 2 |
  • Compressed Klass: 22 bits — 4M(약 4백만) 개의 고유 클래스까지 표현 가능. 일반 애플리케이션의 클래스 수와 비교해 충분한 여유.
  • Hash: 31 bits — identity hashCode가 그대로 보존됩니다.
  • age: 4 bits — GC tenuring age 그대로.
  • self-fwd: 1 bit — GC forwarding을 위한 자기 자신 표시. 기존에 lock 비트 11이 했던 forwarding 인코딩이 이 비트로 분리됩니다.
  • lock: 2 bits — 락 상태 인코딩.

기존 12 바이트 헤더에 비해:

  • Mark Word의 상위 25 비트 unused가 사라짐 → Klass pointer가 그 자리를 차지.
  • 별도 4 바이트 Klass Pointer 필드 제거 → 객체당 4 바이트 절약.

락 메커니즘 변경

전통적인 thin lock은 객체의 Mark Word를 stack pointer로 덮어썼고, 원본 헤더를 displaced header로 스택에 백업했습니다. Compact Object Headers에서는 이 방식이 불가능합니다. 헤더 안에 Klass 비트가 함께 들어 있으므로 헤더를 덮으면 객체의 타입 정보까지 사라지기 때문입니다.

해결책은 Lightweight Locking with Lock Stack이었습니다. JDK 21쯤부터 점진적으로 도입된 이 모델은:

  • 각 스레드가 자기 스택 외에 별도의 lock stack을 들고 있습니다.
  • synchronized 진입 시 객체를 lock stack에 푸시할 뿐, 객체 헤더는 손대지 않습니다.
  • 락 비트는 그대로 01(unlocked)이지만, 다른 스레드가 락을 잡으려고 보면 lock stack을 검사해 owner를 식별합니다.

Compact Object Headers는 이 lock stack 위에서 비로소 성립합니다. JEP 450의 사전 조건이었던 8291555: Implement alternative fast-locking scheme 작업이 바로 이 변경입니다.

flowchart LR
    A[synchronized obj entry] --> B{Lock state}
    B -->|01 unlocked| C[Push obj to thread lock stack]
    C --> D[Continue without header write]
    B -->|01 owned by current thread| E[Increment lock stack count]
    B -->|01 owned by other thread| F[Inflate to ObjectMonitor]
    F --> G[CAS header to monitor ptr 10]
    G --> H[OS-level wait/notify path]

전통적인 stack-locked 모델에서는 동기화 진입 한 번마다 객체 헤더 8 바이트를 읽고 CAS로 쓰는 비용이 들었습니다. Lock stack 모델에서는 자기 스레드의 thread-local 스택에 push만 하면 되므로 cache contention이 사라집니다. 부수적인 효과로 동일 객체에 대한 재귀 락도 thread-local 카운터 증가만으로 처리됩니다.

GC forwarding 인코딩 변경

기존 GC는 forwarding pointer로 객체 헤더 전체를 덮었습니다(lock = 11). Compact 헤더에서는 그렇게 하면 Klass 비트가 사라져 GC가 객체 타입을 모르게 됩니다.

JEP 450/519의 해법은 self-forwarding 비트입니다. GC가 객체를 새 위치로 옮긴 뒤, 원래 객체의 헤더에는:

  • self-fwd 비트를 켜고
  • 별도 side table에 new location을 저장하거나,
  • 압축된 형태로 hash/age 영역의 일부 비트를 forwarding offset으로 재활용

하는 식으로 동작합니다. 결과적으로 Klass 비트는 항상 살아 있습니다.

측정된 효과

워크로드 측정 결과
SPECjbb2015 힙 22% 절감, 처리량 8% 향상
Amazon 프로덕션 (수백 개 서비스) CPU 사용량 최대 30% 감소
평균 객체 크기 작은 워크로드 메모리 10~20% 절감

작은 객체가 많을수록 효과가 크다는 점은 직관적입니다. 헤더 4 바이트는 평균 객체 크기에서 차지하는 비중이 30~50%인 경우도 있어서, 절대값보다 상대 절감률이 큽니다. 반대로 큰 배열·문자열 위주 워크로드에서는 효과가 미미합니다.

활성화 방법

# JDK 24 (experimental)
java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders ...

# JDK 25 (final, 기본 off)
java -XX:+UseCompactObjectHeaders ...

조건:

  • UseCompressedClassPointers 자동 활성 (강제).
  • UseCompressedOops도 강제 활성.
  • ZGC, G1, Parallel, Serial 모두 지원. Shenandoah도 지원.

7. 전체 흐름도

flowchart TD
    A[new Object call] --> B[Allocator]
    B --> C{Compact Headers?}
    C -->|No| D[12-byte header: 8B mark + 4B klass]
    C -->|Yes| E[8-byte header: combined mark+klass]
    D --> F[8-byte alignment padding]
    E --> G[No padding needed if no fields]
    F --> H[Total 16 bytes for empty object]
    G --> I[Total 8 or 16 bytes]
    H --> J[Field layout]
    I --> J
    J --> K[Object aligned to 8 or 16 bytes]

8. 운영 함정 6 가지

  1. identityHashCode 호출 후 락 효율 저하 — 한 번이라도 System.identityHashCode(obj)가 호출되면 Mark Word에 hash가 박힙니다. 이후 thin lock이 displaced header를 백업할 때 hash까지 옮겨야 하므로 비용이 살짝 늘어납니다. 핫 코드에서 임의 객체에 hashCode 호출을 남발하면 잠재적 회귀가 됩니다.

  2. ObjectAlignmentInBytes 무분별 증가-XX:ObjectAlignmentInBytes=16으로 늘리면 Compressed Oops가 64 GiB까지 닿지만, 객체 한 개당 평균 4~8 바이트 padding이 추가됩니다. 작은 객체 위주 워크로드에서 Compact Object Headers의 이득이 통째로 상쇄될 수 있습니다.

  3. Compressed Class Space OOM — 클래스가 너무 많은 애플리케이션(예: Hibernate dynamic proxy 폭주, Groovy 메타클래스 캐시 누수)에서 1 GiB Compressed Class Space 한계에 부딪힙니다. -XX:CompressedClassSpaceSize=2g 같은 조정이 필요할 수 있고, 그래도 부족하면 -XX:-UseCompressedClassPointers로 클래스 포인터 압축을 끄게 됩니다(이 경우 헤더가 16 바이트로 늘어남).

  4. JOL이 보고하는 "external loss" — JOL의 "external loss"는 인접한 객체 사이의 alignment gap을 별도 객체로 의제화한 표현입니다. 단일 객체 내부의 padding은 "internal"로 표시되니 둘을 혼동하면 안 됩니다.

  5. GC 알고리즘별 self-forwarding 호환성 — Compact Object Headers는 JDK 25 기준 모든 표준 GC와 호환되지만, 일부 디버깅 옵션(-Xlog:gc+phases=trace 등)에서 출력 포맷이 바뀝니다. 운영 로그 파서를 두고 있다면 회귀 점검이 필요합니다.

  6. VarHandle/Unsafe 직접 접근 코드 — Mark Word 자체에 의존하는 라이브러리(주로 lock-free 자료구조 일부)는 Compact Object Headers에서 비트 위치가 바뀌므로 호환되지 않을 수 있습니다. Netty의 일부 fast-path, JCTools 큐, Caffeine 일부 캐시 정책이 점검 대상입니다. 대부분의 메인스트림 라이브러리는 이미 패치를 받았습니다.

9. 그래서 언제 켜야 하는가

  • 평균 객체 크기가 작은(필드 1~3개) POJO 위주 서비스 — Spring Bean, JPA Entity, DTO 다수, JSON DOM 트리 등 — 에서 효과 가장 큼.
  • 데이터 처리 파이프라인 — Kafka Streams, Flink 등에서 record 객체 millions per second 흐름. CPU/메모리 모두 이득.
  • GC pause 민감 서비스 — 힙이 작아지면 GC 작업량 자체가 줄어 pause time이 개선됩니다.
  • 반대로 큰 배열/버퍼 위주 워크로드 — Netty buffer pool, Lucene index — 에서는 이득이 미미합니다.

JDK 25 production 환경이라면 카나리아 환경에서 -XX:+UseCompactObjectHeaders를 켜고 평소 베이스라인과 비교하는 것이 안전한 시작점입니다.

마무리

Mark Word 8 바이트와 Klass Pointer 4 바이트는 자바 객체 한 개의 12 바이트 헤더입니다. 그 안에는 lock 상태 4단계, identity hash 31 비트, GC age 4 비트, 그리고 클래스 메타데이터를 가리키는 압축 포인터 32 비트가 한 치 양보 없이 끼어 있습니다. Biased locking이 떠나면서 자리 한 칸이 생겼고, Project Lilliput은 그 자리에서 시작해 헤더 자체를 8 바이트로 압축해냈습니다. JEP 519는 이 작업의 최종 단계입니다.

객체 헤더는 자바 메모리·GC·동기화가 한 점에서 만나는 자리입니다. 한 번 분해해두면 GC 튜닝, 동기화 회피 패턴, 메모리 추정 모두에서 같은 모형을 재사용하게 됩니다.

참고자료

More from this blog

JVM은 컨테이너의 CPU와 메모리 한계를 어떻게 알아낼까

8코어 노드에 컨테이너를 띄웠는데 ForkJoinPool이 스레드를 한두 개만 만들어요. 메모리는 넉넉히 줬는데 컨테이너가 자꾸 OOMKilled로 죽고요. 분명히 같은 JAR인데 로컬에서는 멀쩡하다가 쿠버네티스에만 올리면 이상해져요. 이 글은 "왜 컨테이너 속 JVM은 다르게 행동하는가"를 cgroup이라는 진짜 경계선과, JVM이 그 경계를 읽어내는 내

May 21, 202615 min read

ThreadPoolExecutor는 언제 스레드를 새로 만들까 — execute()의 3단계

Executors.newFixedThreadPool(10) 한 줄을 쓰면서도, 11번째 작업이 오면 스레드가 11개로 늘어날 거라고 막연히 기대해 본 적 없으신가요. 실제로는 큐가 먼저 무한히 쌓이고 스레드는 영원히 10개에 머물러요. 이 글은 ThreadPoolExecutor가 작업을 받았을 때 "스레드를 새로 만들지, 큐에 넣을지, 거부할지"를 결정하는

May 21, 202617 min read

자바 synchronized는 어떻게 동작할까 — 모니터, 락 인플레이션, 그리고 사라진 biased locking

synchronized 키워드 하나로 스레드 안전을 얻는 동안, JVM 안에서는 객체 헤더의 비트를 뒤집고, 스택에 락 레코드를 쌓고, 경합이 생기면 네이티브 모니터로 승격하는 일이 벌어져요. 이 글은 그 한 번의 잠금이 객체 헤더부터 ObjectMonitor까지 어떤 경로를 거치는지, 그리고 한때 있었다가 JDK 18에서 사라진 biased locking

May 19, 202616 min read

JVM 객체 할당의 비밀 — TLAB, Bump-the-Pointer, 그리고 할당이 거의 공짜인 이유

Java에서 new를 호출하면 무슨 일이 벌어질까요? "힙에 메모리를 잡는다"는 한 문장 뒤에는 스레드마다 자기만의 분양 구역을 나눠 갖는 정교한 설계가 숨어 있어요. 이 글은 HotSpot JVM이 객체 할당을 어떻게 "거의 공짜"로 만드는지 그 내부를 따라가 보려는 글이에요. JVM 메모리 동작 원리에 관심 있는 분께 권해요. 자바를 쓰다 보면 객체를

May 15, 202614 min read

Java Zero-Copy — FileChannel.transferTo, sendfile, 그리고 Kafka가 디스크를 네트워크로 흘려보내는 방법

"파일을 읽어서 소켓으로 보낸다." 한 줄짜리 요구사항이에요. 그런데 이 한 줄 뒤에서 데이터는 메모리를 네 번이나 복사하고, CPU는 커널과 유저 공간을 네 번이나 들락거려요. Kafka처럼 초당 수십만 건을 흘려보내야 하는 시스템에서 이 비용은 그냥 넘길 수가 없어요. 이 글은 그 복사를 한 겹씩 벗겨내는 zero-copy의 동작 원리를 따라가요. 전통

May 15, 202617 min read

끄적끄적 테크 블로그

165 posts

물류 회사에 다니고 있는 개발자 블로그입니다. 개발을 너무 좋아해서 정신없이 작업하다가 중간에 끄적거리며 내용들을 몇개 적어봅니다 ㅎㅎ