4 분 소요

Javascript - 메모리 관리

Javascript에서 메모리 관리

  • 저수준 언어의 메모리 관리: 메모리 할당/해제 메서드 사용.
  • 자바스크립트의 메모리 관리: 가비지 컬렉션을 통해 자동으로 메모리 할당 및 해제.

메모리 생존 주기

  1. 필요할 때 할당.
    • c: malloc()
    • javascript: 언어적으로 명시 X
  2. 할당된 메모리 사용.
    • 읽기 및 쓰기:
      • 할당된 메모리에 값을 넣을 수도
      • 거기에 저장되어 있는 값을 읽을 수도
  3. 필요하지 않으면 해제.
    • c: free()
    • javascript: 언어적으로 명시 X

메모리 할당

변수 및 함수 선언 시

// primitive type 메모리 할당
const test = test-text

// object type 메모리 할당
const obj = {
	one: 1,
	two: 2
}
const arr = ["one", "two", "three"]

// 함수 메모리 할당
function squareFunc(num) {
	return num*num
} // 함수를 저장하기 위해 메모리 할당

문자열, 숫자 등 primitive type 부터 object type 까지 선언 시 메모리 할당.

함수 또한 객체이기 때문에 함수 선언 시 메모리 할당.

함수 호출 시

const today = new Date(); // Date 생성자 함수를 호출하면 메모리 할당.

const myDiv = document.createElement("div"); // div 엘리먼트를 위한 메모리 할당.

Date 클래스(생성자 함수)를 사용하면 date 객체가 생겨 메모리 할당됨.
엘리먼트도 마찬가지.

스택: 정적 메모리 할당

  • 컴파일 타임에 크기를 알고 있는 데이터
  • primitive type, 참조(객체와 함수를 가리키는)
  • 고정된 메모리 크기 할당.

: 동적 메모리 할당

  • 런타임에 크기를 알게 되는 데이터
  • 필요에 따라 더 많은 공간을 할당.
  • 객체(함수 포함)

할당

const user = {
  id: 1,
  name: "blah",
}; // user 객체 자체는 동적 메모리 -> 힙에 할당

재할당

let variable = "init"; // 스택에 할당

variable = "initial"; // 스택에 메모리 새로 할당

JS에서는 기본적으로 primitive type들은 immutable

→ 할당된 메모리에 들어있는 값을 변경하는 것이 아님.
새로 메모리를 할당 받아 가리키게 되는 메모리 위치를 변경해주는 것.


메모리 사용

변수나 객체 속성의 값을 읽고 쓰거나 함수 호출 시 함수에 인수를 전달할 때 메모리에 저장되어 있는 것을 사용하여 수행.


메모리 해제 ** (중요)

→ Javascript 에서는 자동 메모리 관리 방법 가비지 컬렉션(GC) 사용
메모리 할당 추적, 메모리 블록 필요 없는지 판단하여 회수


가비지 콜렉션

Reference-counting 가비지 콜렉션

아무 것도 참조하지 않는 오브젝트를 가비지로 처리.

let company = "startup";

let team = {
  cs: "whoever",
  marketing: "none",
  product: ["dev", "design", "po", "qa"],
};

function sprint() {
  return "tired";
}

let ourTeam = team;

let productTeam = team.product;

이미지

  • company는 원시 타입을 가지고 있기에 스택에 쌓임.
  • 스택에 객체를 가리키는 team이라는 참조가 쌓임.
  • 객체 자체는 힙에 있음.
  • 스택에 ourTeam 참조가 쌓임
  • ourTeamteam이 가리키는 똑같은 메모리 위치를 가리킴.
  • productTeam이라는 참조가 스택에 쌓임.
  • 힙에 있는 메모리(team.product의 값이 있는)가 있고, 그걸 productTeam이 가리킴.
team = null;
ourTeam = null;

이미지

  • teamourTeamnull이 되어 가리키고 있는 게 끊어지며 이 쓸모없는(참조되지 않는) 메모리 영역은 가비지 컬렉션의 대상이 됨.
  • 그러나 productTeam이 가리키는 메모리 영역은 그대로 남아있음.
    • team 객체 메모리가 사라졌다고 productTeam이 사라진 건 아님.

그러나, 이 알고리즘은 순환 참조를 고려하지 못함. 😭

let you = {
  name: "your name",
};
let me = {
  name: "my name",
};

you.me = me;
me.you = you;

이미지

you = null;
me = null;

이미지

youme를 서로 참조하게 하고(순환 참조) you 참조와 me 참조를 끊어버려도 힙에서는 메모리 영역이 계속 남아있게 됨.

Mark-and-sweep 알고리즘

닿을 수 없는 오브젝트를 가비지로 처리

roots(전역 오브젝트/브라우저에서는 window, NodeJS 환경에서는 global) 객체에서 시작해서 참조하여 도달할 수 없는 객체를 찾음.

도달할 수 없으면 가비지로 mark하고, 이후에 sweep하게 됨.

let team = {
  cs: "whoever",
  marketing: "none",
  product: ["dev", "design", "po", "qa"],
};

let anotherRefTeam = team;
let productTeam = team.product;

team = null;
anotherRefTeam = null;

console.log(productTeam);

let you = {
  name: "your name",
};
let me = {
  name: "my name",
};

you.me = me;
me.you = you;

you = null;
me = null;

const unusedValue = "will not be used";

이미지

  • 순환 참조 되고 있던 것들은 스택의 참조들이 null로 끊어졌기 때문에 window에서 접근할 수 있는 방법이 없음.
  • unusedValue 같은 경우는 아무데도 사용되지 않으므로 가비지 컬렉션의 대상이 된다.
  • productTeamconsolelog 메서드의 인자로 참조를 사용하고 있기 때문에 reachable하여 가비지 컬렉션의 대상이 되지 않는다.
    • productTeam도 아무데도 사용되지 않으면 가비지 컬렉션의 대상이 된다.

메모리 낭비

가비지 콜렉터의 Trade-offs

  • 아무리 알아서 해준다고 해도 청소되는 것은 필요 없을 때 되는 게 아니라 주기적으로 되는 것이기 때문에 생각하는 것 보다 메모리를 많이 잡아먹을 수 있다.
  • 콜렉션이 주기적으로 실행되는 것을 개발자가 정확히 언제 일어나는지 알 수가 없다.
  • 컬렉팅하게 되는 가비지의 양이 많거나 빈도수가 많아져도 성능을 잡아먹게 됨. → 그래도 다행인건 유저나 개발자가 신경 쓸 정도는 아니라고 함.

전역 변수

var 혹은 아예 키워드를 쓰지 않으면 전역 변수에 저장 됨.
(window 객체의 속성으로 할당 됨.)

→ function 키워드도 마찬가지라고 함 🙀

사용하지 말라기 보다 null로 참조를 해제해주면 좋음.

잊어버린 타이머

// 잊어버린 타이머
const object = {};
const intervalId = setInterval(function () {
  // 여기에서 사용된 모든 것들은 interval이 클리어될 때까지 수집되지 않음.
  doSomething(object);
}, 2000);

2초마다 실행되므로 interval이 끝나지 않는 이상 저 스코프 안에서 참조된 객체들은 수집되지 않음.

clearInterval(intervalId);

clearInterval로 타이머 취소.

→ SPA에서 특히 조심. 페이지가 변경되어도 백그라운드에서 실행되므로

구독 취소를 잊어버린 이벤트 리스너

이벤트 리스너 콜백은 예전에는 수집 못했는데 요새 브라우저는 다 한다고 함.

그래도 remove 해주자!

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); // elements 배열 mutate하여 요소 제거**
  });
}

JS에서 DOM을 참조하여 배열에 저장해두었고, 엘리먼트는 지웠지만

elements 객체는 여전히 살아있으므로 메모리를 잡아먹는다.

따라서 elements에서 참조하고 있는 요소들 모두 제거.

참고 링크

https://developer.mozilla.org/ko/docs/Web/JavaScript/Memory_management

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

(보면 좋을 것 같은 글들)

https://yceffort.kr/2020/07/memory-leaks-in-javascript

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/WeakMap


댓글남기기