분류 javascript

JavaScript의 메모리 관리 설명

컨텐츠 정보

  • 조회 258 (작성일 )

본문

대부분의 경우, JavaScript 개발자로서 메모리 관리에 대해 전혀 알지 못해도 괜찮을 것입니다. 결국 JavaScript 엔진이 이를 처리합니다.


JavaScript Memory Management Explained - Cover 


하지만 어느 시점에서나 메모리 누수와 같은 문제가 발생하는데, 이는 메모리 할당 작동 방식을 알고 있는 경우에만 해결할 수 있습니다.


이 기사에서는 메모리 할당 및 가비지 수집이 작동하는 방식과 일반적인 메모리 누수를 방지 할 수 있는 방법을 소개합니다.


Memory life cycle 


JavaScript에서 변수, 함수 또는 생각할 수 있는 모든 것을 만들면 JS 엔진이 이를 위해 메모리를 할당하고 더 이상 필요하지 않으면 해제합니다.


메모리 할당은 메모리에 공간을 예약하는 과정이며 메모리를 해제하면 다른 용도로 사용할 수 있는 공간이 확보됩니다.


변수를 할당하거나 함수를 만들 때마다 해당 메모리는 항상 다음 단계를 거치게 됩니다.


Memory life cycle overview 

  • Allocate memory
    JavaScript는 이를 처리합니다. 생성 한 객체에 필요한 메모리를 할당합니다.
  • Use memory
    메모리를 사용하는 것은 코드에서 명시적으로 수행하는 작업입니다. 메모리에 읽고 쓰는 것은 변수에서 읽거나 쓰는 것입니다.
  • Release memory
    이 단계는 JavaScript 엔진에서도 처리됩니다. 할당 된 메모리가 해제되면 새로운 용도로 사용할 수 있습니다.

The memory heap and stack 


이제 우리는 JavaScript에서 정의하는 모든 것에 대해 엔진이 메모리를 할당하고 더 이상 필요하지 않으면 해제한다는 것을 알고 있습니다.


다음 질문이 떠 올랐습니다. 이것은 어디에 저장 될까요?


자바 스크립트 엔진은 데이터를 저장할 수 있는 두 곳에 메모리 힙과 스택이 있습니다.


힙과 스택은 엔진이 서로 다른 목적으로 사용하는 두 가지 데이터 구조입니다.


Stack: Static memory allocation 


Memory stack Examples 


모든 값에는 기본 값이 포함되어 있으므로 모든 값이 스택에 저장됩니다.


스택은 JavaScript가 정적 데이터를 저장하는 데 사용하는 데이터 구조입니다. 정적 데이터는 엔진이 컴파일 타임에 크기를 알고 있는 데이터입니다. JavaScript에서 여기에는 객체와 함수를 가리키는 원시 값 (문자열, 숫자, 부울, 정의되지 않음 및 null)과 참조가 포함됩니다.


엔진은 크기가 변경되지 않는다는 것을 알고 있으므로 각 값에 대해 고정 된 양의 메모리를 할당합니다.


실행 직전에 메모리를 할당하는 프로세스를 정적 ​​메모리 할당이라고 합니다.


엔진은 이러한 값에 대해 고정 된 양의 메모리를 할당하기 때문에 기본 값의 크기에 제한이 있습니다.


이러한 값과 전체 스택의 제한은 브라우저에 따라 다릅니다.


Heap: Dynamic memory allocation 


힙은 JavaScript가 객체와 함수를 저장하는 데이터를 저장하는 다른 공간입니다.


스택과 달리 엔진은 이러한 개체에 대해 고정 된 양의 메모리를 할당하지 않습니다. 대신 필요에 따라 더 많은 공간이 할당됩니다.


이러한 방식으로 메모리를 할당하는 것을 동적 메모리 할당이라고도 합니다.


개요를 보려면 다음은 두 저장소의 기능을 나란히 비교 한 것입니다.


 Stack

 Heap

기본 값 및 참조
크기는 컴파일 타임에 알려져 있습니다.

고정 된 양의 메모리 할당 

개체 및 기능
런타임에 크기가 알려짐 

개체 당 제한 없음 



Examples 


몇 가지 코드 예제를 살펴 보겠습니다. 캡션에서 할당되는 항목을 언급합니다.


const person = {
  name: 'John',
  age: 24,
};


JS는 힙에서 이 객체에 대한 메모리를 할당합니다. 실제 값은 여전히 ​​원시적이므로 스택에 저장됩니다.


const hobbies = ['hiking', 'reading'];

배열도 객체이므로 힙에 저장됩니다.


let name = 'John'; // allocates memory for a string
const age = 24; // allocates memory for a number

name = 'John Doe'; // allocates memory for a new string
const firstName = name.slice(0,4); // allocates memory for a new string


원시 값은 변경 불가능합니다. 즉, JavaScript는 원래 값을 변경하는 대신 새 값을 만듭니다.


JavaScript의 참조 


모든 변수는 먼저 스택을 가리킵니다. 원시 값이 아닌 경우 스택에는 힙의 개체에 대한 참조가 포함됩니다.


힙의 메모리는 특정 방식으로 정렬되지 않으므로 스택에 참조를 유지해야 합니다. 참조를 주소로 생각하고 힙의 개체를 이러한 주소가 속한 집으로 생각할 수 있습니다.


JavaScript는 객체와 함수를 힙에 저장합니다. 기본 값과 참조는 스택에 저장됩니다. 


Stack heap pointers explained 


이 그림에서 서로 다른 값이 저장되는 방식을 볼 수 있습니다. person과 newPerson이 모두 동일한 객체를 가리키는 방법에 유의하십시오.


Examples 


const person = {
  name: 'John',
  age: 24,
};


그러면 힙에 새 개체가 생성되고 스택에 이에 대한 참조가 생성됩니다.


Garbage collection 


이제 JavaScript가 모든 종류의 객체에 메모리를 할당하는 방법을 알고 있지만 메모리 수명주기를 기억한다면 마지막 단계 인 메모리 해제가 있습니다.


메모리 할당과 마찬가지로 JavaScript 엔진도 이 단계를 처리합니다. 더 구체적으로, 가비지 수집기가 이를 처리합니다.


JavaScript 엔진은 주어진 변수 나 함수가 더 이상 필요하지 않다는 것을 인식하면 차지한 메모리를 해제합니다.


이것의 주된 문제는 일부 메모리가 여전히 필요한지 여부가 결정 불가능한 문제라는 것입니다. 즉, 더 이상 필요하지 않은 메모리가 쓸모 없게 되는 정확한 순간에 더 이상 필요하지 않은 모든 메모리를 수집 할 수 있는 알고리즘이 있을 수 없다는 것을 의미합니다.


일부 알고리즘은 문제에 대한 좋은 근사치를 제공합니다. 이 섹션에서 가장 많이 사용되는 항목 인 참조 계산 가비지 수집과 마크 및 스윕 알고리즘에 대해 설명합니다.


Reference-counting garbage collection 


이것은 가장 쉬운 근사치입니다. 가리키는 참조가 없는 개체를 수집합니다.


다음 예를 살펴 보겠습니다. 선은 참조를 나타냅니다.


https://felixgerschau.com/video/stack-heap-gc-animation.mp4


마지막 프레임에서는 끝에 참조가 있는 객체이기 때문에 취미 만 힙에 유지되는 방법에 유의하십시오.


Cycles 


이 알고리즘의 문제점은 순환 참조를 고려하지 않는다는 것입니다. 이것은 하나 이상의 객체가 서로를 참조하지만 더 이상 코드를 통해 액세스 할 수 없을 때 발생합니다.


let son = {
  name: 'John',
};

let dad = {
  name: 'Johnson',
}

son.dad = dad;
dad.son = son;

son = null;
dad = null;


Reference cycle illustrated 

아들과 아빠 개체가 서로를 참조하기 때문에 알고리즘은 할당 된 메모리를 해제하지 않습니다. 더 이상 두 개체에 액세스 할 수 있는 방법이 없습니다.


그것들을 null로 설정하면 참조 계산 알고리즘이 둘 다 들어오는 참조가 있기 때문에 더 이상 사용할 수 없음을 인식하지 못합니다.


Mark-and-sweep algorithm 


마크 앤 스윕 알고리즘에는 순환 종속성에 대한 솔루션이 있습니다. 단순히 주어진 객체에 대한 참조를 계산하는 대신 루트 객체에서 도달 할 수 있는지 감지합니다.


브라우저의 루트는 창 객체이고 NodeJS에서는 전역입니다.

Mark-and-sweep algorithm illustrated 

알고리즘은 도달 할 수 없는 객체를 쓰레기로 표시하고 나중에 이를 스윕 (수집)합니다. 루트 개체는 수집 되지 않습니다.


이렇게 하면 순환 종속성이 더 이상 문제가 되지 않습니다. 이전 예제에서는 루트에서 아빠 나 아들 객체에 도달 할 수 없습니다. 따라서 둘 다 쓰레기로 표시되고 수집됩니다.


2012 년부터 이 알고리즘은 모든 최신 브라우저에서 구현됩니다. 성능과 구현 만 개선되었지만 알고리즘의 핵심 아이디어 자체는 개선되지 않았습니다.


Trade-offs 


자동 가비지 수집을 통해 메모리 관리로 시간을 낭비하는 대신 애플리케이션 구축에 집중할 수 있습니다. 그러나 우리가 알아야 할 몇 가지 장단점이 있습니다.


Memory usage 


알고리즘이 정확히 메모리가 더 이상 필요하지 않을 때를 알 수 없다는 점을 감안할 때 JavaScript 응용 프로그램은 실제로 필요한 것보다 더 많은 메모리를 사용할 수 있습니다.


객체가 가비지로 표시 되더라도 할당 된 메모리를 수집 할 시기와 여부를 결정하는 것은 가비지 수집기에 달려 있습니다.


응용 프로그램이 메모리를 최대한 효율적으로 사용해야 한다면 낮은 수준의 언어를 사용하는 것이 좋습니다. 그러나 여기에는 고유 한 장단점이 있습니다.


Performance 


우리를 위해 쓰레기를 수집하는 알고리즘은 일반적으로 사용되지 않는 개체를 정리하기 위해 주기적으로 실행됩니다.


문제는 개발자 인 우리가 정확히 언제 이런 일이 일어날 지 모른다는 것입니다. 많은 가비지를 수집하거나 자주 가비지를 수집하면 일정량의 계산 능력이 필요하므로 성능에 영향을 미칠 수 있습니다.


그러나 영향은 일반적으로 사용자 또는 개발자에게 눈에 띄지 않습니다.


Memory leaks 


메모리 관리에 대한 이 모든 지식을 바탕으로 가장 일반적인 메모리 누수를 살펴 보겠습니다.


뒤에서 무슨 일이 일어나고 있는지 이해하면 쉽게 피할 수 있음을 알 수 있습니다.


Global variables 


전역 변수에 데이터를 저장하는 것이 아마도 가장 일반적인 유형의 메모리 누수 일 것입니다.


예를 들어 브라우저에서 const 또는 let 대신 var를 사용하거나 키워드를 모두 생략하면 엔진이 변수를 window 개체에 연결합니다.


function 키워드로 정의 된 함수도 마찬가지입니다.


user = getUser();
var secondUser = getUser();
function getUser() {
  return 'user';
}

세 가지 변수 인 user, secondUser 및 getUser가 모두 창 개체에 연결됩니다.


이는 전역 범위에 정의 된 변수 및 함수에만 적용됩니다. 이에 대해 자세히 알아 보려면 JavaScript 범위를 설명하는 이 기사를 확인하십시오.


엄격 모드에서 코드를 실행하여 이를 피하십시오.


실수로 루트에 변수를 추가하는 것 외에도 의도적으로 이 작업을 수행 할 수 있는 경우가 많이 있습니다.


확실히 전역 변수를 사용할 수 있지만 데이터가 더 이상 필요하지 않으면 여유 공간을 확보하십시오.


메모리를 해제하려면 전역 변수를 null에 할당하십시오.


window.users = null;


Forgotten timers and callbacks 


타이머와 콜백을 잊어 버리면 애플리케이션의 메모리 사용량이 증가 할 수 있습니다. 특히 SPA (단일 페이지 애플리케이션)에서 이벤트 리스너 및 콜백을 동적으로 추가 할 때 주의해야 합니다.


Forgotten timers 


const object = {};
const intervalId = setInterval(function() {
  // everything used in here can't be collected
  // until the interval is cleared
  doSomething(object);
}, 2000);


위의 코드는 2 초마다 함수를 실행합니다. 프로젝트에 이와 같은 코드가 있는 경우 항상 실행하는 데 필요하지 않을 수 있습니다.


간격이 취소되지 않는 한 간격에서 참조 된 개체는 가비지 수집 되지 않습니다.


더 이상 필요하지 않으면 간격을 지우십시오.


clearInterval(intervalId);


이것은 SPA에서 특히 중요합니다. 이 간격이 필요한 페이지를 벗어나는 경우에도 여전히 백그라운드에서 실행됩니다.


Forgotten callbacks 


나중에 제거되는 버튼에 onclick 리스너를 추가한다고 가정 해 보겠습니다.


오래된 브라우저는 리스너를 수집 할 수 없었지만 이제는 더 이상 문제가 되지 않습니다.


그래도 이벤트 리스너가 더 이상 필요하지 않으면 제거하는 것이 좋습니다.


const element = document.getElementById('button');
const onClick = () => alert('hi');

element.addEventListener('click', onClick);

element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);


Out of DOM reference 


이 메모리 누수는 이전의 것과 유사합니다. JavaScript에 DOM 요소를 저장할 때 발생합니다.


const elements = [];
const element = document.getElementById('button');
elements.push(element);

function removeAllElements() {
  elements.forEach((item) => {
    document.body.removeChild(document.getElementById(item.id))
  });
}


이러한 요소 중 하나를 제거 할 때 이 요소도 배열에서 제거해야 할 것입니다.


그렇지 않으면 이러한 DOM 요소를 수집 할 수 없습니다.


const elements = [];
const element = document.getElementById('button');
elements.push(element);

function removeAllElements() {
  elements.forEach((item, index) => {
    document.body.removeChild(document.getElementById(item.id));
    elements.splice(index, 1);
  });
}


배열에서 요소를 제거하면 DOM과 동기화 됩니다.


모든 DOM 요소는 부모 노드에 대한 참조도 유지하므로 가비지 수집기가 요소의 부모 및 자식을 수집하지 못하도록 합니다.


결론 


이 기사에서는 JavaScript에서 메모리 관리의 핵심 개념을 요약했습니다.


이 기사를 작성하는 것은 내가 완전히 이해하지 못한 몇 가지 개념을 명확히 하는 데 도움이 되었으며, 이것이 JavaScript에서 메모리 관리가 작동하는 방식에 대한 좋은 개요가 되기를 바랍니다.


나는 여기에 언급하고 싶은 다른 훌륭한 기사에서 이것을 배웠다.


관심이 있을 만한 다른 기사 :

  • JavaScript Event Loop And Call Stack Explained
    이 블로그 시리즈의 첫 번째 부분에서는 JavaScript가 단일 스레드 언어 임에도 불구하고 브라우저에서 동시에 작업을 수행 할 수 있는 이유를 설명합니다.
  • My 9 favorite topics of "The Pragmatic Programmer"
    읽기는 프로그래밍 기술을 향상 시키는 좋은 방법입니다. 이 기사에서는 내가 가장 좋아하는 프로그래밍 책의 핵심 내용을 공유합니다.
  • JavaScript Heap Out Of Memory Error
    이 문서는 메모리 부족으로 실행되는 Node.js 앱에 대해 수행 할 수 있는 작업을 설명하므로 이 문서와 밀접한 관련이 있습니다.

https://felixgerschau.com/javascript-memory-management