혹시 C언어같은 저 수준
의 언어를 사용해 본 적이 있는가?
여기서 저 수준
이란, 질이 나쁘다는게 아니고 Low-level : 기계친화적 언어
를 의미한다.
이 같은 언어들은 개발자가 직접 메모리를 할당하고, 참조하고, 관리한다.
여기까지 읽어보면 Javascript
언어가 저 수준
언어가 아니라는 것 쯤은 눈치챘을 것이다.
우리는 개발하며 메모리를 할당시켜주고, 해제시켜주는 작업을 직접 하지 않기 때문이다.
여기서 하나의 의문점이 생기게 된다.
그럼 대체 자바스크립트는 어떻게 메모리를 관리하는거지? 🤔
이런 의문점이 들었다면 벌써 반은 왔다.
대부분의 주니어 개발자들은 Javascript
의 동작 원리가 아닌 당장의 사용법만 갈구하기 때문에 이런 부분을 놓치기 쉽다. (나포함)
이번에는 Javascript
의 메모리 관리 방법(feat.가비지 컬렉터)에 대해 알아보도록 하자.
메모리
라는 단어는 흔히 들어보았을 것이다.
쉽게 말해 저장 공간
인데, 프로그램이 돌아가며 저장해야하는 수많은 일련의 정보(데이터)
를 저장하는 공간이라고 생각하자.
알다시피 메모리
는 유한하다.
컴퓨터 조립을 해 본 사람은 알겠지만, 부품 하나 추가 할 때 마다 드는게 돈이다.
우리는 돈이 없기에 얼마 없는 메모리에서 최고의 효율을 뽑아내야 한다.
그리고 최고의 효율을 뽑아내려면 관리해야 할 것이다.
앞서 말했듯이, 저 수준
의 언어는 개발자가 직접 메모리를 관리한다.
다행히 Javascript
는 고 수준
언어로 자체적으로 메모리를 관리해준다.
이쯤 되면 의아한 사람이 있을 것이다.
아니 자바 스크립트가 자동으로 관리해주면 굳이 알 필요가 있나요? 🤨
단순히 코드의 작동을 목적으로 한다면 충분히 일리가 있는 말이다.
하지만 우리는 단순 작동 코드가 목표가 아닌, 최적화된 작동 코드를 목표로 한다.
따라서 Javascript
가 어떻게 메모리를 관리하는지 알고, 메모리 누수가 되는 부분이 없는지도 확인 할 수 있어야 한다.
MDN에서 메모리의 생존 주기는 다음과 같이 정의한다.
- 필요할때 할당한다.
- 사용한다. (읽기, 쓰기)
- 필요없어지면 해제한다.
이것을 Javascript
에서는 어떻게 하는지 한번 알아보자.
Javascript
에서는 값을 선언할 때 자동으로 메모리를 할당한다.
// 문자열 메모리 할당됨
const testStringValue = 'test';
// 정수 메모리 할당됨
const testNumberValue = 123;
// 객체 및 객체에 포함된 값 메모리 할당됨
const testObjectValue = {
testOne: 'hello',
testTwo: 'world',
};
// 배열 및 배열에 포함된 값 메모리 할당
const testArrayValue = [1, 'hello', null];
// 함수를 위한 할당 (호출 가능한 객체)
function testNormalFunction(a){
return a+4;
}
// 위 함수와 동일하게 할당
const testArrowFunction = (a) => {
return a+4;
}
Javascript
에서 값을 사용하는 모든 일련의 과정들을 일컫는다.
// 메모리 할당
const testString = 'hello world';
// 메모리 사용 (읽기)
console.log(testString);
Javascript
는 고 수준
언어로, 가비지 컬렉션 : GC
를 채택했다.
이는 가비지 컬렉터
라는 프로그램이 자동으로 메모리를 관리해준다.
가비지 컬렉션
은 엔진이 자동으로 수행하기 때문에, 개발자가 억지로 실행하거나 막을 수 없다.
-> 20221127 추가
엄밀히 따지자면 가능하긴 한 것 같다. (해보지는 않음)
하지만 답변에도 있듯이 상식적인 행위는 아닌 듯 하니 참고만 하자...
가장 중요한 부분이다.
다시 앞선 내용을 복기해보자.
- 메모리는 '필요 없어졌을 때' 해제한다.
Javascript
는 메모리 관리(해제 포함)을가비지 콜렉터
가 자동으로 해준다.- 결국 메모리 해제 작업은
가비지 콜렉터
가 진행 한다.
여기서 궁금증이 하나 더 생긴다.
그럼 대체 '필요 없어졌을 때' 의 기준이 뭐지? 😅
이 부분이 가장 중요한, 가비지 콜렉터
가 어떤 알고리즘으로 메모리를 해제시키는지에 대한 내용이다.
가비지 컬렉션
의 주요 알고리즘 중 두가지를 소개해보려 한다.
가비지 컬렉션
알고리즘의 핵심 개념은 참조
다.
참조의 정의는 아래와 같다.
A
라는 메모리를 통해 (명시적 혹은 암시적으로)B
라는 메모리에 접근할 수 있을 때
"B
는A
에 참조된다."라고 할 수 있다.
(실제로 현재 Javascript
에서는 사용하지 않는 알고리즘으로, 참고차 가볍게 읽어보자.)
참조-세기
알고리즘은 다음과 같이 간단하게 구성되어 있다.
더 이상 필요 없는 오브젝트(정보) = 어떤 다른 오브젝트도 참조하지 않는 오브젝트 = 가비지
예시를 보며 알아보자.
let firstValue = {
a : {
b : 2
}
};
let secondValue = firstValue;
(빨간 화살표를 참조 표시로 가정)
- 2개의 오브젝트가 생성되었다.
- 이때
b
오브젝트는a
오브젝트의 속성으로참조
된다.b
오브젝트는FirstValue
변수에 할당되었다.secondValue
변수는 위 오브젝트를참조
하는 두 번째 변수이다.- 현재 상황에서 가비지 컬렉션이 수행 될 메모리는 없다.
여기까지 이해되었다면 다음 상황을 보자.
firstValue = 1;
firstValue
의참조
가 변경되었다.- 이제
secondValue
만 유일하게 오브젝트를참조
한다.
기존에는 firstValue
와 secondValue
가 오브젝트를 참조했으나, firstValue
의 값이 1
로 변경되며 secondValue
만 오브젝트를 참조하는 모습이다.
let newValue = secondValue.a;
- 새로운 변수
newValue
가a
속성을 참조했다.- 이제
secondValue.a
는secondValue
가 속성으로 참조하고,newValue
변수가 참조한다.
secondValue = 2;
secondValue
의참조
가 변경되었다.- 이제 '
secondValue
가참조
하던 오브젝트'를참조
하는 오브젝트는 없다
유일한 참조
변수 secondValue
에 newString
을 대입해 오브젝트의 참조
가 없어졌다.
그럼 이제 가비지 컬렉션이 수행될까?
정답은 아니다.
기존 오브젝트의 a
속성은 현재 newValue
가 참조하고 있기 때문이다.
따라서 아직 참조하는 데이터가 때문에 필요없는 데이터로 분류되지 않는다.
newValue = 'bye~';
newValue
의참조
가 변경되었다.- 이제 가장 처음
firstValue
가 참조한 오브젝트를 참조하는 다른 변수는 존재하지 않는다.참조하는변수X = 필요없는변수
이므로 가비지 컬렉션이 실행된다.
참조가 모두 사라지며 드디어 가비지 컬렉션
이 실행되었다.
처음엔 조금 헷갈릴 수 있지만, 한 단계씩 차근차근 짚어나가면 충분히 이해 할 수 있는 내용이다.
앞서 알아본 참조-세기
알고리즘은 겉보기에는 완벽하지만, 치명적인 한계가 있어 지금은 Javascript
에서 사용되지 않는다.
바로 순환참조를 다루는 한계이다.
예시와 함께 어떤건지 알아보자.
function test() {
let firstValue = {};
let secondValue = {};
// 순환참조 구간
firstValue.a = secondValue;
secondValue.a = firstValue;
return 'complete';
}
test();
현재 firstValue
와 secondValue
가 서로 속성으로 참조하는 순환 구조를 보이고 있다.
정상적으로는 test()
라는 함수가 실행되어 return
으로 종료되면 firstValue
와 secondValue
는 제거 되어야하지만, 실제로 그렇지 않다.
어쨌든 서로를 참조
하고 있기 때문에 필요없는 오브젝트
로 판단되지 않기 때문이다.
이같은 현상은 메모리 누수를 야기시켰고, 이를 보완하고자 현재는 다른 알고리즘을 사용중이다.
현재 Javascript
에서 사용중인 가비지 컬렉션 알고리즘은 이것이다.
표시하고-쓸기
알고리즘은 필요 없는 오브젝트
를 다르게 정의한다.
더 이상 필요 없는 오브젝트(정보) = 닿을 수 없는 오브젝트
도달 가능성(reachability)
이라고 하는 개념을 사용해 도달 가능한(reachable)
값을 판별하는 알고리즘이다.
대체 어디서부터 무엇에 닿는다는 말인지 한번 알아보자.
해당 알고리즘은 roots
라는 오브젝트의 집합을 가지고 있다.
이 roots
에 있는 오브젝트 root
에서 어떻게든 접근이 가능하다면 필요한 오브젝트라고 판단한다.
root
의 대표적인 종류는 아래와 같다.
- 현재 함수의 지역 변수와 매개변수
- 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
- 전역 변수
예시를 보며 이해해보도록 하자.
let user = {
name : "joohyun"
};
전역변수 user
가 {name:"joohyun"}
오브젝트를 참조하고 있는 모습이다.
만약 여기서 user
의 값을 변경하면 어떻게 될까?
user = "himprover";
그림처럼 전역변수 user
의 값은 "himprover"
로 변경되었고, 화살표가 사라졌다.
이제 {name:"joohyun"}
오브젝트에 접근할 방법이 없어진 것이다.
이때 가비지 컬렉션이 실행되며 {name:"joohyun"}
오브젝트를 삭제한다.
잘 이해가 되지 않는다면 각각의 하늘에 떠있는 섬이 있고, 화살표가 다리라고 생각하자.
가장 높은 섬에서부터 사람이 내려가는데, 다리가 연결되어 있지 않아 사람이 갈 수 없는 섬이 있다면 없애버리는 것이다.
위 예제는 매우 간단한 경우라서, 좀 더 복잡한 구조를 예시로 들어본다.
가족관계를 구성하는 객체가 있다고 가정해보자.
let family = {
father : {
name : 'man',
},
mother : {
name : 'woman',
}
}
family.father.wife = family.mother;
family.mother.husband = family.father;
조금 복잡하지만, 현재는 모든 객체에 도달이 가능한 상태이다.
만약 여기서 참조를 두 개 지운다고 가정해보자.
delete family.father;
delete family.mother.husband;
언뜻 보기에는 연결되어 있는 것 같다.
하지만 여기서 잊지 말아야 할 것은 root
에서 도달 할 수 있는지의 여부다.
<global>
에서 부터 그려보면 도달하지 못한다는 것을 알 수 있다.
따라서 빨간색 배경으로 분류된 {name:"man"}
은 가비지 컬렉션이 실행되어 제거된다.
(가비지 컬렉션 실행 결과)
다시 처음 가족 관계 오브젝트를 생성한 시점으로 돌아가보자.
아래 코드를 실행하면 어떻게 될까?
family = null;
family
의 값이 null
이 되면서 기존의 오브젝트 들이 길을 잃었다.
이 빨간색 사각형 부분을 도달할 수 없는 섬
이라고 한다.
사실 섬 안에서는 서로 도달할 수 있지만, root
에서의 길이 없기 때문에 섬 전체가 가비지 컬렉션
에 의해 제거되는 것이다.
이처럼 단순히 연결이 되어 있는것이 중요한 게 아닌, root
에서부터 도달할 수 있는지가 중요한 관점이다.
가비지 컬렉션
의 개념은 모던 자바 스크립트에서 탄생한 것은 아니다.
그러나, 시멘틱태그처럼 이 정도는 당연히 알고 그 다음에 모던 자바 스크립트를 건드리는게 맞다고 판단해 같은 시리즈에 넣었다.
어떤 언어이든 그냥 쓰는 것이 아닌, 어떻게 동작하는지 한번 더 생각해보고 사용하는 습관을 들여보자.
Javascript
는 메모리 생명주기를 가비지 컬렉션
을 통해 자동으로 관리해준다.표시하고-쓸기
알고리즘을 사용한다.가비지 컬렉션
은 엔진이 자동으로 수행하므로, 개발자가 억지로 실행하거나 막을 수 없다.+ 읽어주셔서 감사합니다.
+ 오타, 내용 지적, 피드백을 환영합니다. 많이 해주실 수록 제 성장의 밑거름이 됩니다.
자바스크립트의 가비지 컬랙션에 대해서 궁금했었는데 덕분에 이해할 수 있었습니다
감사합니다! :)