일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- c++
- 애플
- db
- 배포
- ECMAScript6
- 식물키우기
- 알고리즘
- LeetCode
- string
- binary
- git
- 맛집
- 자바스크립트
- 리액트
- ECMAScript2015
- 운영체제
- 정규표현식
- 반응형 웹
- styled-components
- 아이폰
- 컴퓨터
- java
- Algorithm
- codewars
- ES6
- Javascript
- 기억장치
- react
- 자바
- 데이터베이스
- Today
- Total
에브리 저장소
[JavaScript] 메모리 관리와 가비지 컬렉션 동작 방식 본문
최근 면접 스터디를 진행하며 받은 질문 중, 클로저를 사용할 때 주의해야 할 점에 대해 질문을 받았으나
선뜻 대답하지 못했습니다. 😢
이에 메모리 누수를 주의해야 한다.라는 피드백을 받고 알아보니 클로저인 내부 함수가 참조하는 외부 함수의 객체들을 더 이상 사용하지 않아도 가비지 콜렉터가 수집하지 못한다는 내용이 있었습니다.
키워드만 정리해놓고 잊고 있었는데, 다른 분께서 자바스크립트의 가비지 콜렉터 동작 방식에 대해 내용이 있으면 좋을 것 같다는 피드백을 주셨고, 이를 계기로 정리해서 공유합니다.
틀린 내용 지적 등의 피드백은 언제나 감사히 받겠습니다.
참고
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management
- https://www.lambdatest.com/blog/eradicating-memory-leaks-in-javascript/
- https://www.geeksforgeeks.org/mark-and-sweep-garbage-collection-algorithm/
메모리 생명 주기
- 필요한 메모리 할당
- 할당된 메모리 사용(읽기, 쓰기)
- 해당 메모리가 필요 없어지면 해제
프로그래밍 언어에 상관없이 메모리 생명 주기는 위 과정으로, 항상 동일합니다.
하지만 1, 3번을 시행함에 있어 로우 레벨 언어와 하이 레벨 언어에서 차이가 있습니다.
자바스크립트는 하이 레벨 언어에 속합니다.
자바스크립트에서의 메모리 할당
값 초기화
프로그래머가 일일이 메모리 할당을 하지 않도록 하기 위해, 자바스크립트는 값을 초기화 할 때 자동으로 메모리를 할당합니다.
var n = 123; // 정수를 담기 위한 메모리 할당
var s = 'azerty'; // 문자열을 담기 위한 메모리 할당
// 객체와 객체에 포함된 값들을 담기 위한 메모리 할당
var o = {
a: 1,
b: null
};
// 배열과 포함된 값들을 담기 위한 메모리 할당
var a = [1, null, 'abra'];
// 함수(호출 가능한 객체)를 담기 위한 메모리 할당
function f(a) {
return a + 2;
}
// 함수 표현식 또한 객체를 담기 위한 메모리를 할당
someElement.addEventListener('click', function () {
someElement.style.backgroundColor = 'blue';
}, false);
함수 호출을 통한 메모리 할당
일부 함수들을 호출하면 객체 할당이 발생합니다.
var d = new Date(); // Date 객체 할당
var e = document.createElement('div'); // DOM element 할당
일부 메서드에서도 새로운 값이나 객체를 할당합니다.
var s = 'azerty';
var s2 = s.substr(0, 3);
// s2는 새로운 문자열입니다.
// 문자열은 불변값(immutable)이기 때문에
// 자바스크립트는 메모리를 새로 할당하지 않고,
// [0, 3]의 범위를 저장합니다.
var a = ['ouais ouais', 'nan nan'];
var a2 = ['generation', 'nan nan'];
var a3 = a.concat(a2); // 4개의 원소를 가진 새로운 배열
값 사용
값을 사용한다는 의미는 할당된 메모리를 쓰거나 읽는다는 것을 의미합니다.
변수나 객체 속성값을 읽고 쓸 때, 함수 호출 시 함수에 인자를 넘길 때 값을 사용하는 것을 의미합니다.
할당된 메모리가 더 이상 필요 없을 때 해제
할당된 메모리를 해제하려면 할당된 메모리가 더 이상 필요하지 않은 시기를 결정해야 하는데, 이를 잘못하면 메모리 누수가 발생하게 됩니다. 그러므로 메모리 누수 문제는 할당한 메모리를 적절히 해제하지 못해서 발생한다고 볼 수 있습니다.
C, C++과 같은 로우 레벨 언어에서는 개발자가 메모리 해제 시기를 직접 결정해야 합니다. 프로그램에서 할당된 메모리가 더 이상 필요하지 않을 때, 직접 메모리 할당을 해제해야 합니다.
자바스크립트와 같은 몇몇 하이 레벨 언어에서는 가비지 컬렉션(GC)이라는 자동 메모리 관리 형식을 활용합니다. 가비지 콜렉터의 목적은 메모리 할당을 모니터링하고 할당된 메모리의 블록이 더 이상 필요하지 않은 시점을 확인하여 회수하는 것입니다.
가비지 콜렉터는 항상 필요 없어진 메모리만을 해제하지만 항상 필요 없어진 모든 메모리를 모두 다 해제한다고 기대할 수 없기 때문에, 안전하지만 완전하지 않다고 생각할 수 있습니다. 이러한 이유로 가비지 컬렉션을 근사적인 작업(approximation)이라고도 합니다.
가비지 콜렉션
이 파트에서는 두 개의 가비지 컬렉션 알고리즘과 한계점을 알아봅니다.
참조 (Reference)
가비지 콜렉션 알고리즘의 핵심 개념은 참조입니다. A라는 메모리를 통해 (명시적이든 암시적이든) B라는 메모리에 접근할 수 있다면 "B는 A에 참조된다."라고 이야기합니다. 예를 들어 자바스크립트에서 모든 객체는 prototype 객체를 암시적으로 참조하고, 그 객체의 속성을 명시적으로 참조합니다.
참조-세기 (Reference-counting) 가비지 컬렉션
이 알고리즘은 "더 이상 필요 없는 객체"를 "어떤 다른 객체도 참조하지 않는 객체"라고 정의합니다. 특정 객체를 참조하는 객체가 하나도 없다면, 그 객체에 대해 가비지 컬렉션을 수행합니다.
예제
var o = {
a: {
b: 2
}
};
위 코드에는 두 개의 객체가 있습니다. 한 객체는 다른 객체의 속성(a)으로 참조되고 있습니다.
그리고 그를 감싼 객체를 변수(o)에 할당했습니다.
여기서 각 객체는 모두 참조되고 있기 때문에 가비지 컬렉션이 수행될 객체는 없습니다.
이어서 아래 코드를 봅시다.
var o2 = o;
o = 1;
// 'o2' 변수는 객체를 참조하는 두 번째 변수입니다.
// 그 후 o에 다른 값을 저장하여, 이제 객체를 참조하는 변수는 o2가 유일합니다.
var oa = o2.a;
// 위의 객체의 'a' 속성을 oa가 참조합니다.
// 이제 'o2.a'는 두 개의 참조를 가진다. 'o2'가 속성으로 참조하고 있고, 'oa'라는 변수가 참조하고 있습니다.
o2 = "yo";
// 참조하는 유일한 변수였던 o2에 다른 값을 대입했고,
// 이제 맨 처음 'o' 변수가 참조했던 객체를 참조하는 객체는 존재하지 않습니다.
// 하지만 그 객체에 대하여 가비지 콜렉션이 수행 되지 않을 것입니다.
// 객체의 'a' 속성이 여전히 'oa' 변수에 의해 참조되므로 메모리를 해제할 수 없습니다.
oa = null;
// 'oa' 변수에 다른 값을 할당했고,
// 이제 맨 처음 'o' 변수가 참조했던 객체를 참조하는 다른 변수는 없으므로 가비지 콜렉션이 수행됩니다.
한계: 순환 참조
이 알고리즘은 두 객체가 서로를 참조하면 문제가 발생합니다. 두 객체 모두 더 이상 사용하지 않더라도 가비지 컬렉션을 수행할 수 없게 됩니다.
function f() {
var x = {};
var y = {};
x.a = y; // x의 속성 a가 y에 담긴 객체를 참조합니다.
y.a = x; // y의 속성 a가 x에 담긴 객체를 참조합니다.
return 'azerty';
}
f();
위 코드에서 f 함수가 종료되고 나면, x, y에 저장한 객체는 사용되지 않으므로 가비지 컬렉션이 수행되어야 합니다. 그러나 Reference-counting(참조-세기) 알고리즘에서는 두 객체 모두 참조를 가지고 있기 때문에 가비지 콜렉션이 수행되지 않습니다.
😱
Mark-and-sweep 알고리즘
이 알고리즘에서는 "더 이상 필요 없는 객체"를 "닿을 수 없는 객체"로 정의합니다. 이름에서 알 수 있듯이 무엇인 가에 표시(Mark)를 하고, 정리하는(Sweep) 알고리즘입니다.
이 알고리즘은 roots라는 객체의 집합을 가지고 있습니다.(자바스크립트에서는 전역 변수들을 의미합니다.)
주기적으로 가비지 콜렉터는 roots로부터 시작하여 roots가 참조하는 객체들, roots가 참조하는 객체가 참조하는 객체들을 접근할 수 있는 객체라고 표시합니다. 그 후, 접근할 수 없는 객체에 대해 가비지 컬렉션을 수행합니다.
이 알고리즘은 "참조되지 않는 객체"는 모두 "접근할 수 없는 객체"이지만 역은 성립하지 않기 때문에 참조-세기 알고리즘보다 효율적이라고 할 수 있습니다.
2012년 기준, 모든 최신 브라우저들이 가비지 컬렉션에서 Mark-and-sweep 알고리즘을 사용한다고 합니다. 그 후로도 연구되고 있는 자바스크립트 가비지 콜렉션 알고리즘들은 대부분 이 알고리즘을 연구하여 개선한 것이라고 합니다. 그리고 개선된 알고리즘들도 여전히 "더 이상 필요 없는 객체"를 "닿을 수 없는 객체"로 정의하고 있습니다.
순환 참조 문제 해결
참조-세기 알고리즘에서 문제가 됐던 위 코드를 다시 보겠습니다.
function f() {
var x = {};
var y = {};
x.a = y;
y.a = x;
return 'azerty';
}
f();
위 코드에서 함수 f가 리턴되고 나면, 전역 변수들에서 x, y에 담긴 객체들에 접근할 수 있는 방법이 없습니다. 따라서 두 객체에 대해 가비지 컬렉션이 수행될 수 있습니다.
😄
동작 방식
이 알고리즘은 아래 두 단계로 작동합니다.
- Mark
객체가 생성될 때마다 mark bit가 0 (false)로 설정됩니다.
Mark 단계에서 모든 접근 가능한 객체의 mark bit가 1 (true)로 설정됩니다.
- Sweep
Mark 단계 후에 mark bit가 여전히 0 (false)로 설정된 객체들은 도달할 수 없는 객체이므로 가비지 콜렉터가 수집해 메모리에서 해제됩니다.
자바스크립트에서 메모리 관리를 가비지 콜렉터가 한다. 정도의 개념만 알고 있었는데 이번 기회를 통해 가비지 콜렉터의 동작 방식 및 메모리 누수가 발생할 수 있는 상황 등에 대해 개략적으로 공부해 보았습니다.
면접 스터디를 진행하며 간과하고 지나갔던 개념들에 대해 다시 한번 짚어보고 넘어갈 수 있게 되어 좋은 경험을 할 수 있었습니다.
끝까지 읽어주셔서 감사합니다.
참고
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management
- https://www.lambdatest.com/blog/eradicating-memory-leaks-in-javascript/
- https://www.geeksforgeeks.org/mark-and-sweep-garbage-collection-algorithm/
'Front-end > JavaScript' 카테고리의 다른 글
[JavaScript] 정규표현식 - 문자열에 한글 있는지 검사하기 (9) | 2019.03.25 |
---|---|
[JavaScript] ES6 Arrow function(화살표 함수)에 대해 알아보자!! (0) | 2019.02.28 |
[JavaScript] ES6 템플릿 리터럴에 대해 알아보자!! (8) | 2019.02.27 |
[JavaScript] ES6 블록 레벨 스코프에 대해 알아보자!! (feat.let, const) (0) | 2019.02.26 |