- Published on
JS GC 및 Memory
- Authors
- Name
- sulmo
개발 중 GC에대해 생각 없이 살다가 가끔 아주 가끔 뭔가 메모리 계산을 대강 해야할 때가 있었다. 이때 좀 도움이 되어보고자 한다. 기본적인 자료형에 따른 메모리 정보. GC가 도는 방식 및 기준들에대한 정리
1. JS 메모리 할당 케이스
값 초기화
Primitive value 기본 할당 사이즈
자료형 | 예시 | 예상 크기 |
---|---|---|
undefined | let a; | 4 bytes |
null | let a = null; | 0 ~ 4 bytes (엔진 따라 다름) |
boolean | let a = true; | 4 bytes |
number | let a = 123.45; | 8 bytes (IEEE 754 double) |
bigint | let a = 123n; | 가변 크기 (8 bytes 이상) |
string | let a = "abc"; | (2 × 문자 수) bytes + 오버헤드 |
symbol | let a = Symbol() | 고정된 내부 참조 (약 16~24 bytes 추정) |
const n = 123; // 정수를 담기 위한 메모리 할당
const s = "azerty"; // 문자열을 담기 위한 메모리 할당
const o = {
a: 1,
b: null,
}; // 오브젝트와 그 오브젝트에 포함된 값들을 담기 위한 메모리 할당
// (오브젝트처럼) 배열과 배열에 담긴 값들을 위한 메모리 할당
const a = [1, null, "abra"];
function f(a) {
return a + 2;
} // 함수를 위한 할당(함수는 호출 가능한 오브젝트)
// 함수식 또한 오브젝트를 담기 위한 메모리를 할당합니다.
someElement.addEventListener(
"click",
() => {
someElement.style.backgroundColor = "blue";
},
false,
);
함수 호출을 통한 할당
함수 결과로 리턴된 값에대한 할당
const d = new Date(); // Date 개체를 위해 메모리를 할당
const e = document.createElement("div"); // DOM 엘리먼트를 위해 메모리를 할당
const s = "azerty";
const s2 = s.substr(0, 3); // s2는 새로운 문자열
// JavaScript에서 문자열은 immutable 값이기 때문에,
// 메모리를 새로 할당하지 않고 단순히 [0, 3] 이라는 범위만 저장합니다.
const a = ["ouais ouais", "nan nan"];
const a2 = ["generation", "nan nan"];
const a3 = a.concat(a2);
// a 와 a2 를 이어붙여, 4개의 원소를 가진 새로운 배열
2. Garbage Collection
더 이상 필요하지 않은 메모리를 정리하는 가비지 컬렉션 알고리즘들과 그 한계를 이해하는데 필요한 개념을 설명합니다.
객체를 "새로 생성된 객체(Young)"과 "오래 살아남은 객체(Old)"로 나누고, 각 세대에 적합한 방식으로 GC를 수행하는 전략입니다.
V8 힙 메모리
// 설명을 위한 추가 자료
+-------------------------------+
| Heap |
| +-------------------------+ |
| | Young Generation | |
| | +------+ +----------+ | |
| | | Eden | | Survivor | | |
| | | | | (From/To)| | |
| +-------------------------+ |
| | Old Generation | |
| +-------------------------+ |
| |
| +-------------------------+ |
| | ETC | |
| +-------------------------+ |
+-------------------------------+
Mark-and-copy 방식 GC (Minor GC)
"새로 생성된 객체(Young)"를 대상으로 빠르게 진행하는 GC 방식입니다.
From에 대상을 올리고 도달 가능한 객체만 To 공간으로 복사, 나머지는 버립니다. 그리고 다시 To와 From을 스왑하여 지속합니다.
전체 플로우 예시 (Eden → From/To → Old Generation)
1단계: 객체 생성
const obj = { name: "hello" };
- 객체는 Eden 공간에 생성된다.
Heap 상태:
Eden: [obj]
From: []
To: []
Old: []
2단계: Minor GC 발생
조건:
- Eden이 가득 찼거나
- Young Generation 전체 사용량이 일정 임계치를 초과했을 때
GC 대상:
- Eden + From (둘 다)
- 루트(전역, 실행 컨텍스트 등)에서 도달 가능한 객체만 To로 복사
복사 후:
- To가 새 From이 되고, Eden/이전 From은 비워짐
Heap 상태:
Eden: []
From: [obj] To → From 스왑됨
To: []
Old: []
3단계: Minor GC 반복
- Eden에 새 객체가 들어오고 다시 GC가 발생하면,
- 기존 From과 Eden의 객체들이 다시 To로 복사된다.
obj
는 계속 도달 가능한 상태라면 반복해서 살아남게 된다.
복사 횟수가 2회 이상이면 V8은 해당 객체를 Old Generation으로 승격시킨다.
4단계: 승격(Promotion)
obj
가 여러 번의 Minor GC를 생존한 경우,- 이제 더 이상 Young Generation에 남기지 않고 Old Generation으로 이동시킨다.
Heap 상태:
Eden: []
From: []
To: []
Old: [obj]
Mark-and-sweep방식 GC (Major GC)
Minor GC로 부터 살아남은 대상들을 정리합니다. "더 이상 필요없는 객체"를 "도달할 수 없는 객체"로 정의하고 이를 제거합니다.
- 현재 함수의 지역 변수와 매개변수
- 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
- 전역 변수
- 기타 등등 위와 같은 값은 루트(root) 로 칭하며 루트를 시작으로 참조를 Mark 하고, 나머지를 sweep 하는 방식이다.
정확히 누가 Root인가?
루트 대상 | 설명 |
---|---|
Global object | 브라우저 환경에서는 window , Node.js에서는 global 객체가 루트 |
현재 실행 중인 함수의 지역 변수 및 매개변수 | 콜 스택에 올라와 있는 함수들의 활성 레코드(Activation Record, 또는 실행 컨텍스트)는 루트 |
클로저가 참조하는 변수들 | 클로저를 통해 참조되고 있는 외부 변수들도 도달 가능하므로 루트로부터 도달 가능 |
DOM 트리의 루트 (document ) | 브라우저 환경에서는 document 객체도 루트 중 하나로 취급 |
Web API에서 참조하는 객체들 | 예: setTimeout , EventListener 등이 참조 중인 객체는 GC 대상이 아님 |
1. 브라우저
<head>
에 포함된 JS 파일의 변수들은 루트인가?
1-1. HTML 해당 JS 파일이 전역 컨텍스트에서 실행되는 경우, 그 안의 var
, function
으로 선언된 변수/함수는 window
객체의 프로퍼티가 되므로 루트에 포함됨
<head>
<script src="main.js"></script>
</head>
// main.js
var a = 10; // → window.a = 10
function foo() {} // → window.foo = function(){}
let b = 20; // → 스코프에만 존재, window.b ❌`
const b = 20; // → 스코프에만 존재, window.c ❌`
var
,function
→ window의 속성 → 루트에 포함let
,const
→ 스크립트 스코프 안에만 존재 →window
에 안붙음 → 루트는 아님 (단, 아직 실행 중이면 활성 컨텍스트이므로 GC 대상은 아님)
<script>
태그 내 직접 정의된 변수/함수는 루트인가?
1-2. <script>
내부의 코드는 마찬가지로 전역 컨텍스트에서 실행되기 때문에,
var
,function
은window
에 등록되어 루트입니다.let
,const
는Script
스코프에 존재하지만, 전역 컨텍스트의 실행이 끝날 때까지는 참조되고 있으므로 GC 대상은 아님.
<script>
var x = 100; // window.x → 루트
function greet() {} // window.greet → 루트
const y = 200; // window.y ❌ → Script scope (루트 아님)
// 하지만 greet 함수에서 y를 참조한다면 → 클로저에 의해 유지됨
</script>
2. Node.js
node main.js
로 실행했을 때, 변수와 함수는 루트인가?
2-1. 특정 파일을 Node.js에서 파일 스코프는 CommonJS 모듈 시스템에 의해 별도의 함수(wrapper)로 감싸져 실행됩니다.
즉, Node.js는 다음처럼 내부적으로 감싸 실행합니다:
(function(exports, require, module, __filename, __dirname) {
// 여기가 main.js 내용
const a = 1;
function foo() {}
})();
이 구조에서:
const
,let
,function
,var
모두 모듈 스코프에 존재함 → 전역(global) 객체에 붙지 않음.따라서 변수 자체는
global
에 포함되지 않아 GC 루트는 아님.하지만 실행 중인 함수 컨텍스트에서 참조 중이라면 루트에서 도달 가능 → GC 대상 아님.
// main.js
const a = { value: 123 }; // global.a ❌
function test() {
console.log(a); // 클로저로 참조됨 → GC되지 않음
}