Develop Note by J.S.

[Programming] Memory Leak 유형 & 식별방법 본문

Knowledge/Programming

[Programming] Memory Leak 유형 & 식별방법

js-web 2023. 8. 28. 12:30
반응형

Memory Leak, GC에 이어 GC에서 걸러지지 않은 Memory Leak의 유형 및 식별 방법에 대하여 알아보겠습니다.

 

1. Memory Leak 유형

1) 선언되지 않은 변수의 참조 또는 참조가 없는 변수를 생성

 - 선언되지 않은 변수를 참조한 경우에는 컴파일 과정에서 전역 객체(Root)에서 새 변수를 생성합니다. 

function foo(){
    this.message = 'I am accidental variable';
}
foo();

Mark-and-Sheep 알고리즘에서는 Root를 가리키는 변수는 항상 active 상태이기 때문에 GC가 이를 제거할 수 없어 메모리 누수가 발생합니다. 

 - 코딩 시점에 사용되지 않거나 선언없이 참조한 코드를 삭제하거나, 자바스크립트의 use strict Mode를 활성화하여 방지 할 수 있습니다. 

 

2) Closer

 - Closer는 선언 시점의 렉시컬 환경 정보를 기억하는 함수로, 간단히 말해서 내부함수가 외부함수에 접근할 수 있는 것을 Closer라고 합니다. (Lexical Environment : 선언 시점에서의 지역변수, 매개변수 및 상위 Scope 등의 주변 환경)

 - 외부 함수의 변수를 사용하지 않더라도 Closer에 의해 메모리에 남아 있기 때문에 메모리 누수가 발생될 수 있습니다.

function outer(){
    const largeArray = []; // unused array
    return function inner(num){
        largeArray.push(num);
    }
}
const appendNumbers = outer(); // get the inner function
// call the inner function repeatedly
for (let i=0; i< 100000000; i++){
    appendNumbers(i);
}

 - largeArray의 경우 GC에서 접근할 수 없고, 반환되거나 사용되지 않기 때문에, Closer를 사용하는 개발 과정에서 메모리 누수방지를 위한 별도의 체크가 필요합니다. 

 

3) Forgetten Timers

 - setTimeout, setInterval과 같은 Javascript 타이머 실행 이후 사용하지 않음에도 중지시키지 않을 때 메모리 누수가 발생됩니다. 

function generateRandomNumbers(){
    const numbers = []; // huge increasing array
    return function(){
        numbers.push(Math.random());
    }
}
setInterval((generateRandomNumbers(), 2000));

 - 따라서 미사용 타이머는 clearInterval(), cleartimeout()를 사용하여 타이머를 중지해야 합니다.

 

4) Dom 외부참조

 - Dom에서는 제거되었지만 메모리에 남아있는 노드로 인해 메모리 누수가 발생 될 수 있습니다.

let parent = document.getElementById("#parent");
let child = document.getElementById("#child");
parent.addEventListener("click", function(){
    child.remove(); // removed from the DOM but not from the object memory
});

 - parent Click 이벤트로 child 노드를 삭제하여도 parent의 이벤트 리스너는 child 노드를 항상 참조하고 때문에 GC에서 걸러지지 않고 메모리에 남아 있습니다. 따라서 removeEventListener로 이벤트를 제거해야 합니다.

function removeChild(){
    child.remove();
}
parent.addEventListener("click", removeChild);
// after completing required action
parent.removeEventListener("click", removeChild);

 

2. Memory Leak 식별

 - 브라우저 앱의 DevTools에서 제공하는 Memory Graph 및 Perfomance 기능으로 메모리 누수를 식별 할 수 있습니다. 

1) Perfomance Timeline (메모리 소비 시각화)

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Memory leaks</title>
</head>
<body>
<button id="print">Print Numbers</button>
<button id="clear">Clear</button>
</body>
</html>
<script>
    var longArray = [];

    function print() {
      for (var i = 0; i < 10000; i++) {
          let paragraph = document.createElement("p");
          paragraph.innerHTML = i;
         document.body.appendChild(paragraph);
      }
      longArray.push(new Array(1000000).join("y"));
    }

    document.getElementById("print").addEventListener("click", print);
    document.getElementById("clear").addEventListener("click", () => {
      window.longArray = [];
    });
</script>

 - 예제의 Print Number는 <p> 단락 노드를 생성하하면서 전역변수 longArray에 대량의 데이터를 푸시합니다. 이후 clear를 통해 전역변수는 초기화하고 단락 노드는 그대로 두었습니다.

 - 해당 이미지는 Chrome DevTools - Perfomance 타임라인의 스크린샷입니다. 테스트 Action은 Print Number와 Clear버튼을 교차하며 클릭하였는데요, Print Number를 클릭 할 때마다 파란색인 Heap 그래프가 스파이크되고, Clear이후 사라졌습니다. 하지만 생성한 Node는 제거하지 않았기 때문에 지속적으로 증가하는 것을 알 수 있습니다.

 - 실제 개발 과정에서 해당 기능으로 체크할 때에 메모리가 소비가 지속적으로 감소되지 않는 경우에 메모리 누수를 의심해볼 수 있습니다.

 

* 병목현상 추적

 - Perfomance 탭 하단 Summary의 Doughnut Chart는 CPU사용량을 나타내며, 해당 차트는 Rendering 처리시간이 가장 긴것을 알 수 있습니다. 

 - Main Section을 펼치면 x축은 시간, y축은 동작된 이벤트를 표기합니다. 

 - 분석하고 싶은 지점을 마우스 휠로 확대할 수 있으며 해당 타임구간에 발생된 이벤트를 선택할 경우 Summary에서 이벤드가 발생된 정확한 코드 위치를 알 수 있습니다. 

 

2) Memory Graph

 가) Heap Snapshot

  - DOM트리에서 제거된 노드 중 참조에 의하여 메모리에 남아 있는 경우(detached Dom)에 Heap SnapShots에서 Detached 키워드로 필터링하여 메모리 누수를 확인할 수 있습니다. 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Memory leaks</title>
</head>
<body>
<button id="createList">createList</button>
</body>
</html>
<script>
    var detachedElement;
function createList(){
    let ul = document.createElement("ul");
    for(let i=0; i<5; i++){
        ul.appendChild(document.createElement("li"));
    }
    detachedElement = ul;
}
document.getElementById("createList").addEventListener("click", createList);
</script>

 - createdList 버튼 클릭 시, 전역변수로 인하여 참조된 상태지만 Dom에는 보여주지 않는(detached Dom) ul, li 노드를 생성하는 예제입니다. 

 - DevTools Memory -> Heap Snapshot 에서 detached 키워드로 검색할 시 확인할 수 있습니다. 

 

 나) Allocation Timeline

 - 메모리 할당 Timeline을 볼수 있습니다. 녹화 시작 후 메모리 할당 측정을 위해 라우터 전환을 반복한 뒤 메모리가 반환되지 않고 남아 있는 경우(그래프의 파란색영역)를 체크할 수 있습니다. 

 

 

참고사이트

https://blog.logrocket.com/escape-memory-leaks-javascript/

https://codingmoondoll.tistory.com/entry/%ED%81%AC%EB%A1%AC-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%8F%84%EA%B5%AC%EC%9D%98-Performance-%ED%83%AD-%EB%8B%A4%EB%A3%A8%EA%B8%B0-%EA%B8%B0%EB%B3%B8%ED%8E%B8

반응형