본문 바로가기

내직업은 IT종사자/javascript

[javascript] javascript는 메모리관리가 어떻게 될까? (feat. 메모리 누수 사례)

반응형

 목차

 

1. 메모리의 생명주기


자바스크립트에서 변수나 함수를 사용할 때 JS엔진은 메모리를 할당하고 더이상 필요하지 않으면 해제 합니다. 

메모리 할당은 '이 공간을 내가 쓰겟다'라고 찜해놓는 과정이며, 메모리 해제를 하면 공간이 확보되어 또 다른 용도로 사용할 수 있게 됩니다.

 

 

https://velog.velcdn.com/images/sejinkim/post/addbb205-4014-4a6e-9392-bee4e7ee6dcb/image.png

 

[메모리 할당]

js 엔진에서 알아서 처리되는 과정이며, 자바스크립트는 생성한 객체에 필요한 메모리를 할당 합니다.

(* 메모리관점에서 객체는  변수 타입 object뿐만 아니라 함수와  함수의 스코프까지 칭합니다.)

 

[메모리사용]

변수에서 읽거나 쓰는 것

 

[메모리 해제]

js  엔진에서 알아서 처리되며, 할당되었던 메모리가 해제되면서 해제된 메모리는 새로운 용도로 사용할 수 있습니다.

 

javascript 는  Managed언어 이기 때문에 [메모리 할당]과 [메모리 해제]를 알아서 수행합니다.

* Managed language & unmanaged Language
C와 같은 언매니지드 언어는 개발자가 명시적으로 메모리를 할당하고 해제하기 위해 malloc()과 free() 같은 low-level 메모리 제어 기능을 제공한다. 언매니지드 언어는 메모리 제어를 개발자가 주도할 수 있으므로 개발자의 역량에 따라 최적의 성능을 확보할 수 있지만 그 반대의 경우 치명적 오류를 생산할 가능성도 있다.
Javascript 같은매니지드 언어는 메모리의 할당 및 해제를 위한 메모리 관리 기능을 언어 차원에서 담당하고 개발자의 직접적인 메모리 제어를 허용하지 않는다. 즉, 개발자가 명시적으로 메모리를 할당하고 해제할 수 없다. 더 이상 사용하지 않는 메모리의 해제는 가비지 콜렉터가 수행하며, 이 또한 개발자가 관여할 수 없다. 매니지드 언어는 개발자의 역량에 의존하는 부분이 상대적으로 작아져 어느 정도 일정한 생산성을 확보할 수 있다는 장점이 있으나 성능 면에서 어느 정도의 손실은 감수할 수 밖에 없다.

 

 

2. 메모리의 힙과 스택 


메모리 관리는 자바스크립트 엔진이 알아서 할당하고 필요하지 않으면 해제해줍니다.

메모리 저장은 어떻게 될까요?

JS엔진에는 저장할 수 있는 공간이 힙(heap)과  스택(stack)이라는 영역이 존재 합니다. 

힙과 스택은 엔진이 다른 목적으로 사용되는 두가지 자료구조입니다. 

 

[스택(Stack) : 정적메모리할당]

 

 

스택은 자바스크립트가 정적 데이터를 저장하는데 사용하는 자료구조 입니다. (LIFO 방식)

변수타입 중 원시값(String, number,boolean, undefined, symbol, null)이과 object 참조주소값이 저장되며 크기가 변경되지 않는 불변성을 가지고 있어 엔진은 각 값에 대해 고정된 크기의 메모리를 사전에 할당합니다. 그리고 실행 직전에 메모리를 할당한다는 점에서 정적 메모리 할당 이라고 합니다.

 

[힙(Heap): 동적메모리할당]

스택과 달리 엔진은 힙에 저장할때 고정된 크기의 메모리를 할당하지 않습니다.

대신 필요에 따라 더 많은 공간을 할당 합니다. 이러한 메모리 할당 방식을 동적메모리 할당 이라고 합니다.

 

2.1 스택(stack)과  힙(heap) 의 차이

  • 스택(Stack)
    • 원시 값 및 참조
    • 컴파일 타임에 크기를 알 수 있음
    • 고정된 크기의 메모리 할당
  •  힙(Heap)
    • 객체 및 함수
    • 런타임에 크기를 알 수 있음
    • 객체 당 제한 없음

 

 

 

 

3. 가비지 콜렉션 (Garbage collection )


 JS엔진에서 안쓰는 메모리를 해제할 때 가비지 콜렉터가 알아서 이를 처리합니다.

여기서 문제는 메모리가 여전히 필요한지에 대한 여부를 개발자가 결정할 수 없다는 것입니다. 다시말해 더 이상 불필요한 메모리가 사용되지 않는  순간에 메모리 해제할 것들을 수집할 수 있는 알고리즘이 필요한데,

여기서 많이 사용되는 알고리즘은 [Reference-counting garbage collection]과 [Mark-and-Sweep] 이렇게 두가지가 있습니다.

 

 

 

3.1 Reference-counting Garbage collection(오래된 방식)

이 알고리즘은 어떠한 값에 대해서 어디에도 참조(reference)하지 않고 있다면 GC는 이 값을 필요하지 않는 값으로 간주하고 이 값을 제거 합니다. 

 

// people 변수는 해당 object를 참조한다 .
let people = {
  name: "Jane"
};

//null값을 재할당한다.
people = null;

//{name: "Jane"} 이라는 값은 더이상 참조되지 않기 때문에 GC에 의해 메모리가 해제된다.
// people 변수는 해당 object를 참조한다 .
let people = {
  name: "Jane"
};

//girl에 people의 참조중인 값의 주소를 할당한다.
girl = people;

//people에 null을 할당한다.
people = null;

// people은 더이상 {name: "Jane"}을 참조하고 있지 않지만, girl은 참조하고 있기 때문에 해당 object는 여전히 메모리에 남아있게 된다.

 

하지만 reference-counting 방식은 순환참조(Circular reference)가 이루어지는 경우 메무리 누수의 요인이 된다는 문제점이 있습니다. (아래 예시코드 참고)

 

function couple() {
    const jane = {};
    const sam = {};

    // jane.bf는 sam을 참조한다
    jane.bf = sam;

    // sam.gf는 jane을 참조한다
    sam.gf = jane;

    return 'circular';
}

couple();

couple()함수가 호출되고나서 더이상 필요한 값이 아닌데도 불구하고 서로에 대한 참조가 걸려있기 때문에 GC는 이 값에 대한 메모리를 해제하지 않아 계속해서 메모리에 남아 있습니다. 이러한 순환참조는 메모리 누수의 주된 요인이 될 수 있습니다.

 

예전 브라우저인 IE6, IE7 에서  DOM object 에 reference-counting GC를 사용하는 것으로 알려져있는데, 아래의 경우는 메모리 누수를 일으키는 주된 요인이 될 수 있습니다. (아래 코드 참고)

 

let div;
window.onload = function() {
  div = document.getElementById('myDivElement');
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join('*');
};

myDivElement 는 circularReference에 자기 자신의 값을 참조하고 있습니다.

따라서 DOM트리에서 해당 element가 제거 되어도 여전히  reference는 남아있기 때문에 GC는 이값의 메모리를 해제 하지 않습니다. 특히나 순환참조 되고있는 값이 큰 사이즈라면 브라우저가 느려지거나 하는 이슈가 생길 수 있습니다. 

 

 

 

 

3.2 Mark-and-Sweep 

위의 reference-counting의 문제점으로는 불필요한 값임에도 불구하고 어디에선가 참조가 걸려있다면 이에 대한 메모리를 해제 하지 않는다는 문제점이 있었습니다. 반면에 Mark-and-sweep 알고리즘은 이 값이 참조되고 있는 값인지에 중점을 두지않고 도달 가능성(reachability)에 중점을 둡니다.

 

도달 가능성이란?
자바스크립트의 root라는 글로벌 object에서부터 시작하여 참조되는가에 대한 여부입니다.  
다시말해 root에서 부터 해당 값까지 도달이 가능한가에 대한 여부입니다. 

 

따라서 어떤 값에 대한 참조가 없는 경우는 당연히 도달이 불가능하기 때문에 메모리가 해제되어야 하는 값으로 여겨지고, 참조 되고 있다고 하더라도 root로부터 도달할 수 없다고 여겨진다면 처리됩니다.

 

위의 그림처럼 도달되지 않는 값 X로 표신된 메모리는 GC에 의해 해제 됩니다. 

2012년 부터 모던 브라우저 들은 Mark-and-sweep방식의 GC를 사용하고 있습니다. 

 

function Couple() {
    const jane = {};
    const sam = {};

    // jane.bf는 sam을 참조한다
    jane.bf = sam;

    // sam.gf는 jane을 참조한다
    sam.gf = jane;

    return 'circular';
}

Couple();

예시에서 Couple()이라는 함수가 호출된 후 root에서 jane과 same 값에 도달할 수 없기 때문에 GC에 의해서 메모리가 해제됩니다. 

 

 

즉,

  • 어떠한 값이 참조된다고 해서 root에서부터 도달 가능한 것은 아닙니다.
  • reference-counting의 순환 참조 문제점을 mark-and-sweep방식이 보완할 수 있습니다.
  •  GC는 자동으로 실행되며 강제로 멈추거나 실행시킬 수 없습니다. 

 

 

 

 

 

 

 

4. 메모리 누수 사례


4.1 전역변수

const, let키워드 대신 var를 사용한다거나 아예 생략해버리는경우, function 키워드로 정의한 함수인 경우

위에 경우는 엔진이 위의 변수를 window객체에 할당하게 됩니다. 

 

user = getUser();
var secondUser = getUser();
function getUser() {
    return 'user';
}

 

user, secondUser,getUser()모두  window에 할당되게 됩니다.

 

이는 전역 스코프에 정의된 변수 및 함수에만 적용됩니다. 전역변수를 사용할 수 있지만, 더이상 필요하지 않으면 공간을 확보해 주어야 합니다. 메모리를 해제하려면 변수에 null을 할당하면 됩니다.

 

 

4.2 잊어버린 타이머 해제

 

타이머 해제를 잊어버리면 어플리케이션의 메모리 사용량이 증가할 수 있습니다. 

// 잊어버린 타이머
const object = {};
const intervalId = setInterval(function() {
    // 여기에서 사용된 모든 것들은 interval이 클리어될 때까지 수집되지 않습니다
    doSomething(object);
}, 2000);

위 코드는 2초마다 함수를 실행합니다. 더이상 필요하지 않게 된 interval은 취소해야 합니다.

clearInterval(intervalId);

 

 

4.3 잊어버린 콜백

 

사용되다가 나중에 제거될 버튼에 onclick 이벤트 리스너를 추가한다고 가정해보면 

(구식 브라우저는 리스너를 수집할 수 없엇지만, 요즘에는 더이상 문제가 되지 않습니다.)

더이상 필요하지 않으면 이벤트 리스너를 제거하는 것이 좋습니다. 

 

const element = document.getElementById('button');
const onClick = () => alert('hi');

element.addEventListener('click', onClick);
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);

 

 

4.4 DOM참조

자바스크립트에서 DOM엘리먼트를 저장할때 발생합니다. 

엘리먼트를 document에서 제거해 줄 때 저장한 배열에서도 제거를 해주어야 합니다. 

const elements = [];
const element = document.getElementById('button');

elements.push(element);

function removeAllElements() {
    elements.forEach((item, index) => {
        document.body.removeChild(document.getElementById(item.id));

        // 배열에서 엘리먼트를 제거하면, DOM과 동기화된 상태로 유지됩니다.
        elements.splice(index, 1);
    });
}

모든 DOM 엘리먼트는 부모노드에 대한 참조도 유지하기 때문에 가비지 콜렉터가 element의 부모와 자식을 수집하는 것을 방지할 수 있습니다. 

 

 

 

 

 

참고: https://velog.io/@sejinkim/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B4%80%EB%A6%AC-%EC%84%A4%EB%AA%85,https://sustainable-dev.tistory.com/158

 

 

잘못된 정보에 대한 피드백은 언제나 환영입니다  (´▽`ʃƪ)♡

 

반응형