본문 바로가기
개발/Javascript

[Javascript] 자바스크립트의 메모리관리

by 그레이웅 2022. 10. 17. 22:46
반응형

[Javascript] 자바스크립트의 메모리 관리

 

자바스크립트의 메모리 관리

c언어와 같은 저급 언어에서는 메모리를 관리하기 위해 개발자가 직접 메모리에 접근한다. (malloc(), free()와 같은 함수)

반면에 자바스크립트는 객체가 생성되면 자동으로 메모리를 할당해주고, 필요가 없다면 가비지 컬렉션(garbage collection)이 자동으로 메모리를 해제한다.

대부분의 고급 언어에서는 가비지 컬렉터가 존재하고 메모리를 관리해준다.

자동 메모리 관리가 존재하면, 개발자가 메모리 관리에 신경을 쓰지 않게 되고, 이 영역에 대한 이해가 부족할 수 있는 문제가 생긴다. 나도 메모리 관리 영역에 대해선 잘 알지 못하기 때문에 이번 포스팅을 쓴다.

 

 

메모리의 생존 주기

어떤 언어를 사용하던 메모리 생명주기는 거의 비슷하다.

 

1. 메모리 할당(allocatie) 

 

 프로그램을 사용할 수 있도록 운영체제(OS)가 메모리를 할당한다. C언어와 같은 저급 언어는 개발자가 이를 명시적으로 처리해줘야 한다. 자바스크립트 같은 고수준 언어는 암묵적으로 할당해준다.

 

2. 메모리 사용(use)

 할당된 메모리를 실제로 사용하는 단계이다. 개발자가 설정한 변수를 읽거나 쓰며 사용한다.

 

3. 메모리 해제(release)

 프로그램에서 필요하지 않은 메모리 전체를 돌려준 뒤 다시 사용할 수 있는 상태가 되도록 만들어준다. 메모리 할당 작업처럼 저 수준 언어에서는 이를 명시해야 되지만, 자바스크립트 같은 고수준 언어는 암묵적으로 해제해준다.

 

- 저급언어 : 컴퓨터가 이해하기 쉬운 이진법으로 이루어진 언어, 기계어와 어셈블리어가 있다.
- 고급언어 : 사람이 이해하기 쉽게 작성된 프로그래밍 언어이다. 

※ C언어는 고급 언어와 저급 언어의 특징을 모두 가지고 있다.
하지만 내부적으로 메모리 측면에서 개발하기 때문에 여기선 저급 언어로 생각 한 것 같다.

 

 

자바스크립트에서의 메모리 할당

자바스크립트에서는 메모리 할당이 다음과 같이 작동한다. 자바스크립트는 값을 선언하면 자동적으로 메모리를 할당하게 된다.

 

var num = 111; // 숫자에 대한 메모리 할당
var str = "홍길동" // 문자에 대한 메모리 할당


var obj = { 	//Object와 내부의 값을 위한 메모리 할당
	no : 1,
	name : "김철수"
}

var arr = [1, "123" , "arr2"]; // 배열과 배열의 값에 대한 메모리 할당

function func(a) { 	//함수를 위한 메모리 할당(함수는 호출 할 수 있는 객체)
	return a + 1;
}

// 함수 표현식 또는 객체를 위한 할당
someElement.addEventListener('click', function(){
  someElement.style.backgroundColor = 'blue';
}, false);

 

아래와 같은 몇 개의 함수 호출은 객체 할당의 결과를 가져온다.

 

var date = new Date(); // Date객체 할당
var d = document.createElement('div'); // DOM 요소 할당

 

메서드는 새로운 값이나 오브젝트를 할당하기도 한다.

 

var name = '홍길동123';
var newName = name.substr(0, 3); // s2는 새로운 문자열이 됨
// 문자열은 불변(immutable)이므로 자바스크립트는 메모리를 할당하지 않고 
// [0, 3]의 범위만 저장할 수도 있음

var arr1 = ['str1', 'str2'];
var arr2 = ['str3', 'str4'];
var arr3 = arr1.concat(arr2);

// 4개의 요소를 가진 새로운 배열은
// arr1과 arr2 요소의 연결이 됨

 


자바스크립트에서의 메모리의 사용

자바스크립트에서 할당된 메모리를 사용한다는 말은 메모리를 읽어서 그 저장된 값을 읽거나 쓴다는 의미를 뜻한다.

객체, 변수, 함수의 인자를 넘겨줄 때 등에서 메모리의 사용이 일어난다.

 

 


자바스크립트의 메모리의 해제

메모리 해제의 단계는 대부분의 메모리 관리 문제가 일어나는 시점이다.

 

메모리의 해제의 단계에서 이미 선언한 변수나 함수 등에서 할당된 메모리가 필요 없어진 때를 알아내기가 힘들기 때문에 대부분의 문제가 발생한다.

 

그래서 자바스크립트와 같은 고수준의 언어는 가비지 컬렉터(garbage collector , GC)라는 소프트웨어를 내장하여 메모리 할당을 추적 및 더 이상 사용하지 않는 메모리를 파악해 자동으로 반환해준다.

 

가비지 컬렉터(garbage collector , GC)는 어떤 메모리를 가리키는 모든 변수가 스코프를 벗어날 때처럼 더 이상 접근 불가능한 메모리를 수집한다. 하지만 이러한 과정은 추정에 기반하고, 메모리의 일부가 필요할지 알아내는 것은 결정 불가능(undecidable)한 문제이기 때문이다.

그래서 가비지 컬렉터(garbage collector , GC)는 이 문제에 대해 제한적인 해결책만 제시한다.

가비지 컬렉터(garbage collector , GC)가 제시하는 제한적인 해결책들은 무엇일까?

 

 

가비지 컬렉터(garbage collector , GC)의 개념

 

메모리 참조

 

가비지 컬렉터(garbage collector , GC)의 알고리즘이 의존하는 주요 개념은 참조(reference)이다.

메모리 관점에서 볼 때 어떤 객체가 다른 객체에 접근을 한다면, 참조한다고 말한다.

예를 들면 A가 B객체에 메모리를 통해 접근할 수 있다면 "B는 A에 참조된다"라고 할 수 있다.

자바스크립트 객체는 자신의 프로토타입(prototype)에 대한(암묵적) 참조를 가지고 있으며, 자신의 속성 값에 대한 (명시적) 참조도 가지고 있다.

 

이런 관점에서 '객체'는 자바스크립트의 함수 범위와 함수 스코프(Function Scope), 글로벌 렉시컬 스코프(lexical Scope)까지도 포함된다.

 

렉시컬 스코프(lexical scope)는 변수 이름이 중첩된 함수에서 해석되는 방식을 정의한다.
중첩된 함수는 부모의 함수의 스코프를 포함하고 있다.

//부모 함수
function a() {
	
    var a = 0;
    
    //자식 함수
    function b() {
    	console.log(a);
    }
}

 

 

참조 횟수 계산

다음 예제는 가장 단순한 형태의 가비지 컬렉션 알고리즘이다.

객체는 만약 가리키는 참조가 하나도 없는 경우가비지 컬랙션 대상(garbage collectible)으로 간주한다.

 

var obj1 = {
  obj2: {
    x: 1
  }
};


// 두 객체가 생성됨
// 'obj2'는 'obj1'이 자신의 속성으로서 참조함
// 둘 다 가비지컬렉션 될 수 없음


var obj3 = obj1; // 'obj3' 변수는 'obj1'이 가리키는 오브젝트에 대한 참조를 갖는다.
obj1 = 1;      // 1을 할당함으로써 이제 'obj1'에 있던 객체는 하나의 참조만 남게 되고
             // 'obj1'은 'obj3' 변수에 들어 있음
             
var obj4 = obj3.obj2; // 'obj2' 속성에 대한 참조
                // 이제 이 객체는 두개의 참조를 가짐. 하나는 속성으로서 
                // 다른 하나는 'obj4' 변수로서
                
                
obj3 = '374'; // 원래 'obj1'에 있던 객체는 이제 참조를 하는 곳이 없음 
			// obj1 = 1과 obj3= '374'로 할당을 하였기때문에 객체는 사라짐
            // 따라서 가비지컬렉션 될 수 있음
            // 하지만 'obj2' 속성은 'obj4' 변수가 참조하므로 가비지컬렉션 될 수 없음
            
obj4 = null; // 원래 'obj1'객체 내에 있던 'obj2'속성은 이제 참조하는 곳이 없으므로
           // 가비지컬렉션 될 수 있음

 

 

순환 참조 때문에 생기는 문제

순환 때문에 생기는 문제들이 있다.

아래 예제는 두 객체가 만들어지고 서로를 참조하므로 순환 참조가 생성된다.

순환 참조는 두 객체가 서로를 참조하는 것이라고 보면 된다.

function f() {
  var obj1 = {};
  var obj2 = {};
  obj1.p = obj2; // obj1은 obj2를 참조함
  obj2.p = obj1; // obj2는 obj1을 참조함. 이를 통해 순환 참조가 만들어짐.
}
f();

 

이 두 객체는 함수 호출 뒤에는 스코프를 벗어나게 되므로 실질적으로는 쓸모가 없게 돼버린다.

이들이 차지하고 있던 메모리는 반환되어야 하지만, 위에서 설명했던 참조 횟수 계산 알고리즘으로 설명한다면, 

두 객체가 서로 한 번씩 참조(reference)한 것으로 간주되었기 때문에 두 객체 다 가비지 컬렉션 될 수 없다.

 

 

마크 스위프 알고리즘

마크 스위프 알고리즘은 객체가 필요한지 결정하기 위해 해당 객체에 닿을 수 있는지(reachable) 여부를 판단한다.

 

마크 스위프 알고리즘은 다음 세 단계를 거친다.

 

1. 루트(Root) : 일반적으로 루트는 코드에서 참조되는 전역 변수이다. 예를 들면 자바스크립트에서 루트로 동작할 수 있는 전역 변수는 window 객체이다. Node.js에서 루트로 동작하는 객체는 global이다. 가비지 컬렉터모든 루트의 완전한 목록을 만들어 낸다.

 

2. 모든 루트의 목록을 만든 모든 루트와 자식들을 검사해 활성화 여부를 표시한다. 활성 상태가 아니면 가비지 컬렉션 되지 않으며 루트가 닿을 수 없는 것들은 가비지 컬렉션 된다.

 

3. 가비지 컬렉터는 활성으로 표시되지 않은 모든 메모리를 OS에 반환한다.

 

마크 스위프 알고리즘을 그려보았다.

직접 그려보았는데 루트와 연결해서 활성화되지 않는 객체들은 닿을 수 없기 때문에 반환한다고 이해하였다.

 

2012년 기준의 브라우저들은 마크 스위프 가비지 컬렉터를 장착하였다. 지금 현재 2022년에 와서의 가비지 컬렉터의 변화도 따로 포스팅해보아야겠다.

 

마크 스위프 알고리즘을 이용하면 위의 순환 참조에 의해서 생기는 문제는 해결 가능하다. 위의 순환 참조의 예시처럼 두 객체는 함수의 호출 뒤 더 이상 전역 객체로부터 닿을 수 없기 때문에 참조하는 대상이 아니게 되어버린다. 가비지 컬렉터 입장에서도 닿을 수 없는 것이 된다.

 


※ 2019년 이후 JavaScirpt는 명시적 또는 프로그래밍 방식으로 가비지 컬렉션을 작동할 수는 없다.

 

Node.js를 이용한 메모리 문제 해결

 

v8 엔진 플래그

node --max-old-space-size=6000 index.js

사용 가능한 힙 메모리의 최대양을 다음과 같은 명령어로 늘릴 수 있다.

예전 React프로젝트를 빌드 할 때 힙 메모리가 부족하여 이 명령어를 수행한 적이 있다. 프로젝트 크기가 커지고 빌드시 현재 사용 메모리 양이 많이 늘어나게되면 컴퓨터가 과부하 되고 heap out of memory란 표시를 본적이있다.

 

그 때의 그 경험이 지금의 포스팅과 연관이 되는 것이 신기하다.

가비지 컬렉터의 알고리즘에 대해서도 좀 더 자세히 포스팅 해보아야겠다.

 

 

 

도움이 된 글

 

- 자바스크립트 메모리는 어떻게 관리되는가?

- mdn자바스크립트의 메모리관리

반응형

'개발 > Javascript' 카테고리의 다른 글

[JavaScript] Event Loop(이벤트 루프)  (0) 2022.10.07
[JavaScript] 호이스팅(Hoisting)  (0) 2022.10.06
[JavaScript] 클로저(Closure)  (1) 2022.10.06
WebStorage API  (0) 2022.09.30
Babel과 polyfill  (1) 2022.09.29

댓글