자바스크립트 메모리 관리(V8) 조금 더 알아보기

작성일: 2023.06.14

AI 요약

개발자의 블로그 글은 딥다이브 스터디에서의 발표와 자바스크립트 메모리 관리에 대한 내용을 다루고 있음. 자세한 구현은 엔진과 브라우저에 따라 다르다고 함.

Contents

발단

딥다이브 스터디에서 동료 분의 발표를 듣던 중에 궁금한(잘못 알고 있던) 점이 있었다.

230712-000905

let a = 'hello'
let b = a // 요 부분에서의 동작

나는 데이터 영역에서 @5002의 데이터를 다른 데이터 영역으로 복사하고 (예를들면 @5004), 그 값을 변수 영역에 할당된 b에 링크해준다고 생각하고 있었다.

그런데, 발표자 분의 말씀으로는 엔진에서 효율성을 위해 데이터 영역에서 중복되는 값은 하나만 유지한다고 말씀하셔서 의아했다.

primitive value는 불변하다.

‘hello’는 원시값이고 불변하므로 a와 b는 같은 value memory space의 주소값을 참조하지만, 다른 변수 주소값을 가진다.

(위 개념에서 혼동이 있었음)

대화를 나눠보니 내가 잘못 알고 있는 것 같기도 하고, 사실 이 부분에 대해서 자세히 알려주는 곳이 거의 없어서 직접 확인해보자~ 하고 구글링과 이런 저런 시도를 해 보았다.

이론

const foo = 'baz'
const bar = foo

위의 코드는 문자열을 중복으로 구성하는 두 개의 개별 메모리 청크로 끝나게 되는가?

또는 런타임에 변수와 ‘baz’가 가리키는 문자열이 하나만 있는가?

답은 후자이다.

원시값은 스택에, 참조값은 힙 스페이스에 할당된다를 공식처럼 알고 있는데, 이것도 엄밀히 파보면 애매한 내용이다.

V8의 JavaScript 값은 개체, 배열, 숫자 또는 문자열인지 여부에 관계없이 객체로 표시되고 V8 힙에 할당된다. 이를 통해 객체에 대한 포인터로 모든 값을 나타낼 수 있다.

객체 리터럴을 사용하면 항상 새롭고 고유한 객체가 제공되는 반면 기본 값(숫자는 제외)은 재사용된다. 즉, string interning에 의해 런타임 시 메모리에 const a = ‘foo’; const b = ‘foo’;를 할 시 하나의 문자열만 저장된다.

230712-000922

숫자의 경우는 더 복잡하다. 아래 블로그의 내용을 옮겨봤다.

https://v8.dev/blog/pointer-compression#value-tagging-in-v8

smi에 해당하는 값은 재사용 처럼 보이지만 사실 tagged pointer 방식으로 작동하기 때문에, 그렇게 보이는 것 뿐. 예를 들어 정수값이 반복문에서 증가할 때 마다 새 값을 할당하는 비효율을 피하기 위해 포인터 값 자체에 추가 데이터를 담은 tagged pointer 방식을 사용한다. 그래서 smi 범위 안에 같은 정수 값이면 같은 포인터 값이 할당되기 때문에 결과만 보면 재사용한다고 생각되는 것… 사실 그 주소값에는 아무 값도 없다.

smi에 해당하지 않는… (예를들어 3.14) 값은

  1. smi 의 경우에는 아무것도 가리키지 않는, 유효하지 않은 포인터로 인코딩되므로 재사용의 전체 개념이 실제로 적용되지 않는다(위에서 말한 내용).
  2. HeapNumber (smi 로 간주되지 않는 숫자)의 경우
    1. 객체 프로퍼티가 가리키면 mutable한 HeapNumber가 되어 매번 새로운 HeapNumber를 할당하지 않고 값을 업데이트 할 수 있다. → HeapNumber를 공유할 수 없게 만드는 대가로 대다수 사용 패턴에 대한 유익한 tradeoff라고 생각한다.
    2. 다른 경우에는 재사용할 수 있지만 추가 성능 오버헤드가 발생하지 않는 경우에만 재사용할 수 있다. → 예를들어 3+0.14 와 314/100과 같은 두 개의 계산 결과가 할당될 때 이는 두 개의 HeapNumber값으로 할당된다. → 이 둘의 계산 결과를 확인한 이후 3.14가 존재하는지 확인하는 것이 그만한 가치가 없기 때문.

2-1. 내용의 내용이 제대로 이해가 안 간다… 번역을 잘못한건지

재사용이 가능하다는 의미처럼 들렸는데, 계속 보다보니 프로퍼티의 변수를 변경 가능한 HeapNumber로 유지해 값이 업데이트 되어도 다른 HeapNumber를 찾거나 하지 않고 그 자체를 갱신한다는 내용같은데, 때문에 같은 값이라도 여러 개의 HeapNumber를 memory space에 할당할 수 있다는걸까? 다른 의견이 있으시면 알려주세요…

실행

실행 코드 1

		<script>
      const btn = document.querySelector("#btn");
      btn.onclick = () => {
        const string1 = "foo";
        const string2 = string1;
        const string3 = "foo";
        const string4 = "bar";
        const number1 = 5;
        const number2 = number1;
        const number3 = 5;
        const number4 = 6;
      };
    </script>

230712-001045

‘foo’와 ‘bar’는 메모리 스페이스에 각 하나씩 들어있다.

그리고 각 변수는 각각의 value에 맞는 같은 주소를 가져다 사용한다.

실행 코드 2

230712-001101

그냥 실행했을 때: heap number 30개가 기본적으로 init 돼 있다.

const num3 = 3.14
const num4 = num3
const num5 = 2.17

위 코드를 script 내에서 초기화 해주었다.

230712-001112

32 개 → heap number에서도 중복이 일어나지 않는 경우로

num3와 num4가 같은 주소의 3.14를 사용했다.

실행 코드 3

function MyNumbers() {
  this.word1 = 'foo'
  this.word2 = 'goo'
  this.word3 = 'foo'
}
const num1 = new MyNumbers()
const num2 = new MyNumbers()

생성자의 인스턴스의 결과를 확인해보자.

230712-001128

string의 경우에는 여전히 같은 주소 값의 value를 사용한다.

			const myHeapNumber = 3.14;
      function MyNumbers() {
        this.smi = 123;
        this.number = myHeapNumber;
	    }
      const num1 = new MyNumbers();
      const num2 = new MyNumbers();

      const num4 = myHeapNumber;

230712-001140

smi에 해당하는 값은 @796339로 동일한 값이 사용됐다.

heap number에 해당하는 값은 같은 값을 가져도 @818117과 @818113으로 다른 메모리 스페이스의 주소를 사용한다.

결론

전에 읽었던 자바스크립트 딥다이브에서는 관련 내용을 설명할 때 세부 구현은 엔진과 브라우저 사양 등에 따라 다르다. 라는 내용이 있었는데, 그걸 직접 확인해보니 뭔 소리인지 조금은 더 알 것만 같은 기분이랄까…

엔진의 구현은 효율적인 관리를 위해 여러 장치가 되어있는 것 같다. 저기에 나타난 주소(@숫자)도 실제 주소도 아니다.

역시 눈으로 직접 보기 전까지는 믿기 힘들다.

참고 문서

(위 글의 주요 내용은 아래 글들의 내용을 토대로 번역해 작성하고, 직접 코드를 작성해서 테스트해보았다.)

What are JavaScript variables made of

Pointer Compression in V8 · V8

Tagged pointer

JavaScript memory model demystified

태그: