V8에서 자바스크립트 변수는 어떻게 관리될까?
# 변수는 힙에 저장한다.
자바스크립트의 원시값, 객체는 힙에 할당되어 있다.(일정 용량이 넘으면 스택이 아니라 힙에 저장)
$ node --v8-options --stack-size
--stack-size (default size of stack region v8 is allowed to use (in kBytes))
type: int default: --stack-size=864
위처럼 기본은 864kb라고한다.
function memoryUsed() {
const mbUsed = process.memoryUsage().heapUsed / 1024 / 1024
console.log(`Memory used: ${mbUsed} MB`)
}
console.log('before')
memoryUsed() // Memory used: 5.12969970703125 M
const bigString = 'x'.repeat(10 * 1024 * 1024)
console.log(bigString) // 컴파일러가 bitString을 최적화하여 사용한 메모리를 제거하지 않게 함
console.log('after')
memoryUsed() // Memory used: 15.134803771972656 MB
위처럼 10mb string을 선언하고 살펴보면 10mb 정도의 메모리 차이가 있는것을 확인할 수 있다.
# stack에 얼마나 많은 데이터를 가지고 있을 수 있을까?
스택 데이터 크기는 포인터 크기일 뿐이며 더 큰 데이터로 무엇을 해야 할지 아는 것은 컴파일러나 런타임 환경이다.
언어별, CPU 별 차이가 있다고 하지만, c++이 1MB 정도라고 하니까
JS도 얼추 비슷할 것이긴 할것이다.
# 자바스크립트 원시값은 재활용된다.
function memoryUsed() {
const mbUsed = process.memoryUsage().heapUsed / 1024 / 1024
console.log(`Memory used: ${mbUsed} MB`)
}
console.log('before')
memoryUsed()
const bigString = 'x'.repeat(10 * 1024 * 1024)
console.log(bigString) // 컴파일러가 bitString을 최적화하여 사용한 메모리를 제거하지 않게 함
setTimeout(() => {
console.log('after12');
memoryUsed();
}, 0);
setTimeout(() => {
console.log('after13');
memoryUsed();
}, 0);
중복된 문자열은 메모리를 별도로 할당하지 않았다.
메모리에서 제거되지 않은 데이터 10MB만큼을 가지고 있고나서
그다음에 after12, after13이 순서대로 처리되었다.
자바스크립트에서 변수를 할당 하는 동작은 실제 값의 크기에 비례하는 비용이 드는 것은 아니다.
자바스크립트 변수의 대부분은 포인터로 이루어져 있다.
위처럼 문자열을 중복해서 사용하고 있다는 것을 알 수 있다.
# 포인터란?
컴퓨터 과학에서 포인터는 컴퓨터 메모리에 있는 다른 값의 메모리 주소를 저장하는 프로그래밍 언어 개체이다.
포인터는 메모리의 위치를 참조하고 해당 위치에 저장된 값을 얻는 것을 포인터 역참조라고 합니다.
(위키피디아)
포인터 역참조를 통해서 메모리 주소에 저장되어있는 변수를 가져오는 것이다.
그러면 어떻게 같은 변수를 참조한 데이터를 1개만 저장할 수 있을까?
이는 string interning, string-table, oddball 과 관계 있다.
# String Interning
각 문자열의 값을 복사본 하나만 저장하는 방법으로, 불변해서 관리하는 것을 string interning이라고 한다.
java에도 비슷한것으로 string.intern() 이라는것이 존재한다.
참고로 string pool은 heap에서 관리하게 된다.
String pool에 이미 존재하는가를 찾고, 이미 존재하므로 해당 문자열을 반환하는 것이다.
# String Table
이와 비슷하게, V8에서는 string-table이라는 코드의 형태로 코드를 관리한다.
# 클래스를 사용하여 키를 표현
class StringTableKey
# 테이블 키를 효과적으로 저장하는 방법을 결정
class V8_EXPORT_PRIVATE StringTableShape : public BaseShape<StringTableKey*>
# StringTable 클래스는 HashTable 클래스를 상속하며, 문자열을 테이블에서 검색하거나 추가하는 등의 기능을 제공
class V8_EXPORT_PRIVATE StringTable
: public HashTable<StringTable, StringTableShape>
# StringSet: 이 클래스도 문자열을 저장하기 위한 해시 테이블을 정의
class StringSet : public HashTable<StringSet, StringSetShape>
# StringSetShape 클래스는 StringSet에서 사용되는 해시 테이블의 모양을 결정하며,
# 문자열을 추가하고 검색하는 등의 기능을 제공.
class StringSetShape : public BaseShape<String>
...
EXTERN_DECLARE_HASH_TABLE(StringSet, StringSetShape)
class StringSet : public HashTable<StringSet, StringSetShape>
또한 oddball이라는 것이 미리 만들어둔 값을 부르는 것에 관여한다.
# Oddball이란?
type Null extends Oddball;
type Undefined extends Oddball;
type True extends Oddball;
type False extends Oddball;
type Exception extends Oddball;
type EmptyString extends String;
type Boolean = True|False;
JavaScript 엔진에서의 Oddball은 일반적으로 원시값 중 하나인 undefined, null, true, false와 같은 특별한 값들을 말한다.
Oddball은 내부적으로 JavaScript 엔진에서 사용되는 특수한 객체 타입으로, 원시값을 나타내는데 사용된다.
이런 객체들은 일반적인 객체보다는 메모리 효율성과 속도를 위해 특별한 방식으로 처리될 수 있다.
Oddball 객체는 엔진이 이러한 특수한 값들을 효율적으로 처리하는 데 도움을 준다.
즉, oddball의 객체타입을 가지고 있는 변수를 만들경우,
이들은 미리 만들어 둔 값인 원시값의 메모리를 참조해서 관리한다는 것이다.
# 맺는말
컴퓨터 메모리는 엄청나게 복잡하고 어렵다.
메모리와 관련된 질문에 대한 대부분의 답은 컴파일러와 프로세서 아키텍처 마다 다르기 때문이다.
예를 들어, 변수는 항상 메모리(RAM)에 있는 것은 아니다.
즉, 대상 레지스터에 직접 로드될 수도 있고, 즉각적인 값으로 명령의 일부가 될 수도, 심지어 완전히 무의 상태로 최적화 될 수 있다.
V8과 같은 자바스크립트 엔진은 너무 복잡하고 다양하다.
만약 메모리 레이아웃과 같은 low level의 세부 정보를 공부하기 위해서는 C, C++로 공부를 시작해서 소스코드가 기계어로 변환 되는 과정을 이해하는 것이 좋을것이다.
참고
https://www.dashlane.com/blog/how-is-data-stored-in-v8-js-engine-memory
https://yceffort.kr/2022/04/how-javascript-variable-works-in-memory
https://www.zhenghao.io/posts/javascript-memory