∮explotación≒ 개발

JavaScript 메모리 관리

TipoAzul 2023. 4. 17.
반응형

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

C 언어같은 저수준 언어에서는 메모리 관리를 위해 malloc()  free()를 사용합니다. 반면, 자바스크립트는 객체가 생성되었을 때 자동으로 메모리를 할당하고 더 이상 필요하지 않을 때 자동으로 해제합니다(가비지 컬렉션). 이러한 자동 메모리 관리는 잠재적 혼란의 원인이기도 한데, 개발자가 메모리 관리에 대해 고민할 필요가 없다는 잘못된 인상을 줄 수 있기 때문입니다.

메모리 생존주기

메모리 생존주기는 프로그래밍 언어와 관계없이 비슷합니다.

  1. 필요할 때 할당합니다.
  2. 할당된 메모리를 사용합니다. (읽기, 쓰기)
  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 엘리먼트를 위해 메모리를 할당

메소드가 새로운 값이나 오브젝트를 할당하기도 합니다.

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);
// a 와 a2 를 이어붙여, 4개의 원소를 가진 새로운 배열

값 사용

값 사용이란 기본적으로는 할당된 메모리를 읽고 쓰는 것을 의미합니다. 변수나 객체 속성의 값을 읽고 쓰거나 함수 호출 시 함수에 인수를 전달하여 수행 할 수 있습니다.

할당된 메모리가 더 이상 필요없을 때 해제하기

이 단계에서 대부분의 문제가 발생합니다. "할당된 메모리가 더 이상 필요없을 때"를 알아내기가 어렵기 때문입니다.

저수준 언어에서는 메모리가 필요없어질 때를 개발자가 직접 결정하고 해제하는 방식을 사용합니다.

자바스크립트와 같은 고수준 언어들은 "가비지 콜렉션(GC)"이라는 자동 메모리 관리 방법을 사용합니다. 가비지 콜렉터의 목적은 메모리 할당을 추적하고 할당된 메모리 블록이 더 이상 필요하지 않게 되었는지를 판단하여 회수하는 것입니다. 이러한 자동 메모리 관리 프로세스가 궁극의 방법은 아닙니다. 왜냐하면 어떤 메모리가 여전히 필요한지 아닌지를 판단하는 것은 비결정적 문제이기 때문입니다.

가비지 콜렉션

위에서 언급한 것처럼 "더 이상 필요하지 않은" 모든 메모리를 찾는건 비결정적 문제입니다. 따라서 가비지 컬렉터들은 이 문제에 대한 제한적인 해결책을 구현합니다. 이 섹션에서는 주요한 가비지 컬렉션 알고리즘들과 그 한계를 이해하는데 필요한 개념을 설명합니다.

참조

가비지 콜렉션 알고리즘의 핵심 개념은 참조입니다. A라는 메모리를 통해 (명시적이든 암시적이든) B라는 메모리에 접근할 수 있다면 "B는 A에 참조된다" 라고 합니다. 예를 들어 모든 자바스크립트 오브젝트는 prototype 을 암시적으로 참조하고 그 오브젝트의 속성을 명시적으로 참조합니다.

이 컨텍스트에서 "오브젝트"의 개념은 일반 JavaScript 오브젝트와 함수 범위(또는 정적 어휘 범위)를 포함하여 더 넓게 확장됩니다.

참조-세기(Reference-counting) 가비지 콜렉션

참조-세기 알고리즘은 가장 소박한 알고리즘입니다. 이 알고리즘은 "더 이상 필요없는 오브젝트"를 "어떤 다른 오브젝트도 참조하지 않는 오브젝트"라고 정의합니다. 이 오브젝트를 "가비지"라 부르며, 이를 참조하는 다른 오브젝트가 하나도 없는 경우, 수집이 가능합니다.

예제

var x = {
  a: {
    b: 2
  }
};
// 2개의 오브젝트가 생성되었습니다. 하나의 오브젝트는 다른 오브젝트의 속성으로 참조됩니다.
// 나머지 하나는 'x' 변수에 할당되었습니다.
// 명백하게 가비지 콜렉션 수행될 메모리는 하나도 없습니다.


var y = x;      // 'y' 변수는 위의 오브젝트를 참조하는 두 번째 변수입니다.

x = 1;          // 이제 'y' 변수가 위의 오브젝트를 참조하는 유일한 변수가 되었습니다.

var z = y.a;    // 위의 오브젝트의 'a' 속성을 참조했습니다.
                // 이제 'y.a'는 두 개의 참조를 가집니다.
                // 'y'가 속성으로 참조하고 'z'라는 변수가 참조합니다.

y = "mozilla";  // 이제 맨 처음 'y' 변수가 참조했던 오브젝트를 참조하는 오브젝트는 없습니다.
                // (역자: 참조하는 유일한 변수였던 y에 다른 값을 대입했습니다)
                // 이제 오브젝트에 가비지 콜렉션이 수행될 수 있을까요?
                // 아닙니다. 오브젝트의 'a' 속성이 여전히 'z' 변수에 의해 참조되므로
                // 메모리를 해제할 수 없습니다.

z = null;       // 'z' 변수에 다른 값을 할당했습니다.
                // 이제 맨 처음 'x' 변수가 참조했던 오브젝트를 참조하는
                // 다른 변수는 없으므로 가비지 콜렉션이 수행됩니다.

한계: 순환 참조

순환 참조를 다루는 일에는 한계가 있습니다. 다음 예제에서는 두 객체가 서로 참조하는 속성으로 생성되어 순환 구조를 생성합니다. 함수 호출이 완료되면 이 두 객체는 스코프를 벗어나게 될 것이며, 그 시점에서 두 객체는 불필요해지므로 할당된 메모리는 회수되어야 합니다. 그러나 두 객체가 서로를 참조하고 있으므로, 참조-세기 알고리즘은 둘 다 가비지 컬렉션의 대상으로 표시하지 않습니다. 이러한 순환 참조는 메모리 누수의 흔한 원인입니다.

function f() {
  var x = {};
  var y = {};
  x.a = y;         // x는 y를 참조합니다.
  y.a = x;         // y는 x를 참조합니다.

  return "azerty";
}

f();

실제 예제

인터넷 익스플로러 6, 7 은 DOM 오브젝트에 대해 참조-세기 알고리즘으로 가비지 콜렉션을 수행합니다. 흔히, 이 두 브라우저에서는 다음과 같은 패턴의 메모리 누수가 발생합니다.

var div;
window.onload = function() {
  div = document.getElementById('myDivElement');
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join('*');
};

위의 예에서 DOM 요소 "myDivElement"는 "circularReference" 속성에서 자신에 대한 순환 참조를 가지고 있습니다. 속성이 명시적으로 제거되거나 null이 되지않으면 참조-계산 가비지 수집기는 항상 하나 이상의 참조를 그대로 유지하며 DOM 트리에서 제거된 경우에도 DOM 요소를 메모리에 유지합니다. DOM 요소가 많은 양의 데이터를 보유하는 경우 (위의 예에서 'lotsOfData'속성으로 설명됨)이 데이터에 사용된 메모리는 절대 해제되지 않으며 브라우저가 점점 더 느려지는 등 메모리 관련 문제로 이어질 수 있습니다.

표시하고-쓸기(Mark-and-sweep) 알고리즘

이 알고리즘은 "더 이상 필요없는 오브젝트"를 "닿을 수 없는 오브젝트"로 정의합니다.

이 알고리즘은 roots 라는 오브젝트의 집합을 가지고 있습니다(자바스크립트에서는 전역 변수들을 의미합니다). 주기적으로 가비지 콜렉터는 roots로 부터 시작하여 roots가 참조하는 오브젝트들, roots가 참조하는 오브젝트가 참조하는 오브젝트들... 을 닿을 수 있는 오브젝트라고 표시합니다. 그리고 닿을 수 있는 오브젝트가 아닌 닿을 수 없는 오브젝트에 대해 가비지 콜렉션을 수행합니다.

이 알고리즘은 위에서 설명한 참조-세기 알고리즘보다 효율적입니다. 왜냐하면 "참조되지 않는 오브젝트"는 모두 "닿을 수 없는 오브젝트" 이지만 역은 성립하지 않기 때문입니다. 위에서 반례인 순환 참조하는 오브젝트들을 설명했습니다.

2012년 기준으로 모든 최신 브라우저들은 가비지 콜렉션에서 표시하고-쓸기 알고리즘을 사용합니다. 지난 몇 년간 연구된 자바스크립트 가비지 콜렉션 알고리즘의 개선들은 모두 이 알고리즘에 대한 것입니다. 개선된 알고리즘도 여전히 "더 이상 필요없는 오브젝트"를 "닿을 수 없는 오브젝트"로 정의하고 있습니다.

순환 참조는 이제 문제가 되지 않습니다.

첫 번째 예제에서 함수가 반환되고 나서 두 오브젝트는 닿을 수 없습니다. 따라서 가비지 콜렉션이 일어납니다.

두 번째 예제에서도 마찬가지입니다. div 변수와 이벤트 핸들러가 roots로 부터 닿을 수 없으면 순환 참조가 일어났음에도 불구하고 가비지 콜렉션이 일어납니다. (역자2: div 선언을 블럭안에다 넣어야 됩니다.(테스트는 못 해봤습니다.))

한계: 수동 메모리 해제.

어떤 메모리를 언제 해제할지에 대해 수동으로 결정하는 것이 편리할 때가 있습니다. 그리고 수동으로 객체의 메모리를 해제하려면, 객체 메모리에 도달할 수 없도록 명시하는 기능이 있어야 합니다.

2019년 현재의 JavaScript에서는 명시적으로 또는 프로그래밍 방식으로 가비지 컬렉션을 작동할 수 없습니다.

Node.js

Node.js는 브라우저 환경에서 실행되는 JavaScript에 사용할 수 없는 메모리 문제를 구성하고 디버깅하기 위한 추가 옵션과 도구를 제공합니다.

V8 엔진 플래그

사용 가능한 힙 메모리의 최대 양은 플래그를 사용하여 늘릴 수 있습니다.

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

플래그 및 Chrome Debugger 디버거를 사용하여 메모리 문제를 디버깅하기 위해 가비지 수집기를 노출할 수도 있습니다:

node --expose-gc --inspect index.js

 

 


 

 

※ 쿠팡 파트너스 활동을 통해 일정액의 수수료를 제공 받을 수 있습니다 

반응형

'∮explotación≒ 개발' 카테고리의 다른 글

Error: Permission denied to access property  (0) 2023.04.23
JavaScript 상속과 프로토타입  (0) 2023.04.17
javascript Array.prototype.indexOf()  (0) 2023.04.12
java 예외( Exception )  (0) 2023.03.26
java 주석  (0) 2023.03.15

댓글