|
| 1 | +--- |
| 2 | +title: "동적 타이핑 언어 JavaScript 값은 실제로 어떻게 저장될까? (Feat. Tagged Pointer, NaN-Boxing)" |
| 3 | +createdAt: 2025-07-02 |
| 4 | +category: JavaScript |
| 5 | +description: JavaScript 에서는 숫자, 문자열, 불리언, 객체 등 다양한 값을 다룰 수 있습니다. 일반적인으로는 원시값은 스택에, 나머지는 힙에 저장된다고 합니다. 하지만 동적 타입 언어인 JavaScript 는 런타임에 타입이 결정되는데, 그렇다면 실제로 값은 어디에 또 어떻게 저장될까요 ? |
| 6 | +--- |
| 7 | + |
| 8 | +::: danger |
| 9 | +작성중인 게시글 또는 초안입니다. |
| 10 | +::: |
| 11 | + |
| 12 | +# 🤔 JavaScript 값은 어디에 저장되나? (힙 or 스택) |
| 13 | + |
| 14 | +JavaScript에서는 숫자, 문자열, 불리언, 객체 등 다양한 값을 다룰 수 있습니다. |
| 15 | +일반적인 언어 지식에서는 "원시 값은 스택에 저장되고, 객체는 힙에 저장된다"고 배웁니다. |
| 16 | + |
| 17 | +하지만 스택에는 고정된 크기의 메모리에 데이터를 저장하게되는데, <br /> |
| 18 | +그렇다면 런타임에 타입이 결정되는 JavaScript 에서는, <br /> |
| 19 | +크기도 타입도 다양한 값들을 어떻게 고정된 메모리에 저장할 수 있을까요? <br /> |
| 20 | + |
| 21 | +또 값을 읽을 때는 어떻게 타입을 구분할까요 ? |
| 22 | + |
| 23 | +이를 이해하기 위해서는 JavaScript 엔진의 메모리 관리 방식을 알아야 합니다. |
| 24 | + |
| 25 | +## 💾 실제로는 JavaScript 의 모든 값은 '힙메모리' 에 저장됩니다 |
| 26 | + |
| 27 | +V8 엔진에서는 숫자, 문자열, 객체를 포함해 **모든 값이 힙 메모리에 저장** 됩니다 |
| 28 | + |
| 29 | +이를 통해 어떤 값이든 **객체 처럼** 다뤄지고, Garbage Collector (GC) 가 메모리를 효율적으로 관리할 수 있습니다. |
| 30 | + |
| 31 | +> **❓ JavaScript 의 원시값은 스택에 저장되는거 아닌가요?** <br/> |
| 32 | +> |
| 33 | +> JavaScript 언어 관점에서 원시값은 스택에 저장되고, 객체 배열 등 참조값은 힙에 저장됩니다 <br/> |
| 34 | +> |
| 35 | +> 여기서 모든 값이 힙에 저장된다는것은 V8 엔진 관점에서의 이야기입니다. <br/> |
| 36 | +> |
| 37 | +> _정리하자면, JS 의 모든 값은 **객체처럼** 다루어 지며, <br/> |
| 38 | +> V8 엔진 입장에서는 어떤 값이든 HeapObject 로 취급될 수 있도록 설계되어있다는 의미입니다 <br/> |
| 39 | +> (즉, 값을 '동일한 인터페이스로 다룰 수 있도록' Tagging 구조로 표현한다는 뜻)_ |
| 40 | +
|
| 41 | +## 🏷️ Tagged Value |
| 42 | + |
| 43 | +V8 엔진에서는 값을 저장할 때, **타입 정보(Tag Bits)** 를 함께 저장합니다. |
| 44 | + |
| 45 | +64비트 슬롯의 **하위 2비트 (LSB, Least Significant Bit)** 를 사용하여 값을 구분합니다. |
| 46 | + |
| 47 | +```cpp |
| 48 | +// v8/objects/objects.h |
| 49 | + |
| 50 | +// Tagged<T> represents an uncompressed V8 tagged pointer. |
| 51 | +// |
| 52 | +// The tagged pointer is a pointer-sized value with a tag in the LSB. The value |
| 53 | +// is either: |
| 54 | +// |
| 55 | +// * A small integer (Smi), shifted right, with the tag set to 0 |
| 56 | +// * A strong pointer to an object on the V8 heap, with the tag set to 01 |
| 57 | +// * A weak pointer to an object on the V8 heap, with the tag set to 11 |
| 58 | +// * A cleared weak pointer, with the value 11 |
| 59 | +``` |
| 60 | + |
| 61 | +하위 비트만 확인하면 별도의 타입 정보를 찾지 않고도, 해당 값이 정수(SMI)인지, Heap Object 를 가리키는 포인터인지 즉시 알 수 있습니다. |
| 62 | + |
| 63 | +| LSB 값 | 설명 | |
| 64 | +| ------ | ------------------------ | |
| 65 | +| 00 | SMI (Small Integer) | |
| 66 | +| 01 | Strong Ref (Heap Object) | |
| 67 | +| 10 | Weak Ref (Heap Object) | |
| 68 | + |
| 69 | +## 🤔 Tag Bits 의 역할 |
| 70 | + |
| 71 | +이 `Tag Bits` 는 두가지의 역할을 합니다. |
| 72 | + |
| 73 | +> **1. V8 힙에 있는 객체를 가리키는 포인터인지 (강한참조 / 약한 참조) 구분** |
| 74 | +> |
| 75 | +> **2. 혹은 작은 정수 (SMI, Small Integer) 인지 구분** |
| 76 | +
|
| 77 | +이 덕분에 V8은 |
| 78 | + |
| 79 | +작은 정수 (SMI) 는 별도의 Heap Object 를 생성하지 않고, 64비트 스택 슬롯에 직접 값을 저장할 수 있습니다. |
| 80 | + |
| 81 | +> **❓ 어떻게 가능한데 ??** <br/> |
| 82 | +> 별도의 V8 실제 힙공간을 할당하지 않고, 스택 슬롯에 바로 31비트 정수 + 태그 비트를 넣어서 표현할 수 있습니다. <br/> |
| 83 | +
|
| 84 | +### ⌨️ 실제 코드 예시 |
| 85 | + |
| 86 | +> **❗️ JavaScript 관점의 힙메모리** <br/> |
| 87 | +> JS에서 말하는 힙은 객체나 배열처럼 참조 타입이 저장되는 공간을 말합니다. <br/> |
| 88 | +> |
| 89 | +> **❗️ V8엔진 관점의 힙메모리** <br/> |
| 90 | +> 실제로 JS 에서 선언되는 모든 값은 V8 엔진의 힙에 저장됩니다. <br/> |
| 91 | +> V8 엔진에 의해 GC (Garbage Collector) 가 관리되는 물리적인 힙 영역을 말합니다 |
| 92 | +
|
| 93 | +> V8 엔진은 항상 힙에 객체를 word 단위 (word-aligned) 의 주소에 할당합니다. <br/> |
| 94 | +> 64비트 시스템에서는 8바이트 단위로 할당되며, 32비트 시스템에서는 4바이트 단위로 할당됩니다. |
| 95 | +
|
| 96 | +실제로 v8 엔진에서는 다음과 같이 `kHeapObjectTag`, `kWeakHeapObjectTag`, `kHeapObjectTagSize` 를 정의하고 있습니다. |
| 97 | + |
| 98 | +```cpp |
| 99 | +// v8/include/v8-internal.h |
| 100 | + |
| 101 | +// Tag information for HeapObject. |
| 102 | +const int kHeapObjectTag = 1; // 01 |
| 103 | +const int kWeakHeapObjectTag = 3; // 11 |
| 104 | +const int kHeapObjectTagSize = 2; // 10 |
| 105 | + |
| 106 | +// Tag information for Smi. |
| 107 | +const int kSmiTag = 0; |
| 108 | +const int kSmiTagSize = 1; |
| 109 | +const intptr_t kSmiTagMask = (1 << kSmiTagSize) - 1; |
| 110 | +``` |
| 111 | + |
| 112 | +## 💡 SMI (Small Integer) |
| 113 | + |
| 114 | +하지만, 숫자와 같은 작은 값들을 매번 객체로 만들면 성능이 떨어집니다. |
| 115 | + |
| 116 | +V8 은 SMI(Small Integer) 라는 방법을 사용해 작은 정수는 힙 객체를 만들지 않고, **스택 슬롯에 직접 저장** 합니다. |
| 117 | + |
| 118 | +<img src="./img/v8-pointer-compression-1.png" alt="V8 Pointer Compression" width={400} /> |
| 119 | + |
| 120 | +정수 값, SMI (Small Integer) 는 왼쪽으로 Shift 한 뒤, 하위 2비트 (LSB)를 `00` 으로 두고 "이건 SMI야!" 라는 태그를 붙입니다. <br /> |
| 121 | + |
| 122 | +<br /> |
| 123 | + |
| 124 | +V8 엔진 내에서 JavaScript의 정수도 객체처럼 다뤄야 합니다. |
| 125 | + |
| 126 | +하지만 정수 값이 변경될 때 마다 매번 새로운 객체를 만들면 성능이 떨어집니다. |
| 127 | + |
| 128 | +그래서 V8은 **SMI (Small Integer)** 라는 트릭을 씁니다. <br/> |
| 129 | +정수값을 별도의 객체 없이, **64비트 스택 슬롯에 직접 넣는 방식**입니다. <br/> |
| 130 | +그리고 그게 정수인지 포인터인지 구분하기 위해 **하위 LSB 2비트를 Tag Bits** 로 사용합니다. |
| 131 | + |
| 132 | +```cpp |
| 133 | +V8_INLINE static constexpr int SmiToInt(Address value) { |
| 134 | + int shift_bits = kSmiTagSize + kSmiShiftSize; |
| 135 | + // Shift down and throw away top 32 bits. |
| 136 | + return static_cast<int>(static_cast<intptr_t>(value) >> shift_bits); |
| 137 | +} |
| 138 | +``` |
| 139 | +
|
| 140 | +정리하면, LSB 1비트는 SMI vs HeapObject 를 구분하는데 사용되고, <br /> |
| 141 | +LSB 2비트는 Strong Ref vs Weak Ref 를 구분하는데 사용됩니다. |
| 142 | +
|
| 143 | +## 👀 예를들어, 아래와 같은 코드가 있다고 해보겠습니다. |
| 144 | +
|
| 145 | +<center> |
| 146 | + <img src="./img/v8-pointer-compression-2.png" alt="V8 Pointer Compression" width={800} /> |
| 147 | +</center> |
| 148 | +
|
| 149 | +### 🧠 JavaScript 관점에서는... |
| 150 | +
|
| 151 | +- `a = 10` 은 원시 타입이기 때문에, 변수 `a` 자체에 값 10이 직접 저장됩니다. |
| 152 | +- 반면 `b = {name : "김대건"}` 은 객체 타입이기 때문에, 변수 `b` 는 실제 객체의 "참조 (주소)" 만 스택에 저장되고, 객체 자체는 힙(Heap) 에 저장됩니다. |
| 153 | +
|
| 154 | +### 🧠 V8 엔진의 관점에서는... |
| 155 | +
|
| 156 | +- V8 엔진은 내부적으로 모든 값을 Tagged Value 포맷으로 저장합니다. |
| 157 | +- `a = 10` 의 경우 SMI (Small Integer) 를 힙에 객체로 만들지 않고, 64비트 슬롯 안에 직접 저장합니다. |
| 158 | +- `10` 은 내부적으로 좌측으로 2비트 shift 한 뒤, 하위 2비트 (LSB)를 `00` 으로 두고 "이건 SMI야!" 라는 태그를 붙입니다. |
| 159 | +- `b = {name : "김대건"}` 의 경우, 객체는 메모리상의 힙에 새롭게 할당됩니다. |
| 160 | +- `b` 에는 객체가 저장된 주소 `0x12` 를 좌측으로 2비트 shift 한 뒤, 하위 2비트 (LSB)를 `01` 로 두고 "이건 Strong Ref로 가리키는 Heap Object 야!" 라는 태그를 붙입니다. |
| 161 | +
|
| 162 | +<br /> |
| 163 | +<br /> |
| 164 | +
|
| 165 | +ㅇㅇ |
| 166 | +
|
| 167 | +# 🔗 참고 자료 |
| 168 | +
|
| 169 | +- [V8 - Pointer Compression](https://v8.dev/blog/pointer-compression) |
| 170 | +- [NodeJS - Understanding and Tuning Memory](https://nodejs.org/en/learn/diagnostics/memory/understanding-and-tuning-memory) |
0 commit comments