JVM 객체 헤더의 Lock flag와 Java Memory Model 작업 메모리까지 한 번에 정리하기

JV-CO-03calendar_today2026-02-03 23:40#Java #Level2

====## 객체 메모리 레이아웃 복습

  • Java 객체는 메모리에서 크게 Object Header, Instance Data, Padding으로 나뉘어 배치됩니다. :contentReference[oaicite:0]{index=0}
  • Object Header는 보통 Mark Word와 **Klass Pointer(Klass Word)**로 구성됩니다. :contentReference[oaicite:1]{index=1}
  • 배열(Array)은 여기에 length 같은 추가 필드가 더 붙습니다.

Object Header 구성

  • Mark Word에는 실행 중 변하는 메타 정보가 들어갑니다. :contentReference[oaicite:2]{index=2}
    • Identity Hash Code가 들어갈 수 있습니다.
    • GC용 정보가 들어갈 수 있습니다.
    • Lock 관련 상태 비트가 들어갑니다. :contentReference[oaicite:3]{index=3}
  • **Klass Pointer(Klass Word)**에는 “이 객체가 어떤 클래스의 인스턴스인지”를 가리키는 정보가 들어갑니다. :contentReference[oaicite:4]{index=4}
    • HotSpot에서는 이 포인터를 통해 메타스페이스의 클래스 메타데이터로 연결됩니다.

Lock flag는 무엇을 해결하려고 있나

  • Lock flag는 “객체에 접근하려면 반드시 락을 잡아야 한다”는 의미는 아닙니다.
  • 일반적인 필드 읽기와 쓰기는 락 없이도 수행됩니다.
  • Lock flag는 주로 synchronized(모니터 락) 같은 동기화가 걸릴 때 경쟁 조건을 제어하려고 존재합니다.

Lock flag가 중요한 이유

  • 동기화는 대체로 “공유 데이터”에서 경쟁 조건이 생길 때 필요합니다.
  • HotSpot은 동기화 상태를 **객체 헤더(Mark Word)**에 기록해서 빠르게 판단합니다. :contentReference[oaicite:5]{index=5}
  • 즉 “락의 상태”를 객체가 들고 있는 구조입니다.

HotSpot에서 Lock 상태는 어떻게 변하나

  • HotSpot은 객체 헤더의 태그 비트를 바꿔가며 락 상태를 표현합니다. :contentReference[oaicite:6]{index=6}
  • 대표적인 흐름은 다음처럼 이해하면 실무에서 충분합니다.
  • Unlocked
    • 기본 상태입니다.
    • 태그 비트가 01로 표현됩니다(Compact Object Headers 설명 기준). :contentReference[oaicite:7]{index=7}
  • Lightweight Lock(Thin Lock)
    • 경쟁이 심하지 않을 때 빠르게 잡는 경량 락입니다.
    • 태그 비트가 00으로 표현됩니다. :contentReference[oaicite:8]{index=8}
    • 이 단계에서 짧게 **스핀(바쁜 대기)**을 할 수 있습니다(구현과 상황에 따라 다릅니다).
  • Monitor Lock(Inflated Lock)
    • 경쟁이 커지거나 wait/notify 같은 조건이 붙으면 모니터 구조로 “팽창”합니다.
    • 태그 비트가 10으로 표현됩니다. :contentReference[oaicite:9]{index=9}

메모에 있던 “00이면 스핀락”은 어떻게 보정하면 좋나

  • 00은 “스핀락 그 자체”라기보다 경량 락 상태를 의미한다고 보는 편이 안전합니다. :contentReference[oaicite:10]{index=10}
  • 스핀은 경량 락 단계에서 “경쟁이 짧을 것”이라고 예상될 때 사용하는 전략 중 하나입니다.
  • 즉 **상태 표현(00)**과 **대기 전략(스핀/블로킹)**은 분리해서 이해하는 게 정확합니다.

인스턴스 락과 static 락의 차이

  • 인스턴스 메서드에 synchronized를 붙이면 **this(인스턴스)**의 모니터를 잡습니다.
  • static 메서드에 synchronized를 붙이면 해당 클래스의 Class 객체 모니터를 잡습니다. :contentReference[oaicite:11]{index=11}

“static은 객체가 없어서 통제 불가”는 어떻게 봐야 하나

  • static 멤버는 인스턴스 없이 존재하는 것은 맞습니다.
  • 하지만 동기화 대상이 “없다”기보다는, 락을 거는 기준이 인스턴스가 아니라 Class 객체로 바뀝니다. :contentReference[oaicite:12]{index=12}
  • 즉 static도 락을 걸 객체가 있다고 이해하면 정리가 깔끔합니다.

예시 코드

java
class Counter {
    private int x = 0;
    private static int y = 0;
    public synchronized void incX() {
        x++;
    }
    public static synchronized void incY() {
        y++;
    }
}
  • incX()는 각 인스턴스별로 락이 다릅니다.- incY()는 클래스 하나당 하나 락이라서 인스턴스가 여러 개여도 같은 락을 씁니다.

JVM과 별개인 CPU 캐시 일관성

메모 구조 메모의 사실 관계 보정

  • Register와 캐시(L1/L2/L3)는 CPU 내부 계층으로 보는 게 일반적입니다.
  • “L1~L3는 OS 명령어로만 사용 가능”이라고 단정하면 오해가 생깁니다(하드웨어가 자동 관리합니다).
    • 개발자는 직접 주소로 “캐시를 읽는 API”를 쓰기보다, **지역성(locality)**을 고려해 캐시 효율을 유도합니다.
    • 필요하면 컴파일러 배리어, 원자적 명령, 메모리 펜스 같은 형태로 간접 제어가 일어납니다(언어와 플랫폼별로 다릅니다).
  • “User mode 앱은 RAM 접근 불가”도 표현상 오해 소지가 큽니다.
    • 유저 모드는 가상 주소 공간을 통해 메모리에 접근합니다.
    • 접근이 제한되는 것은 “물리 주소를 마음대로 만지는 행위”에 가깝습니다(보호와 격리 목적입니다).
  • “RAM+SSD/HDD를 논리적 가상 메모리로 추상화”는 방향은 맞습니다.
    • 다만 일반적으로 가상 메모리는 “주소 변환 + 페이지 관리”이며, SSD/HDD는 스왑/페이지 아웃으로 연결됩니다(구현은 OS마다 다릅니다).

캐시 일관성과 Java는 진짜 상관이 없나

  • “캐시 일관성(coherence)” 자체는 주로 하드웨어 프로토콜(MESI 등)로 유지됩니다.
  • 하지만 Java도 완전히 무관하진 않습니다.
  • Java의 volatilesynchronized, *원자 클래스(Atomic)**는 결국 CPU의 원자적 명령과 메모리 배리어로 매핑되어 가시성/순서 보장을 만듭니다(매핑은 JVM과 플랫폼이 담당합니다).
  • 즉 “일관성은 하드웨어가 해결”이 맞지만, “그 하드웨어 기능을 어떻게 써서 스레드 안전을 만들지”는 JMM이 규칙으로 정의합니다.

JVM 메인 메모리와 작업 메모리 JMM 핵심

  • JMM(Java Memory Model)의 핵심 목표는 스레드가 변수에 접근할 때의 규칙을 정의하는 것입니다.
  • 변수는 결국 “메모리를 읽고 쓰는 문법적 추상화”라는 관점이 도움이 됩니다.

메인 메모리와 작업 메모리 모델

  • JMM은 추상적으로 Main Memory와 **Working Memory(스레드별)**를 둡니다.
  • Working Memory는 스레드마다 따로 있다고 가정합니다.
  • Working Memory는 CPU 캐시와 “비슷한 느낌”으로 비유할 수 있지만, 1:1로 동일한 실체라고 보면 위험합니다(추상 모델입니다).

제일 중요한 규칙

  • 모든 공유 변수는 메인 메모리에 존재한다고 모델링합니다.
  • 각 스레드는 메인 메모리 값을 작업 메모리로 복사해서 쓰는 것으로 모델링합니다.
  • 스레드 내부 연산은 작업 메모리의 사본에 반영된다고 봅니다.
  • 그래서 동기화 없이는 “다른 스레드가 최신 값을 못 볼 수 있다”가 자연스럽게 발생합니다.

JMM 동기화 프로토콜 Read Load Store Write

  • JMM에는 메인 메모리와 작업 메모리 사이 전송을 설명하는 동작들이 있습니다.
  • 핵심 4가지는 아래처럼 외우면 구현 디버깅 때 도움이 됩니다.
  • Read
    • 메인 메모리에서 값을 읽어 “전송”합니다.
  • Load
    • 전송된 값을 작업 메모리(사본)에 적재합니다.
  • Store
    • 작업 메모리 값을 메인 메모리로 “전송”합니다.
  • Write
    • 전송된 값을 메인 메모리의 변수에 반영합니다.

왜 모든 접근에 동기화를 안 하나

  • 모든 변수 접근마다 Read/Load/Store/Write를 강제하면 성능이 무너집니다.
  • 그래서 JMM은 “필요한 경우에만” 강한 동기화가 일어나도록 장치를 제공합니다.
    • synchronized는 락 경계를 기준으로 가시성과 순서를 강하게 만듭니다.
    • volatile은 변수 단위로 가시성과 순서를 강하게 만듭니다.
    • java.util.concurrent는 더 고수준으로 안전한 패턴을 제공합니다.

실전 관점에서 한 줄 결론

  • 객체 헤더의 **Lock flag(Mark Word)**는 synchronized를 빠르게 처리하기 위한 런타임 메타데이터입니다.
  • static 동기화는 “없던 객체에 락을 거는 것”이 아니라 Class 객체에 락을 거는 것입니다.
  • JMM의 작업 메모리 모델을 떠올리면 “왜 volatile/synchronized가 필요한지”가 직관적으로 정리됩니다.

참고로 알아두면 좋은 변화

  • Biased Locking은 JDK 15부터 기본 비활성화되었고, 이후 제거된 흐름으로 알려져 있습니다(버전/배포판에 따라 체감이 달라질 수 있습니다).
  • 그래서 최신 JVM에서는 synchronized의 시작점이 thin lock 중심으로 설명되는 자료가 많습니다.