자바스크립트의 메모리 관리 및 가비지 컬렉션과 관련해 공부한 내용에 대해서 포스팅을 작성해 보았습니다.
1. 가비지 컬렉션이란? (GC)
자바스크립트는 객체가 생성되었을 때 자동으로 메모리를 할당하고 쓸모 없어졌을 때 자동으로 해제한다.
그렇다면 메모리는 언제 생겨나고 없어질까? 메모리의 생존주기는 아래와 같다.
1-1. 메모리 생존주기
1) 필요할때 할당한다.
2) 사용한다. (읽기, 쓰기)
3) 필요없을 때 해제한다.
1)과 3)은 가비지 컬렉션을 사용하는 Javascript 에서는 암묵적으로 작동하며, C 언어와 같은 저수준 언어에서는 아래와 같이 명시를 해주어야 한다.
#include<stdlib.h>
int main() {
int pNum = malloc(sizeof(int)); // 4 byte, int 형 데이터 할당
free(pNum); // 동적으로 할당한 메모리 해제
}
저수준 언어인 C 언어에서는 malloc, free 키워드를 통해 메모리를 할당하고 해제할 수 있지만, Javascript 에서는 언제 어떻게 메모리가 할당될까?
1-2. Javascript 에서 메모리가 할당되는 경우
1) 값 초기화 : 값을 선언할 때 메모리 할당
var number = 8;
var name = 'kim';
var obj = {
a: 1,
b: 'young'
};
2) 함수 호출 : 함수 호출 결과 메모리 할당이 일어나기도 한다. (값 또는 오브젝트)
var arr1 = ['hello'];
var arr2 = ['world'];
var arr3 = arr1.concat(arr2);
console.log(arr3);
// ["hello", "world"]
* 값 사용 : 할당된 메모리를 읽고 쓰는 것 = 변수나 객체를 읽고 쓸 때 = 함수 호출 시 함수에 인수를 넘길 때
2. Javascript 가비지 컬렉션 알고리즘 2가지
할당된 메모리가 더 이상 필요없을 때 메모리가 해제된다. 그럼 할당된 메모리가 더 이상 사용되지 않을 때는 어떻게 알아낼까?
1. 참조세기 (Reference-counting) 알고리즘
참조세기 (Reference-counting) 알고리즘은 "더 이상 필요없는 오브젝트를"를 "어떤 다른 오브젝트도 참조하지 않는 오브젝트"라고 정의하는 알고리즘이며, 참조하지 않는 오브젝트를 "가비지"라고 부른다.
(참조하지 않을 때 가비지로 수집 가능)
* 참조 이해하기
A라는 메모리를 통해 B라는 메모리에 접근할 수 있다면 "B는 A에 참조된다고 볼 수 있다."
(명시적이든 암시적이든)
ex1) C 언어의 포인터와 비슷한 개념
ex2) 모든 Javascript 오브젝트는 prototype 를 암시적으로 참조하고 오브젝트의 속성을 명시적으로 참조한다.
ex3) 코드를 통한 이해
const a = [1, 'hello', 99];
let b = a;
b.push(123);
console.log(a);
console.log(b);
// [1, "hello", 99, 123]
b 라는 변수가 a 배열을 참조하고 있으며, b 는 a 의 메모리에 접근가능하다.
참조 관계이기 때문에 a 또는 b 의 값을 바꾸게 되면, 동시에 a 와 b 의 값이 변경된다.
* 참조세기 알고리즘 예제
var obj1 = {
obj2: {
val: 2
}
};
var p1 = obj1;
obj1 = 0;
var p2 = obj1.obj2;
// 이때 가비지 컬렉션이 일어날까? : No
// p1 은 obj1 을 가리키고 있다.
// p2 은 obj2 를 가리키고 있다.
// obj1 의 값이 바뀌었지만 p1 과 p2 가 obj1 객체의 메모리를 가리키고 있어 메모리가 해제되지 않는다.
p1 = 'ok';
p2 = null;
// 이때 가비지 컬렉션이 일어날까? : Yes
// p1, p2 의 값 모두 obj1, obj2 객체를 가리키지 않게된다.
// 메모리에 할당된 obj1, obj2, val 객체를 어떠한 변수도 가리키지 않게된다.
// 할당되지 않은 값들은 가비지로 인식 된다.
그림을 통해서 참조 관계가 어떻게 되는지 파악해 볼 수 있다.
* 한계 : 순환참조
두 객체가 서로 참조하는 속성으로 순환 구조가 생성되는 경우 발생
function recursion(){
var obj1 = {};
var obj2 = {};
obj1.a = obj2; // obj1는 obj2를 참조한다.
obj2.a = obj1; // obj2는 obj1를 참조한다.
return "ref each other";
}
recursion();
원래라면 obj1, obj2 는 recursion() 함수의 지역변수로 할당되어, recursion() 함수 실행 후 메모리가 해제 되어야 하지만 실제 메모리 상에서는 두 객체가 서로를 참조하고 있기 때문에 메모리가 해제되지 않게 된다.
결국 recursion() 함수가 호출 될 때마다 obj1, obj2 객체 만큼의 메모리가 할당된 뒤 해제되지 않고 남게 되어 메모리가 낭비 될 것이다.
2. 표시하고 쓸기 (Mark-and-sweep) 알고리즘
표시하고 쓸기 (Mark-and-sweep) 알고리즘은 "더 이상 필요없는 오브젝트를"를 "닿을 수 없는 오브젝트"라고 정의하는 알고리즘이다.
주기적으로 가비지 콜렉터는 roots 로부터 시작하여 roots 가 참조하는 오브젝트들, 참조되는 오브젝트가 참조하는 오브젝트 ... 로 진행하면서 닿을 수 있는 오브젝트를 표시한다.
그리고 닿을 수 없는 오브젝트들에 대해 가비지 콜렉션을 수행한다.
=> "닿을 수 없는 오브젝트"라는 개념을 적용하게 되면 순환참조로 인한 메모리 누수 문제는 생기지 않는다.
* 서로 같은 의미 : 닿을 수 있는 오브젝트 = 도달 가능한 값 (reachable)
- "닿을 수 있는 오브젝트"가 되는 기준은?
1. 원래 닿을 수 있는 오브젝트 (root = 루트)
ex 현재 함수의 지역 변수와 매개 변수, 전역 변수...
2. root 가 참조하는 값이나 루트에서 참조할 수 있는 값
표시하고 쓸기 알고리즘 예제
1) 하나의 객체를 두 번 참조하는 경우
let obj = {name: 'kim'};
let pointer = obj;
obj = null;
위의 소스코드를 그림으로 표현하면 아래와 같다.
2) 객체간 참조 상황
객체간 서로 참조를 하고 있어도 roots 에서 도달 불가능한 경우 메모리가 해제된다.
* 한계 : 수동 메모리 해제
어떤 메모리를 언제 해제할지에 대해 수동으로 결정하려면 수동으로 "닿을 수 없는 객체"를 만들 수 있어야 하지만 표시하고 쓸기 알고리즘에서는 그 기능을 지원하지 않아 수동으로 메모리 해제를 할 수 없습니다.
- 참고한 사이트
1. MDN 사이트
2. Javascript Info
'웹 프론트엔드 > Javascript' 카테고리의 다른 글
AbortController를 사용해 API 비동기요청 취소하기 (0) | 2020.10.03 |
---|---|
웹 성능 향상을 위한 Throttle과 Debounce 개념부터 활용까지 (0) | 2020.09.07 |
댓글