J-C-03 JVM 객체 헤더의 Lock flag와 Java Memory Model 작업 메모리까지 한 번에 정리하기
2026-02-03 |
글 정보
- 카테고리
- Programming/Java/Core
- 태그
- JavaLevel2
객체 메모리 레이아웃 복습
- 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도 락을 걸 객체가 있다고 이해하면 정리가 깔끔합니다.
예시 코드
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의 volatile, synchronized, _원자 클래스(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 중심으로 설명되는 자료가 많습니다.