━━━━ ◇ ━━━━
-/js

[JS] 자바스크립트 메모리 누수

메모리 누수란?

프로그램이 더 이상 사용하지 않는 메모리를 해제하지 못하는 현상. 메모리가 사용되지 않더라도 수동 또는 자동으로 해제되지 않아 메모리를 계속 점유하게 되고, 메모리 누수가 일어난다면 성능의 저하를 불러오게 된다.

자바스크립트 메모리는 단순 변수(원시타입)에 사용되는 스택복잡한 객체(참조 데이터 타입)에 사용되는 으로 구분된다

가비지 컬렉션

가비지 컬렉션 메커니즘은 수동과 자동 두가지 범위로 나뉜다. C와 C++은 수동 정리 메커니즘을 사용한다. 개발자가 변수를 위해 메모리를 할당받고, 필요가 없어지는 시점에 수동으로 메모리를 비워주어야 한다. 자바스크립트는 자동 정리 메커니즘을 사용하여 메모리의 할당과 해제에 드는 비용을 절감해준다.

아래와 같은 값들은 태생부터 도달가능하므로 명백한 이유 없이는 삭제되지 않으며 이를 root라고 부른다

  • 현재 함수의 지역 변수와 매개변수
  • 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
  • 전역 변수

자바스크립트의 기본 가비지 컬렉션 방식은 'mark-and sweep'이다.

  • 가비지 컬렉터는 root 정보를 수집하고 이를 mark한다.
  • root가 참조하는 모든 객체를 방문하고 이를 mark한다.
  • mark된 모든 객체에 방문하고 그 객체들이 참조하는 객체도 mark 한다.
  • 도달가능한 모든 객체를 방문할때까지 반복한다
  • mark되지 않은 객체를 메모리에서 삭제한다.

이러한 기본적인 알고리즘에 더해, 몇가지 최적화 기법을 적용하여 가비지 컬렉션을 빠르게 한다

  • generational collection(세대별 수집): 객체를 새로운 객체와 오래된 객체로 나누어 새로운 객체를 공격적으로 제거하고 오래된 객체는 감시하는 빈도수를 낮춘다. 객체 상당수는 생성 이후 역할을 수행하고 금방 쓸모가 없어지기 때문!
  • incremental collection(점진적 수집): 가비지 컬렉션을 여러 부분으로 분리하여 별도로 수행함으로써 한번에 많은 리소스를 사용하는 것을 방지
  • idel-time collection(유휴 시간 수집): CPU가 유휴 상태일때만 가비지 컬렉션을 실행

이 외에도 다양한 기법과 알고리즘을 통해 최적화한다.

메모리 누수 상황

메모리 누수 상황에는 공통적인 상황이 몇가지 존재한다.

  • 클로저의 잘못된 사용 (?)

클로저는 알다시피 자신이 생성될 때의 환경을 기억하는 함수다. 클로저의 변수를 밖에있는 전역변수에 할당할 시 의도치않은 메모리 누수가 발생할 수 있다.

저자의 의도가 잘 이해되지 않아서 나는 '클로저 속의 변수를 의도치 않게 전역 변수에 할당했을때 전역 변수는 해제되지 않으므로 클로저 속의 변수도 해제되지 않는다' 정도로 이해했다.

//저자의 예제. 어디서 클로저가 발생하는 것인지 잘 이해가 되지 않는다.
function fn1() {
  let a = new Array(10000);

  let b = 3;

  function fn2() {
    let c = [1, 2, 3];
  }

  fn2();

  return a;
}

let res = [];

function myClick() {
  res.push(fn1());
}
//내가 만들어본 코드
//이렇게 사용한다면 클로저의 변수를 전역변수에 저장하게 되어
//해제되지 않는 전역변수 속 클로저의 변수도 해제되지 않는다.
function fn1() {
    let a = new Array(10000);

    function push42(){
        a.push(42);
        return a;
    }
    return push42;
}

let res = [];
let test = fn1();

function myClick() {
    res.push(test());
}
  • 의도치않게 생성된 전역 변수

선언 없이 변수를 할당한다면 전역에 생성되는 원인이 된다. strict 모드를 사용하면 경고 메세지를 받을 수 있다.

function createArray(){
    array = new Array();
}
createArray();
//strict 사용
function createArray(){
    'use strict';
    array = new Array();
}
createArray();
//Uncaught ReferenceError: array is not defined
  • 분리된 DOM 노드

코드가 삭제된 DOM 노드를 참조하고 있다면 메모리는 해제되지 못한다.

let btn = document.querySelector('button')
let child001 = document.querySelector('.child001')
let root = document.querySelector('#root')

btn.addEventListener('click', function() {
    //child001을 삭제하지만, 전역변수가 아직 참조하고 있어 메모리가 해제되지 못한다
    root.removeChild(child001)
})
let btn = document.querySelector("button");

btn.addEventListener("click", function () {
    //로컬 스코프에서 변수를 생성하여 메모리가 로컬 스코프가 종료될 때 해제될 수 있도록 한다
  let child001 = document.querySelector(".child001");
  let root = document.querySelector("#root");
  root.removeChild(child001);
});
  • 콘솔 출력

브라우저는 우리가 출력하고자 하는 객체의 정보를 저장해놓는다. 따라서 콘솔 출력을 사용하면 가비지 컬렉터가 수거해가지 않는다.

//메모리가 해제되지 않는다
document.querySelector('button').addEventListener('click', function() {
  let obj = new Array(1000000);
    console.log(obj);
})
//메모리가 정상적으로 해제된다
document.querySelector('button').addEventListener('click', function() {
  let obj = new Array(1000000);
})

정말 변수 출력을 원한다면 아래와 같이 isDev 변수를 설정하고, isDev 변수가 참일때만 출력되도록 작성하라고 한다. (물론 이렇게 설정하여도 콘솔출력을 하게되면 메모리 누수가 발생한다. 그러니까 디버깅할때만 isDev 를 참으로 설정하고, 서비스할때는 거짓으로 설정하라는 뜻인것 같다)

if(isDev) {
    console.log(obj)
}
  • 해제 하지 않은 타이머

타이머를 설정하고 해제하지 않는다면, 메모리 누수가 발생할 수 있다. 따라서 계속 반복할 필요가 없다면 적절한 타이밍에 해제해주는것이 중요하다.

function createArr() {
    let largeArr = new Array(100000);

    setInterval(() => {
      let myArr = largeArr
    }, 1000);
}

document.querySelector('button').addEventListener('click', function() {
    //해제 없이 계속해서 setInterval()을 실행한다
    createArr();
});
function createArr() {
    let largeArr = new Array(100000);
    let count = 0;

    let timer = setInterval(() => {
        //count가 3을 넘으면 타이머를 삭제한다
        if(count > 3) clearInterval(timer);
        let myArr = largeArr;
        count++;
    }, 1000);
}

document.querySelector('button').addEventListener('click', function() {
    createArr();
});

참고 사이트

'- > js' 카테고리의 다른 글

[JS] 비교 연산자  (0) 2021.01.15
[JS] self vs this  (0) 2021.01.15
[JS] DOM  (0) 2021.01.15
[JS] 연동방식  (0) 2021.01.15
[JS] Hoisting  (0) 2021.01.14
COMMENT