js
- plain object, Array, Date, Error ...
객체 (plain object)
-
원시값
- 문자형, 숫자형, Bigint, boolean, symbol, null, undefined
-
객체
심볼형
-
문자형과 함께 객체 프로퍼티의 키로 사용
-
유일한 키
Symbol("key")
: 같은 문자열로 심볼을 만들어도 다른 심볼이면 서로 다름
-
let key1 = Symbol("key");
let key2 = Symbol("key");
let dog = {
name: "arrr",
[key2]: 55,
};
dog[key1] = 1;
-
심볼은 확장을 위한 기능
-
심볼형은 암시적 변환 X
-
심볼을 키로 만든 프로퍼티는 hidden property
- key 값이 같아서 생기는 충돌 방지
for let key in obj
: 반복문에 포함되지 않음
- Object.keys(): 배열에 포함되지 않음
- Object.assign(): 객체에 포함됨
-
global symbol registry
- 이름이 같을 때 같은 심볼을 가리키려면
- Symbol.for("key"): 심볼이 없으면 생성, 있으면 읽음
- Symbol.keyFor(symbol): 심볼을 넘겨주고 문자열 반환, 전역 심볼이 아니면 undefined
-
system symbol: 내장 심볼
참조
- 객체는 대입 연산을 하면 참조로 작동
- 복사하려면
객체 -> 원시형 캐스팅
- boolean
- 숫자
- 문자
- hint
- string, number, default
- 객체에
Symbol.toPrimitive(hint)
메서드가 있으면 호출 (시스템 심볼)
- 없고 string이면
toString
또는 valueOf
호출 (toString이 없으면 valueOf)
- 위 사항이 모두 아니고 number, default면
valueOf
또는 toString
호출
- Symbol.toPrimitive 함수는 hint를 받아 이에 따른 적절한 행동
- 기본 toString, valueOf 반환값
[object Object]
- valueOf는 객체 자신을 반환 (호출 무시?)
- 와일드카드?
- Symbol.toPrimitive와 valueOf가 없으면 toString이 모든 형 변환 처리
- 항상 hint에 명시된 자료형을 반환해주는 것은 아님(항상 원시값 반환은 맞음)
- 객체가 피연산자일 때
- 앞서 본 규칙에 따라 원시형으로 변환
- 변환 후 타입이 적절치 않은 경우 한 번 더 형변환
GC
- reachability 기준으로 메모리 관리
- root 가 될 수 있는 값
- 스택에서 루트를 검색
- 지역 변수가 가리키는 객체는 루트 객체
- 전역 객체는 루트 객체
- mark and sweep
- root를 mark
- root가 참조하는 것 mark
- mark 한 객체가 참조한느 것 mark
- 갱신되지 않을 때 까지 반복
- mark 안된 것 삭제
- 최적화
- generational collection: 오래 남은 것이 지속적으로 사용된 것
- incremental collection: 작업량 줄이기 위해 GC를 여러 부분으로 분리 후 각 부분 별도 수행
- idle-time collection: cpu가 idle 일 때 수집
힙 구조
- V8
- new-space: 대부분의 객체. 작고 GC 되기 쉽게 설계, 다른 공간과 독립적
- old-pointer-space: 다른 객체에 대한 포인터를 가질 수 있는 대부분의 객체. new-space에서 살아남으면 옮겨짐
- old-data-space: 원시 값. 다른 객체에 대한 포인터 없음. new-space에서 살아남으면 옮겨짐
- large-object-space: 다른 공간의 크기를 넘어선 객체 포함. 옮겨지지 않음 (오버헤드 크기 때문에)
- code-sapce: JIT 컴파일된 코드 객체. 유일한 실행 가능한 메모리(large-object-space에도 코드가 할당되고 실행될 수 있다.)
- cell-space, property-cell-space, map-space: Cell, PropertyCell, Map 모두 같은 크기의 오브젝트이고 가리키는 객체에 제약이 있어 collection 단순화
- 각 공간은 페이지로 구성 (large-object-space 제외 1MB)
- 페이지는 객체를 저장하기 위해 header(flags, meta-data), marking bitmap(위에서 다룬 mark 객체 표현) 포함
- slot buffer: 각 페이지에 할당되어 저장된 개체를 가리키는 리스트 만들 때 사용
포인터 식별
- GC는 포인터와 데이터를 구별할 수 있어야 함
- Conservative: word 들을 포인터라고 가정.
- 데이터를 포인터로 잘못 분류할 수 있음
- 메모리 압축과 객체 이동을 제한
- 포인터라고 생각하고 데이터를 변경할 수 있기 때문
- Compiler hints: 정적 언어는 컴파일러가 정보를 제공할 수 있음
- 각 클래스 내의 포인터를 찾음
- js는 동적이라 불가능 (모든 필드에 포인터나 데이터가 포함될 수 있음)
- Tagged pointers: word에 tag bit를 추가하여 포인터인지 데이터 인지 명시적으로 표현
- V8은 Tagged pointers 사용
- 정수는 32비트 워드와 낮은 비트가 0으로 설정
- 포인터는 1
Generational collection
- 대부분의 객체는 수명이 짧고 적은 수의 오브젝트많이 수명이 길다.
- new space: 공간이 다 차면 scavenge라 불리는 빠른 GC 사이클이 객체 정리, 할당 비용이 작음, (1MB ~ 8MB)
- to-space from-space로 한 번 더 나눔
- to가 가들 차면 from과 교체
- 모든 객체는 from space에 있음
- 살아있는 객체는 to-space로 옮기거나 old로 승격
Write Barrier
- old-space 포인터 -> new-space 객체
- new-space 객체를 지우기 위해 old-space 를 검색하는 것은 비효율적
- write barrier는 store 후 실행되는 작은 코드
- write barrier가 포인터를 기록하여 new-space가 collected 될 때 사용
- 비용이 크지만 GC가 live 객체를 식별하는 데 필요
- 최적화 기법
- 정적으로 new-space에 있는 것을 증명할 수 있다면 (write barrier 생략)
- 로컬 참조가 아닌 참조가 없을 때 스택에 객체 할당 (write barrier 생략)
- 페이지 헤더에 어느 space에 있는지 체크
GC Pauses
- incremental marking
- 힙이 5~10ms의 작은 pause로 보이게 함
- 힙이 임계값에 도달하면 활성화
- 활성화되면
- lazy sweeping
- 도달 가능한 객체를 파악 후 바로 collect 하지 않고 페이지가 교체될 때 collect
참조
https://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection