Javascript에서의 메모리 관리에 대해 알아보자.
메모리 생명 주기 Memory Life Cycle
Javascript에서 변수, 함수, 객체 등을 만들 때 JS 엔진은 메모리를 할당하고 필요 없어지면 해제합니다.
메모리 할당은 메모리의 빈 공간을 예약하는 과정이고 메모리 해제는 다른 목적으로 사용할 공간을 확보합니다.
과정
메모리 할당 Allocate
생성한 객체에 필요한 메모리를 할당합니다.
(자바스크립트는 값을 선언할 때 자동으로 메모리를 할당합니다.)메모리 사용 Use
작성한 코드에서 메모리를 사용합니다.
메모리 사용이란 기본적으로는 할당된 메모리를 읽고 쓰는 것을 의미합니다.메모리 해제 Release
할당된 메모리가 해제되면 새로운 목적으로 다시 사용할 수 있습니다.
메모리 관리에서 ‘객체’ 란 함수나 여러 스코프들도 포함합니다.
힙 Heap, 스택 Stack
스택 Stack
Javascript가 사용하는 정적 데이터를 저장하는 공간입니다.
- 정적 데이터(Static data)란?
- 엔진이 컴파일시 데이터의 크기를 알고있는 데이터입니다.
Javascript에서 원시값들(string, number, boolean, undefined, null)과 객체와 함수의 참조를 포함하고 있습니다.
엔진은 크기가 변경되지 않는다는 것을 알고 있기에 각 값들에게 고정된 양의 메모리를 할당합니다.
힙 Heap
Javascript의 객체와 함수를 저장하는 다른 공간입니다.
스택과는 다르게 엔진은 객체에 고정된 양이 아닌 필요한만큼 많은 공간을 할당해줍니다.
이런 식으로 메모리를 할당하는 것을 동적 메모리 할당이라고 부릅니다.
Javscript에서의 참조
모든 변수들은 스택을 먼저 가리킵니다.
원시 값이 아닌 경우, 스택에서는 힙의 객체에 대한 참조를 가지고 있습니다.
힙의 메모리는 정렬되어있지 않기때문에 스택에 참조를 유지해야합니다.
참조(reference)를 주소라고 생각하고 힙의 객체는 주소를 가진 집이라고 생각할 수 있습니다.
즉, Javascript는 객체와 함수들을 힙에 저장하고 원시 값들과 참조는 스택에 저장됩니다.
가비지 컬렉션 Garbage Collection
Javascript 엔진은 가비지 컬렉션으로 메모리 해제를 처리합니다.
가비지 컬렉션 알고리즘의 핵심 개념은 참조입니다.
A라는 메모리를 통해 B라는 메모리에 접근할 수 있다면 “B는 A에 참조된다” 라고 합니다.
Javascript 엔진이 주어진 변수나 함수가 더 이상 필요가 없다고 인식하고나면 메모리를 해제합니다.
메모리의 필요성을 판단하는 알고리즘
레퍼런스 카운팅 가비지 컬렉션 reference-counting garbage collection
자신을 가리키는 참조가 없는 객체를 수집합니다.
참고 사이트를 보면 빨간 상자가 마지막 코드를 가리킬 때 hobbies 객체만 힙에 reference가 유지되어 있습니다.
이 알고리즘이 가진 문제는 순환 참조를 고려하지 않는다는 것입니다.
즉, 하나 이상의 객체를 서로 참조하지만 더 이상 코드로 접근할 수 없을 때 문제가 발생합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13
let son = { name: "John", }; let dad = { name: "Johnson", }; son.dad = dad; dad.son = son; son = null; dad = null;
두 객체에 더 이상 접근할 수 있는 방법이 없습니다.
son과 dad 객체는 서로 참조하기 때문에 알고리즘은 할당된 메모리를 해제하지 않습니다.
마크 앤 스윕 알고리즘 the mark and sweep algorithm
단순히 주어진 객체의 참조의 수를 세는 대신에 루트 객체에서 도달할 수 있는지 감지합니다.
브라우저에서 루트는
window
이고 NodeJS는global
입니다.도달할 수 없는 객체를 가비지로 표기하고 나중에 수집합니다.
이 방법은 순환 종속성이 더 이상 문제가 되지않습니다.
이전 예제를 다시 보면 dad나 son객체는 루트에서 도달 할 수 없으므로 둘 다 가비지로 표기(mark)되어 수집됩니다.
메모리 누수 Memory Leaks
프로그램에서 버려진 메모리를 해제하지 못하는 경우를 말합니다.
가비지 컬렉터가 메모리를 올바른 방식으로 해제하지 않은 경우 발생합니다.
객체에 대한 참조
1 2 3 4 5 6 7 8
var foo = { bar1: memory(), // 5KB bar2: memory(), // 5KB }; function test() { alert(foo.bar1); }
다음 코드에서 test함수는 bar1만 참조하기 때문에 메모리를 5KB만 사용할 것 같지만 10KB를 사용합니다.
foo 객체 전체를 로딩해야하기 떄문입니다.
DOM 메모리 누수
DOM 항목을 가리키는 변수가 이벤트 콜백 외부에 선언된 경우 해당 DOM을 제거하더라도 해당 항목은 메모리에 남게됩니다.
1 2 3 4 5 6 7
var one = document.getElementById("one"); var two = document.getElementById("two"); one.addEventListener("click", () => { two.remove(); console.log(two); // 삭제 이후에도 html 출력 });
이벤트 콜백에서 사용된 DOM은 HTML에서 사라지더라도 참조가 남아 메모리 누수가 발생할 수 있습니다.
따라서, 등록을 해제하거나 안에서 선언하는 등으로 누수를 막아줍니다.
1 2 3 4 5 6 7 8
var one = document.getElementById("one"); one.addEventListener("click", () => { var two = document.getElementById("two"); two.remove(); }); one.removeEventListener("click");
전역 변수들 Global variables
window는 브라우저가 실행하는 데 필요한 객체이기 때문에 window의 속성으로 선언된 추가 객체는 제거할 수 없습니다.
선언된 전역변수는 window 객체의 속성으로 설정될 수 있습니다.
따라서 전역변수를 사용하지 않음으로 메모리를 절약할 수 있습니다.