본문 바로가기
Development/JavaScript

자바스크립트의 메모리 누수 (Memory Leaks)

by 개발자 데이빗 2022. 2. 8.

Memory Leaks

메모리 누수란?

부주의 또는 일부 프로그램 오류로 인해 더 이상 사용되지 않는 메모리를 해제하지 못하는 것

 

자바스크립트의 메모리

스택 메모리: 단순 변수 (String, Number, Boolean, Null, Undefined, Symbol, Bigint)

힙 메모리: 참조 데이터 타입 (Object, Array, Function)

 

MDN 문서의 자바스크립트 메모리 관리

C 언어같은 저수준 언어에서는 메모리 관리를 위해 malloc() 과 free()를 사용한다.

반면, 자바스크립트는 객체가 생성되었을 때 자동으로 메모리를 할당하고 쓸모 없어졌을 때 자동으로 해제한다(가비지 컬렉션).

이러한 자동 메모리 관리는 잠재적 혼란의 원인이기도 한데, 개발자가 메모리 관리에 대해 고민할 필요가 없다는 잘못된 인상을 줄 수 있기 때문이다.

 

메모리의 생존주기

  1. 필요할때 할당한다.
  2. 사용한다. (읽기, 쓰기)
  3. 필요없어지면 해제한다.

 

가비지 콜렉터

변수 또는 데이터가 더 이상 필요하지 않을 때 이들은 가비지 변수 또는 가비지 데이터가 된다.

그런 데이터가 메모리에 계속 쌓이면 결국에는 메모리 사용량을 초과하게 된다.

가비지 콜렉터의 목적은 메모리 할당을 추적하고 할당된 메모리 블록이 더 이상 필요하지 않게 되었는지를 판단하여 회수하는 것이다.

C와 C++는 개발자가 특정 양의 메모리를 할당받고 필요가 없어지면 수동으로 해당 메모리를 비워주워야 한다.

자바스크립트는 자동으로 가비지 컬렉팅을 해준다.

이러한 자동 메모리 관리 프로세스는 궁극의 방법은 아니다.

왜냐하면 어떤 메모리가 여전히 필요한지 아닌지를 판단하는 것은 비결정적 알고리즘이기 때문이다.

즉, 메모리 수집이 언제 수행될지 확신 할 수가 없고 예측 불가능하여 언제든 예상된 메모리보다 많은 메모리를 사용할 수 있다.

 

자바스크립트 가비지 컬렉션이 자동으로 이루어지지만 특정 변수들의 메모리를 수동으로 해제하는 일이 필요하다.

예를 들자면, 더 필요하지 않은 변수가 외부 변수에 의해 참조되고 있어 메모리가 해제될 수 없을 때 null을 할당해서 다음 가비지 컬렉션이 동작할 때 메모리를 해제할 수 있다.

 

Reference-counting 가비지 콜렉션

더 이상 필요 없는 오브젝트를 어떤 다른 오브젝트도 참조하지 않는 오브젝트 라고 정의함

Reference-counting 가비지 콜렉션의 한계로는 순환참조가 있다.

 

아래 순환참조의 예시에서는 element 라는 변수에 할당된 버튼 요소가 DOM 트리에서 제거 되어도 onClick 이벤트가 구독되어있기 때문에 명시적으로 이벤트 구독 해제를 하지 않으면 가비지 컬렉팅이 되지 않는다.

// 순환참조의 예시
let element = document.getElementById('button')

function onClick(event) {

  element.innerHtml = 'text'

}

element.addEventListener('click', onClick)

// element를 제거하기 전에 구독된 이벤트를 해제하여야 한다.
element.removeEventListener('click', onClick)

element.parentNode.removeChild(element)

 

Mark-and-Sweep 알고리즘

최신 브라우저에서 사용된다.

더 이상 필요 없는 오브젝트를 닿을 수 없는 오브젝트로 정의

순환참조가 일어나도 element는 더이상 닿을 수 없는 DOM 요소이기 때문에 클릭 이벤트 또한 사라져 해제 대상이 된다.

즉, removeEventListener를 호출할 필요가 없다.

 

메모리 누수 대표적인 예시

클로저의 잘못된 사용

https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156

 

An interesting kind of JavaScript memory leak

Recently, Avi and David tracked down a surprising JavaScript memory leak in Meteor’s live HTML template rendering system. The fix will be…

blog.meteor.com

 

메테오 개발자들이 발견한 흥미로운 자바스크립트 메모리 누수 예시이다.

1초마다 replaceThing이 반복해서 호출되며 longStr과 someMethod 클로저를 생성되고 unused 변수는 originalThing을 참조하는 클로저를 가지게 된다.

이때, 스코프 체이닝에 의해 unused의 내부함수는 부모함수의 스코프를 공유한다.

여기서 unused 내부함수가 없었다면 매번 생성되는 longStr은 Mark-and-Sweep 알고리즘을 사용하는 최신 브라우저에서는 originalThing이 사용되지 않음을 파악하고 가비지컬렉팅의 대상이 된다.

그러나 unused 내부함수에서 originalThing을 계속해서 참조하고 이 때문에 메모리 누수가 일어나게 된다.

var theThing = null

var replaceThing = function () {
  var originalThing = theThing

  var unused = function () {
    if (originalThing) console.log('hi')
  }

  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage)
    },
  }
}
setInterval(replaceThing, 1000)

 

의도치 않게 생성된 전역변수

일반적으로 전역변수는 가비지 컬렉팅의 대상이 되지 않는다.

또한 여러 개발자들이 협업하는 경우 동일한 변수명을 사용하여 변수가 섞이는 등의 문제가 될 수 있어 일반적으로 전역변수의 사용을 지양한다.

그러나 자바스크립트에서는 아래 예시처럼 정의하지 않은 변수에 값을 할당하면 전역변수에 값을 할당한 것처럼 작동한다.

이와 같이 의도치 않게 생성된 전역변수는 가비지 컬렉팅의 대상이 되지 않고 이는 메모리 누수의 원인이 될 수 있다.

use strict를 사용하면 정의하지 않은 변수에 값을 할당하는 경우 에러를 일으켜 실수를 예방할 수 있다.

'use strict'
function globalExample() {
    noDefinedVariable = new Array(100);
}

globalExample();

 

 

분리된 DOM 노드

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

// root의 자식요소인 child을 DOM 트리에서 제거하였지만
// child 변수가 해당 요소를 참조하고 있어 메모리 누수의 원인이 된다.
btn.addEventListener('click', function() {
    root.removeChild(child)
})
let btn = document.querySelector("button");
// 리스너의 콜백 함수 내부의 지역변수로 child를 정의하여 메모리 누수를 방지한다.
btn.addEventListener("click", function () {
    let child = document.querySelector(".child");
    let root = document.querySelector("#root");
    root.removeChild(child);
});

 

콘솔 출력

개발 환경일 때 디버그 목적으로 콘솔을 출력할 수 있다.

하지만 실제 프로덕션 환경에서는 가능한 콘솔에 데이터를 출력하지 말아야 한다.

콘솔 출력은 출력하고자 하는 정보를 브라우저에 저장하기 때문에 메모리 누수의 원인이 된다.
그래서 많은 자바스크립트 코딩 스타일 스펙에서 console.log를 사용하지 않기를 요구한다.
console.log, console.error, console.info, console.dir 등 불필요한 변수 출력을 자제하여 메모리 누수를 방지할 수 있다.

 

 

해제하지 않은 타이머

아래의 타이머 예시와 같이 사용 용도, 상황에 따라 적절히 타이머를 해제시켜주지 않는 경우 메모리를 계속 점유하고 있어 메모리 누수의 원인이 된다.

// 타이머를 해제하지 않아 메모리를 계속 점유한다.
function timerHandler() {
    let array = new Array(100);

    setInterval(() => {
        let myObj = array
    }, 1000)
}

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

// 60초 뒤에 clearInterval을 통해 메모리에서 해제된다.
function timerHandler() {
    let array = new Array(100);
    let second = 0;

    let timer = setInterval(() => {
        if (second === 60) clearInterval(timer);
        let myObj = largeObj;
        index++;
    }, 1000);
}

document.querySelector("button").addEventListener("click", function () {
    timerHandler();
});

 

 

 

댓글