[JVM] JVM 밑바닥까지 파헤치기 독서 #2 - 자바는 어떻게 객체를 저장하는가?

2025. 11. 27. 08:07·CS/JVM

https://dev-dx2d2y-log.tistory.com/138

 

[CS] JVM 밑바닥까지 파헤치기 독서 #1 - 자바 런타임 메모리 영역

C와 C++은 객체 (C에는 객체가 없지만)의 생성, 관리, 삭제까지 모두 관리할 책임을 가지지만, 자바에서는 JVM이 대체적으로 이 역할을 수행한다. JVM에 대해서 배우면서 가장 먼저 자바 메모리 영역

dev-dx2d2y-log.tistory.com

저번에는 JVM의 메모리구조와 각 메모리구조마다 어떤 데이터가 저장되는지에 대해 알아봤다.

이번에는 객체가 생성되고 제거되며 어떤 방식으로 메모리에 접근하는지 등에 대해 알아보고자한다.

 

문제 시 삭제

이번 블로그에서도 중요한 도식

앞으로 JVM의 기초를 다루는 모든 글마다 아마 저 그림을 맨 위로 올리지 않을까 싶다.

 

또한 여기 정보들은 유기적으로 다른 장, 절과 연결되어있는데, 주기적으로 업데이트를 통해 그 장과 절을 읽으면 그 게시글의 링크를 걸어둘 예정이다. 발화 블로그를 통해서 합본으로 내보내도 되고.


객체 생성

객체를 생성하는 방법은 보통 new 연산자를 통해 생성하면 된다. 과연 어떠한 과정으로 객체가 생성되는가? 메모리에 대해서 다룰 때 런타임 데이터 영역 중 메서드 영역에서 간략하게 다룬 적이 있었으나 이번에는 좀 더 다뤄보고자한다.


Encounter

JVM이 컴파일 후 보내진 바이트코드를 읽다가 new 연산자를 만나면 이 클래스가 런타임 상수 풀 안의 클래스를 가리키는지 확인한다. 저번 메모리 게시글에서 심벌참조가 생성되는 경우를 설명하지 않았는데, 심벌참조는 컴파일과정에서 생성된다.

이 '클래스를 가리키는지 확인'은 결국 new 연산자를 통해 생성하려는 클래스가 컴파일과정에서 생성된 심벌참조에 있는지 확인하는 과정이다.

 

그리고나서 심벌참조에 해당하는 클래스가 로딩, 해석, 초기화되어있는지 확인한다.

 

로딩

로딩은 7장에서 자세히 다루기때문에 간략히 설명하자면 클래스를 읽어서 JVM이 원하는 형태로 변환한 후에 메서드영역에 올리는 과정이다. 즉, 가져올 클래스의 상세한 정보를 클래스를 읽어와서 메모리에 저장하는 과정이다.

 

클래스가 로딩되고나면 객체 저장에 필요한 메모리 크기를 알 수 있는데 (계산법도 뒤에서 나온다. 곧 나온다.) 로딩 후에는 메모리 공간할당을 한다. 과거 자바 관련 서적을 읽다보면 객체 인스턴스 변수를 선언만하고 생성하지 않으면 메모리 공간만 할당한다고 했는데, 아마 선언까지만하면 여기까지 일어나는 듯싶다.

 

할당

메모리 공간 할당은 힙메모리 영역에서 진행한다. 힙메모리가 연속적이라면 여유 메모리의 시작점에 포인터를 위치시키고 필요한 메모리만큼 포인터를 '밀쳐서' 여유 공간을 확보할 수 있으나, 불행히도 힙메모리는 불규칙적이다. 그대신 가상머신은 가용 메모리 블럭들을 따로 관리하고, 거기서 객체 인스턴스를 저장하기 위한 메모리를 할당한다.

 

다만 자바에서는 객체를 생성하는 일이 아주 빈번하게 일어나고, 멀티 스레딩 환경에서는 더더욱이 빈번하게 일어난다. 또한 여유 메모리에 접근하는 것도 스레드 안전하지 않다. 이를 방지하기 위해서는 할당을 동기화해 한 번에 하나의 스레드만 접근할 수 있게하거나, 아니면 스레드 각각이 힙 메모리에 작은 전용메모리를 할당받는 것이다. 후자를 스레드 로컬 할당 버퍼(TLAB)이라한다.

 

스레드는 로컬 버퍼에서 메모리를 할당받아 사용하다가 버퍼가 부족해지면 또 새로운 메모리를 할당받아 사용하는 식으로 동작한다. 새로운 메모리를 할당하는 과정을 동기화된 과정이다.

 

초기화

할당이 끝났으면 가상머신은, 헤더를 제외하고 (헤더는 나중에 나온다. 곧.) 할당받은 공간을 0으로 초기화한다. TLAB을 사용했다면 TLAB 공간이 할당되기 전에 미리 수행된다. 이것 덕분에 자바에서 모든 인스턴스 변수들은 별다른 초기화 없이 사용할 수 있게 되는 것이다.

 

이 과정 이후에는 객체마다 필요한 설정을 진행한다. 상속정보나 메타 정보를 찾는 방법 메서드 영역에 저장된 클래스 정보를 읽어온다. GC 세대 나이 (GC는 3장에서 다룬다.)등.. 이런 정보들이 객체에 헤더에 저장된다. 

 

여기까지 클래스를 읽어와 메서드 영역에 올리고, 클래스 정보를 토대로 메모리 공간을 할당하고, 그 메모리 공간을 헤더를 제외하고 모두 0으로 바꾸고, 헤더에 여러 메타데이터를 설정하여 자바 가상 머신의 객체 생성은 종료되었다.

 

그러나 자바프로그램에서는 '이제부터가 시작이다.' 그래서 new 연산자 뒤에 생성자 메서드를 전해주어 개발자의 의도대로 초기화해야한다. 자바 명령어가 바이트코드로 전환될 때 new 연산자는 new (생성 및 메모리 할당) 과 invokespecial (초기화)로 변환하는데, 이를 통해 개발자의 의도대로 초기화를 진행한다.

 

책 71쪽에 간단한 핫스팟 인터프리터의 코드가 나와있는데, 나는 C++을 읽을 줄 모르기 때문에 대충 한 번 읽고만 넘어가려한다.


객체 메모리 레이아웃

핫스팟 가상 머신(오라클에 의해 관리되는 대부분의 JVM 구현체)에서는 객체를 세 부분으로 나눠서 힙에 저장한다.


헤더

헤더에는 두 유형의 정보가 들어간다.

첫 번째는 객체 자체의 실시간 데이터다. 해시코드, GC 세대의 나이, 락(스레드의 그 락) 정보, 편향된 스레드 아이디, 편향된 시각의 타임스탬프 등, 전반적인 객체에 대한 정보가 들어간다.

 

편향된 스레드 아이디는 특정 스레드가 해당 객체의 락을 사용해야한다면, 매번 동기화연산을 진행할 것이 아니라 특정 스레드에서 자유롭게 접근이 가능하도록, 그러니까 특정한 스레드에 락을 미리 걸어주는 것과 비슷하다. 이 '락'은 스레드가 락 상태를 해제하거나 스레드가 종료되는 경우에는 초기화되어야하는데, 이런 경우에는 객체 내부에 타임스탬프 값을 변경하여 해당 편행된 스레드 아이디가 더이상 유효하지 않다는 역할을 수행한다. (객체 내부 타임스탬프 값을 변경하여 기존 클래스에 저장된 타임스탬프 값과 차이나도록)

 

이 편향된 스레드아이디, 타임스탬프는 편향 락 관련된 사항이기에 JKD18부터는 해당되지 않는다.

 

 

아무튼 이러한 부분들을 마크워드라고 칭하며, 32비트 가상머신에서는 32비트, 64비트 가상머신에서는 64만큼의 크기를 가진다.

32비트 가상머신에서는 저장공간 중 25비트는 해시코드를 저장하고, 4개의 비트는 객체의 세대나이, 1개의 비트는 0으로 고정, 나머지 2개의 비트는 락 플래그를 정의한다.

64비트 가상머신에서는 31개의 비트를 해시코드에 저장하고, 나머지는 32비트와 동일하다. 25개의 비트는 미사용 비트다

 

위에서 헤더부분을 제외하고는 모두 0으로 초기화된다고 했는데, 헤더 부분에서도 해시코드 부분은 0으로 초기화되었다가 hashCode()가 처음 호출될 때 할당된다.

 

 

마크 워드 다음으로는 클래스워드가 온다. 저번에 메모리에 대해 다룰 때 나왔던, 런타임 상수 풀에 저장되는, 메서드 영역에 있는 클래스의 정보를 나타내는 klass 포인터가 이곳에 들어간다. 이를 통해 이 객체가 어느 클래스의 인스턴스인지 확인할 수 있다.

 

모든 가상 머신이 객체 헤더에 클래스 포인터를 저장하지 않아도된다고한다. 자세한 것은 다음장에서 다룬다.

 

배열의 경우에는 배열길이가 같이 헤더에 저장된다. 힙메모리에 공간을 확보하기 위해서는 메모리의 크기를 계산해야하는데, 배열의 경우에는 헤더에 원소의 타입만 저장된다. 따라서 제대로된 메모리 확보를 위해서는 배열길이까지 알아야 제대로 공간을 확보할 수 있다.


인스턴스 데이터

객체가 실제로 담고 있는 정보를 저장한다. 이를테면 필드 관련 내용, 부모 클래스가 있는지 없는지, 부모 클래스에서 상속받은 필드 등이 이 부분에 들어간다. 저장순서는 가상 머신마다 다른데, 핫스팟 가상 머신은 원시타입 → 일반 객체의 포인터 순으로 할당한다.

 

원시타입은 long·double → int → short·char → byte·boolean 순으로, 길이가 같은 필드들은 항상 할당과 저장이 동시에 일어난다. 또한 필드 길이가 같을 때는 부모클래스에서 정의된 필드가 자식클래스의 필드보다 앞에 저장된다.

 

그래서 원시형 변수는 '값을 저장한다'고 참조형변수는 '참조값을 저장한다'가 나오게된 것이다. 객체가 힙메모리에 저장될 때 객체의 인스턴스 데이터에서 원시타입은 그대로 저장되고, 참조형 변수는 참조값을 저장하기 때문이다.


정렬 패딩

이 부분은 딱히 큰 의미가 없고, 존재하지 않을 수도 있다.

자바에서는 메모리 효율을 위해서 객체의 크기를 8바이트의 배수가 되도록 해야하는데, 헤더는 반드시 32비트 또는 64비트이므로 이 조건을 충족하나 인스턴스 데이터 때문에 객체가 8바이트의 배수가 되지 못할 수도 있다. 이 경우에만 정렬 패딩을 사용하여 객체의 크기를 8바이트의 배수로 맞추고 넘어간다.


이렇게 객체의 생성과 힙메모리에 저장하는 방식에 대해서 알아봤다.

 

1. JVM이 클래스 A를 처음 로드할 때, 해당 클래스가 참조하는 클래스 B는 심벌참조로 참조하게 된다. 심벌참조는 런타임 상수 풀에 저장된다.

 

2. 이후 클래스 A의 인스턴스 객체 A를 실행시키면서, 클래스 B의 인스턴스를 실행시켜야하는 상황이 생길 때, JVM은 클래스 B가 로딩되어있는지 확인하고 로딩되어있지 않은 경우에는 1번의 과정으로 로딩한다.

 

3. 클래스가 로드되고 나서 JVM은 클래스 B의 인스턴스 객체 B의 레이아웃을 결정한다. 레이아웃에는 헤더, 인스턴스 데이터, 정렬패팅으로 이루어져있으며, 헤더에는 클래스의 상세한 정보와 해당 클래스가 메서드 영역에 어디에 속하는지를 나타내는 포인터가 들어있으며, 인스턴스 데이터에는 객체 B의 필드정보가, 정렬패딩을 통해서 8바이트의 배수가 되도록 B의 크기를 조정한다. 중요한 점은 이 때는 인스턴스 필드의 초기화는 이루어지지 않고, 단지 인스턴스 필드에 어느 요소들이 있는지만 확인한다.

 

4. 객체 B의 레이아웃을 통해 메모리의 크기를 알 수 있으면 JVM은 힙메모리에서 객체 B만큼의 메모리 크기를 확보하고, 헤더 부분을 제외하고 값을 0으로 초기화시킨다. 그리고나서 init() 메서드를 통하여 객체의 초기화를 진행한다.

 

다소 복잡하긴하지만, 위의 과정을 거쳐서 최종적으로는 힙메모리에 데이터가 저장된다.


원래는 객체의 사용까지하려고 했는데, 시간상 여기까지만

내용은 좀 어려워도 재미는 있다. 읽기 잘 한 것 같다.

 

다만 발화에서는 좀 더 이해하기 쉽게 다듬어야할 것이다.

'CS > JVM' 카테고리의 다른 글

[JVM] GC의 객체회수과정은 어떻게 일어나는가? - 마크-스윕, 마크-카피, 마크-컴팩트  (0) 2025.12.26
[JVM] 참조 카운팅 알고리즘과 파이썬의 순환 검출 알고리즘에 대해서  (0) 2025.12.25
[JVM] JVM 밑바닥까지 파헤치기 독서 #4 - 메모리 영역 별 오버플로우  (1) 2025.12.06
[JVM] JVM 밑바닥까지 파헤치기 독서 #3 - 자바는 어떻게 객체를 불러오는가?  (0) 2025.12.05
[JVM] JVM 밑바닥까지 파헤치기 독서 #1 - 자바 런타임 메모리 영역  (0) 2025.11.26
'CS/JVM' 카테고리의 다른 글
  • [JVM] 참조 카운팅 알고리즘과 파이썬의 순환 검출 알고리즘에 대해서
  • [JVM] JVM 밑바닥까지 파헤치기 독서 #4 - 메모리 영역 별 오버플로우
  • [JVM] JVM 밑바닥까지 파헤치기 독서 #3 - 자바는 어떻게 객체를 불러오는가?
  • [JVM] JVM 밑바닥까지 파헤치기 독서 #1 - 자바 런타임 메모리 영역
Radiata
Radiata
개발을 합니다.
  • Radiata
    DDD
    Radiata
  • 전체
    오늘
    어제
    • 분류 전체보기 (211)
      • 신년사 (3)
        • 2025년 (2)
        • 2026년 (1)
      • CS (59)
        • JVM (12)
        • 백엔드 (20)
        • 언어구현 (1)
        • 객체지향 (1)
        • 논리회로 (5)
        • 컴퓨터구조 (9)
        • 데이터베이스 (1)
        • 컴퓨터 네트워크 (10)
      • 언어공부 (64)
        • Java | Kotlin (48)
        • JavaScript | TypeScript (9)
        • C | C++ (6)
      • 개인 프로젝트 (11)
        • [2025] Happy2SendingMails (3)
        • [2026] 골든리포트! (8)
        • [2026] 순수자바로 개발하기 (0)
        • 기타 이것저것 (0)
      • 팀 프로젝트 (29)
        • [2025][GDG]홍대 맛집 아카이빙 프로젝트 (29)
      • 알고리즘 (13)
        • 백준풀이기록 (11)
      • 놀이터 (0)
      • 에러 수정일지 (2)
      • 고찰 (24)
        • CEOS 23기 회고록 (2)
  • 블로그 메뉴

    • CS
    • 언어공부
    • 개인 프로젝트
    • 팀 프로젝트
    • 알고리즘
    • 고찰
    • 신년사
    • 컬러잇 개발블로그
  • 링크

    • 컬러잇 개발블로그
  • 공지사항

  • 인기 글

  • 태그

    144
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
Radiata
[JVM] JVM 밑바닥까지 파헤치기 독서 #2 - 자바는 어떻게 객체를 저장하는가?
상단으로

티스토리툴바