V8 Engine의 integer, string이 메모리 어디에 할당 되는가?
JavaScript는 debug하기가 어렵습니다. 때문에 node에서 컴파일러로 사용되는 V8 engine의 docs를 보거나,
여러 사람들이 작성한 Reference를 봐야합니다.
책 JavaScript Deep Dive에서 간략하게 JavaScript메모리에 대해 설명하고 있는데, 이를 좀 더 파보려고 합니다.
1. V8 Engine 메모리 구조
V8 Engine에는 기본적으로 Stack과 Heap메모리가 있으며, Literal을 저장하기위한 Constant pool이 있습니다.
2. Integer가 저장되는 방식.
Integer는 기본적으로 V8 engine의 stack pointer에 저장됩니다.
저장되는 영역은 SMI(Small Integer)라는 곳에 저장되는데, 운영되는 OS에 따라 32Bit, 64Bit저장 방식이 있습니다.
32Bit방식에서는 최대 31비트까지 저장할 수 있습니다.(0xFFFFFFFE)
나머지 1비트는 flag값으로 31비트 이상되는 값은 SMI가 아닌 Heap영역에 저장되며, Double형태를 가집니다.
64Bit방식에서는 최대 32Bit까지 저장이됩니다.
여기서 MSB는 부호를 나타내고 (+, -) 마지막 LSB는 Integer의 flag를 나타냅니다.
32Bit혹은 64Bit를 넘어서면 Heap에 할당됩니다.
이를 확인해보면 다음과 같습니다.
function test1() {
let hwan1 = 123123123123
}
function test2() {
let a = 9876;
}
test1()
test2()
$ node --print-bytecode --print-bytecode-filter='test*' test.js > code.txt
[generated bytecode for function: test1 (0x000107d51b19 <SharedFunctionInfo test1>)]
Parameter count 1
Register count 1
Frame size 8
34 S> 0x107d524de @ 0 : 12 00 LdaConstant [0]
0x107d524e0 @ 2 : 26 fb Star r0
0x107d524e2 @ 4 : 0d LdaUndefined
47 S> 0x107d524e3 @ 5 : aa Return
Constant pool (size = 1)
0x107d52481: [FixedArray] in OldSpace
- map: 0x0001071c0729 <Map>
- length: 1
0: 0x000107d52499 <HeapNumber 123123123123.0>
Handler Table (size = 0)
Source Position Table (size = 6)
0x000107d524e9 <ByteArray[6]>
[generated bytecode for function: test2 (0x000107d51b69 <SharedFunctionInfo test2>)]
Parameter count 1
Register count 1
Frame size 8
78 S> 0x107d52596 @ 0 : 00 0c 94 26 LdaSmi.Wide [9876]
0x107d5259a @ 4 : 26 fb Star r0
0x107d5259c @ 6 : 0d LdaUndefined
84 S> 0x107d5259d @ 7 : aa Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 7)
0x000107d525a1 <ByteArray[7]>
※실행한 OS는 64Bit입니다.
test1()의 Debug결과를 확인해보면 Number가 32Bit를 넘어서서 Heap에 할당된 것을 확인할 수 있으며,
Constant pool (size = 1) 인걸 알 수 있습니다.
해당 숫자는 Literal로 선언되어 있어서 Constant pool에 등록된 것입니다.
반면 test2()의 경우 Debug결과 Constant pool에도 등록이 안되었으며, Heap에 등록된 내용이 없습니다.
이것은 해당 Literal값이 stack에 들어갈 때 pointer자체에 해당 값이 박혔기 때문입니다.
3. String이 저장되는 방식
String의 경우에는 문자열 취급이라서 모든 Literal은 Constant pool 에 들어가게 됩니다.
테스트 코드를 보겠습니다.
function test1() {
let hwan1 = "hwan1"
let hwan2 = "hwan1"
}
function test2() {
let hwan1 = "hwan2"
let hwan2 = new String("hwan2")
}
test1()
test2()
[generated bytecode for function: test1 (0x000107611b31 <SharedFunctionInfo test1>)]
Parameter count 1
Register count 2
Frame size 16
34 S> 0x1076124e6 @ 0 : 12 00 LdaConstant [0]
0x1076124e8 @ 2 : 26 fb Star r0
55 S> 0x1076124ea @ 4 : 12 00 LdaConstant [0]
0x1076124ec @ 6 : 26 fa Star r1
0x1076124ee @ 8 : 0d LdaUndefined
63 S> 0x1076124ef @ 9 : aa Return
Constant pool (size = 1)
0x107612499: [FixedArray] in OldSpace
- map: 0x000106940729 <Map>
- length: 1
0: 0x000107611921 <String[#5]: hwan1>
Handler Table (size = 0)
Source Position Table (size = 8)
0x0001076124f1 <ByteArray[8]>
[generated bytecode for function: test2 (0x000107611b81 <SharedFunctionInfo test2>)]
Parameter count 1
Register count 4
Frame size 32
98 S> 0x1076125be @ 0 : 12 00 LdaConstant [0]
0x1076125c0 @ 2 : 26 fb Star r0
119 S> 0x1076125c2 @ 4 : 13 01 00 LdaGlobal [1], [0]
0x1076125c5 @ 7 : 26 f9 Star r2
0x1076125c7 @ 9 : 12 00 LdaConstant [0]
0x1076125c9 @ 11 : 26 f8 Star r3
0x1076125cb @ 13 : 25 f9 Ldar r2
119 E> 0x1076125cd @ 15 : 65 f9 f8 01 02 Construct r2, r3-r3, [2]
0x1076125d2 @ 20 : 26 fa Star r1
0x1076125d4 @ 22 : 0d LdaUndefined
139 S> 0x1076125d5 @ 23 : aa Return
Constant pool (size = 2)
0x107612569: [FixedArray] in OldSpace
- map: 0x000106940729 <Map>
- length: 2
0: 0x000107611939 <String[#5]: hwan2>
1: 0x0001069440d1 <String[#6]: String>
Handler Table (size = 0)
Source Position Table (size = 11)
0x0001076125d9 <ByteArray[11]>
test1()을 보면 Constant pool(size = 1) 인걸 확인할 수 있습니다.
즉, 문자열 "hwan1"이라는 Literal은 Constant pool에 등록되어,
2개의 변수가 동시에 하나의 문자열을 참조하고 있는걸 확인할 수 있습니다.
또한 Constant pool에 등록된 값들은 Heap에 들어가게되며, String같은 경우 같은 값을 참조하게 됩니다.
const arr = [];
setTimeout(() => {
for(let i = 0;i< 10000;i++) {
arr.push('hwan');
}
}, 3000);
const arr2 = [];
setTimeout(() => {
for(let i = 0;i< 10000;i++) {
arr2.push(new String('hwan'));
}
}, 5000);
위 사진을 보면 같은 String에 대해서 같은 주소값을 참조하고 있는걸 확인할 수 있습니다.
특이한 점은 new로 할당된 변수들의 메모리 사용을보면 Literal보다 더 많이 사용하는걸 알 수 있습니다.
구조를 보면 다음과 같습니다.
때문에 변수에 할당된 String을 비교해보면 위와같은 결과가 나오는 것을 확인해볼 수 있습니다.
참고
https://medium.com/@stankoja/v8-bug-hunting-part-1-setting-up-the-debug-environment-7ef34dc6f2de
https://blog.outsider.ne.kr/1307
https://levelup.gitconnected.com/bytefish-vs-new-string-bytefish-what-is-the-difference-a795f6a7a08b
https://stackoverflow.com/questions/61722899/constant-pool-content-lost-when-generating-v8s-bytecode
https://blog.dashlane.com/how-is-data-stored-in-v8-js-engine-memory/
http://www.egocube.pe.kr/lecture/content/html-javascript/202004070001