메모리 누수는 어플리케이션의 속도 저하 뿐만 아니라 다른 어플리케이션에게도 악영향을 끼치는 등 전반적인 문제들의 원인이 될수도 있기 때문에 개발자는 메모리 누수에 신경을 써야한다.
❓ 메모리 누수
다양한 이유로 더이상 프로그램에서 사용되지 않는 메모리를 해제하지 못해 계속적으로 메모리를 점유하는 것을 의미한다.
위에서 언급된 메모리 누수를 해결하기 위해 가바지 컬렉션의 개념이 존재한다.
c언어의 경우 개발자들이 malloc()나 free()등의 함수를 이용해서 명시적으로 메모리를 할당하고 반환한다.
Java에서는 JVM의 가비지 컬렉터가 불필요한 메모리를 알아서 정리
를 해주며 Javascript에서도 가비지 컬렉션 개념이 존재하기 때문에 이러한 불필요한 메모리를 알아서 정리해준다.
❓ 가비지 컬렉션
할당되었던 메모리가 더 이상 사용되지 않을때 자동으로 메모리가 반환되는데 이런 과정을 가비지컬렉션(Garbage Collection)이라고 한다.
하지만,
가비지 컬렉션이 메모리를 관리해준다고 해서 개발자가 메모리 관리에 신경을 쓰지 않아도 되는건 아니다.
일반적으로 전역 변수는 GC에 의해 삭제되지않는다.
function sample(x){
example = 'Inside a function'
// === window.example = 'Inside a function'
}
sample(1)
위의 경우 example 변수는 전역에 생성되고 'Inside a function'값을 할당받게 된다.
전역에 생성되었다는 것은 해당 변수가 GC에 의해 수집되지 않아 삭제되지 않음을 의미하기 때문에 전역변수의 사용은 주의하며 사용해야 한다.
기본적인 GC의 동작원리의 이해
let user = { name: "John" }; let admin = user;
GC의 동작원리의 핵심은 참조이다.
위와 같은 경우에 전역 변수 user, admin은 {name: "John"}를 참조하게 된다.
여기서 해당 값을 더이상 사용을 하지 않아도 전역변수 user와 admin이 참조하고 있어 불필요한 메모리를 계속 잡고있게 된다.이를 해결하기 위해 {name: "John"}에 대한 참조를 끊어줘야하는데 user, admin 값에 다른 값을 할당하거나 명시적으로 null을 지정하면 {name: "John"}에 대한 참조가 더이상 없기 때문에 GC가 해당 값을 수집해서 메모리 해제가 가능해지게 된다.
혼동했던 내용
GC는 stack 영역을 정리해주는게 아니라 heap 영역을 정리해준다.
Garbage Collection은 Heap 메모리를 주기적으로 체크하면서 참조 연결이 끊기고 접근할(도달할) 가능성이 사라진 객체들을 찾아내 삭제하는 역할을 합니다.
<button>start a timer</button>
<script>
function fn1() {
const largeObj = new Array(100000);
setInterval(() => {
let myObj = largeObj
}, 1000)
}
document.querySelector('button')
.addEventListener('click', function() {
fn1()
})
</script>
해당 코드는 버튼을 클릭 하게되면 largeObj라는 큰 배열을 생성하고 매초마다 largeObj를 참조하는 코드이다.
버튼을 클릭 후 fn1() 함수가 실행되고 내부적으로 코드가 한번씩 실행되고 나면 해당 함수의 실행 컨텍스트가 종료되면서 지역변수의 메모리가 해지되었을거라고 생각했다.
하지만 performance 탭을 사용하여 메모리 누수를 확인해봤는데 처음 시작된 메모리 곡선의 높이보다 종료시점에 수동으로 가비지 컬렉터를 수행을 하여도 기존의 높이보다 높아진 것을 보아 메모리 누수
가 일어나고 있는 것을 알 수 있었다.
더 이상 사용되지 않을 때 명시적으로 타이머를 제거하는 것이 중요
하다.
<button id='create'>생성</button>
<script>
// 분리된 Dom 노드
let detachedTree;
function create() {
const ul = document.createElement('ul');
for (let i = 0; i < 10; i++) {
const li = document.createElement('li');
ul.appendChild(li);
}
detachedTree = ul;
}
document.getElementById('create').addEventListener('click', create);
</script>
버튼을 클릭시 ul 태그와 10개의 li 태그가 ul 하위에 생성된다. DOM tree에는 존재하지 않아 위와 같은 상황을 DOM으로부터 ul 노드가 분리되었고 가정할 수 있다.
하지만 DOM에서 ul 노드가 분리되었지만 해당 메모리가 해제되지 않고 있는 것을 확인 할 수 있다.
(생성 버튼을 클릭 후 찍은 두번째 스냅샵으로 detached를 검색했을 때 관련 노드가 나온다는 의미는 메모리가 해제된 것이 아니다.)
위의 상황에서는 전역변수 detachedTree가 참조하고 있기 때문에 메모리가 해제하지 않은 것이다. 그래서 간단하게 함수내부로 변수를 옮겨 해결할 수 있다.
위의 상황과 같이 DOM으로부터 특정 노드를 제거하여 우리 생각처럼 그 노드의 메모리가 해제되기를 바랬지만 몇몇 코드는 여전히 삭제된 노드를 참조하고 있기 때문에 삭제를 하지 못하는 상황이 생길 수 있다.
https://developer.chrome.com/docs/devtools/memory-problems/
<button onclick="myClick()">execute fn1</button>
<script>
function fn1() {
let a = new Array(100000);
function fn2() {
let c = [1, 2, 3];
}
fn2();
return a;
}
let res = [];
function myClick() {
res.push(fn1());
}
</script>
위와 같은 코드가 있다고 가정하자.(사실 어디서 클로저가 사용되고 있는 예제인지 모르겠다.)
fn1()함수의 실행 컨텍스트가 종료될 때 변수 a는 GC에 메모리가 해제됬을거지만 fn1()함수가 a를 반환하고 전역변수 res에 할당함으로써 res는 변수 a의 값을 참조하기 때문에 메모리 누수가 일어나게 된다.
https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156
위의 클로저 문제는 최신 크롬에서는 해결했다고 한다.
결국 위에서 언급된 사항들의 공통적인 내용은 전역 변수에 값이 할당되는 것을 최대한 피해야 하는 것이며 불가피하게 전역 변수를 사용하더라도 사용하지 않을 때 null을 할당하여 GC에서 회수해갈 수 있도록 해야한다는 것이다.