자바스크립트의 데이터 타입 & 메모리 구조 & 가비지 컬렉션

Tori·2024년 12월 7일
post-thumbnail

poiemaweb - 자바스크립트 엔진
toast ui - 자바스크립트의 가비지 컬렉션
JavaScript Info - 원시값의 메서드
자바스크립트의 콜스택, 메모리 힙 구조 - charming-kyu
JavaScript Info - 가비지 컬렉션




학습 목표

  • 데이터 타입
  • Wrapper Objects
  • 콜 스택과 메모리 힙
  • 메모리 생명주기를 통한 메모리 할당과 해제
  • 힙과 스택을 통해 메모리 할당
  • 가비지 컬렉션을 통해 메모리 해제




데이터 타입

자바스크립트의 모든 값은 데이터 타입(data type)을 갖는다.

원시 타입 (Primitive Type)

JavaScript에서, 원시 값(primitive, 또는 원시 자료형)이란 객체가 아니면서 메서드 또는 속성도 가지지 않는 데이터를 말한다. 원시 값에는 7가지의 종류가 있다.

  • 원시 자료형은 값 자체에 대한 변경이 불가능(immutable)하지만, 변수에 데이터를 재할당 할 수 있다.
  • 한 개의 메모리에 1개의 데이터를 보관한다.
  • 변수를 재할당해도 변수의 할당 값에 영향을 주지 않는다.
데이터 타입설명
문자열(string)문자열
숫자(number)숫자, 정수와 실수 구분없이 하나의 숫자 타입만 존재
BigintBigInt 는 Number 원시 값이 안정적으로 나타낼 수 있는 최대치인 2^53 - 1보다 큰 정수를 표현할 수 있는 내장 객체
불리언(boolean)논리적 참(true)과 거짓(false)
undefinedvar 키워드로 선언된 변수에 암묵적으로 할당되는 값
null값이 없다는 것을 의도적으로 명시할 때 사용하는 값
SymbolES6에서 추가된 7번재 타입

예시 코드

let string = 'hello javascript';
string = 'test code'; // 재할당 가능

string[0] = 'T'; // 원시 타입이기 때문에 Test code로 변경이 불가능하다. 조용히 실패함(strict mode에서도 오류가 발생하지 않음)
console.log(string); // test code

let stringCopy = string; // string 값이 stringCopy에 그대로 복사되어 'test code'가 할당된다.
console.log(stringCopy); // test code

string = 100;

console.log(string); // 100
console.log(stringCopy); // test code

위 코드에서 내부적으로 일어나는 과정을 살펴보면
  1. string 원시값에 대해 임시 String Wrapper 객체가 생성된다.
  2. 인덱스 접근 시도
  3. 원시 문자열은 불면(immutable)이므로 변경되지 않음
  4. 래퍼 객체는 폐기된다.



참조 타입 (Reference Type)

  • 참조 타입은 변수에 할당할 때는 값이 아닌 '주소'를 저장한다.
  • 변수는 주소를 저장하고, 주소는 특별한 동적인 데이터 보관함에 보관되는데 이 보관함을 메모리 힙이라고 한다.
  • 값을 재할당 할 경우 주소는 참조한 모든 값이 영향을 받는다. (값이 공유 됨)
  • 프로퍼티에 다양한 종류의 값을 저장할 수 있다.
구분설명
객체타입객체, 함수, 배열 등



기본형과 참조형을 구분하는 기준

  • 기본형은 할당이나 연산시 복제되고 참조형은 참조된다고 알려져 있다. 엄밀히 말하면 둘 모두 복제를 하긴한다.
  • 다만 기본형은 값이 담긴 주솟값을 바로 복제하고, 참조형은 값이 담긴 주솟값들로 이루어진 묶음을 가리키는 주솟값을 복제한다.
  • 기본형은 불변성(immutability)을 띄는데 "변경하지 않는다"는 게 어떤 의미일까?

메모리 구조에서 자세하게 살펴보도록 하자.



원시값에서의 메소드 호출 - Wrapper Objects

원시 값의 메서드

자바스크립트는 원시값을 마치 객체처럼 다룰 수 있게 해준다.
원시값에서도 객체에서처럼 메서드를 호출할 수 있다.

자바스크립트는 날짜, HTML element 등을 다룰 수 있게 해주는 다양한 내장 객체를 제공하고 이 객체들은 고유한 프로퍼티와 메서드를 가진다.
하지만, 이런 기능을 사용하면 시스템 자원이 많이 소모된다는 단점이 있다.

객체는 원시값보다 "무겁고", 내부 구조를 유지하기 위해 추가 자원을 사용하기 때문이다.

원시값을 객체처럼 사용하기

모순적인 상황을 해결해야 된다!

자바스크립트 창안자(creator)는 다음과 같은 모순적인 상황을 해결해야만 했다.

  • 문자열, 숫자와 같은 원시값을 다룰 때 메서드를 사용하면 작업을 수월하게 할 수 있을텐데?
  • 그런데 원시값은 가능한 빠르고 가벼워야해!

자바스크립트 창안자는 아래와 같은 방법을 사용해 해결책을 모색하였다.

  1. 원시값은 원시값 그대로 남겨둬 단일 값 형태를 유지한다.
  2. 문자열, 숫자, 불린, 심볼의 메서드와 프로퍼티에 접근할 수 있도록 언어 차원에서 허용한다.
  3. 이를 가능하게 하기위해, 원시값이 메서드나 프로퍼티에 접근하려 하면 추가 기능을 제공해주는 특수한 객체, "원시 래퍼 객체(object wrapper)"를 만들어 준다. 이 객체는 곧 삭제된다.

"래퍼 객체"는 원시 타입에 따라 종류가 다양하다. 각 래퍼 객체는 원시 자료형의 이름을 그대로 차용해, String, Number, Boolean, Symbol라고 부른다. 래퍼 객체마다 제공하는 메서드 역시 다르다.


예시 코드

인수로 받은 문자열의 모든 글자를 대문자로 바꿔주는 메서드 str.toUpperCase()를 예로 살펴보기

// 원시 타입
let primitiveString = "Hello";

// 원시 타입에 메서드 호출 시 내부적으로 발생하는 과정
primitiveString.toUpperCase(); // HELLO

primitiveString.toUpperCase()가 호출될 때 내부에서 일어나는 일

  1. new String(primitiveString) 임시 래퍼 객체 생성
    : 문자열 primitiveString은 원시값이므로 원시값의 프로퍼티(toUpperCase)에 접근하는 순간 특별한 객체가 만들어진다. 이 객체는 문자열의 값을 알고있고, toUpperCase()와 같은 유용한 메서드를 가지고 있다.
  2. 래퍼 객체의 메서드 호출
    : 메서드가 실행되고, 새로운 문자열이 반환된다.
  3. 래퍼 객체 폐기 (가비지 컬렉션)
    : 특별한 객체는 폐기되고, 원시값 primitiveString만 남는다.

Wrapper Objects의 장점

  1. 메서드와 프로퍼티 접근 제공
// 원시 타입에 대한 풍부한 기능 제공
const str = "hello world";

// 문자열 조작 메서드
console.log(str.toUpperCase());  // "HELLO WORLD"
console.log(str.slice(0, 5));    // "hello"
console.log(str.split(" "));     // ["hello", "world"]

// 숫자 형식화
const num = 12345.6789;
console.log(num.toLocaleString()); // "12,345.679"
console.log(num.toExponential(2)); // "1.23e+4"

  1. 타입 변환 용이성
// 다양한 타입 변환 메서드 제공
const num = 123;
console.log(String(num));        // "123"
console.log(Boolean(num));       // true

const str = "456";
console.log(Number(str));        // 456
console.log(parseInt(str, 10));  // 456

  1. 확장성
// 프로토타입을 통한 기능 확장
String.prototype.reverse = function() {
  return this.split('').reverse().join('');
};

const str = "hello";
console.log(str.reverse()); // "olleh"



Wrapper Objects의 단점

  1. 성능 오버헤드 - 불필요한 래퍼 객체 생성은 성능에 영향을 줄 수 있다.
    (자바스크립트 엔진은 내부 최적화가 잘 되어있어 메서드를 호출해도 많은 리소스를 쓰지 않음)
// 불필요한 래퍼 객체 생성은 성능에 영향을 줄 수 있음
function badPerformance() {
  let str = "hello";
  for (let i = 0; i < 1000000; i++) {
    // 매 반복마다 새로운 래퍼 객체 생성
    str = new String(str).toString();
  }
}

// 더 나은 방법
function goodPerformance() {
  let str = "hello";
  for (let i = 0; i < 1000000; i++) {
    str = str.toString();
  }
}

  1. 혼란스러운 동작
// 원시 타입과 래퍼 객체의 비교
const str1 = "hello";
const str2 = new String("hello");

console.log(typeof str1); // "string"
console.log(typeof str2); // "object"
console.log(str1 === str2); // false
console.log(str1 == str2); // true

// instanceof 연산자의 혼란
console.log("hello" instanceof String); // false
console.log(new String("hello") instanceof String); // true

  1. 불필요한 메모리 사용
// 명시적 래퍼 객체 생성의 문제
const numbers = [];
for (let i = 0; i < 1000000; i++) {
  // 불필요한 메모리 사용
  numbers.push(new Number(i));
}

// 더 나은 방법
const betterNumbers = [];
for (let i = 0; i < 1000000; i++) {
  betterNumbers.push(i);
}



Wrapper Objects 권장 사항

  1. String/Number/Boolean를 생성자론 쓰지 말자
// 권장되는 사용법
const str = "hello";
console.log(str.toUpperCase());

const num = 123.456;
console.log(num.toFixed(2));

// 피해야 할 사용법
const strObj = new String("hello");
const numObj = new Number(123.456);

  1. 타입 변환 시 주의사항
  • new를 붙이지 않고 String / Number / Boolean을 사용하는 건 괜찮다.
  • new 없이 사용하면 상식에 맞게 인수를 원하는 형의 원시값(문자열, 숫자, 불린 값)으로 바꿔준다. 아주 유용하다.
// 권장되는 타입 변환, 
const num = Number("123"); // 123
const str = String(123); // "123"
const bool = Boolean(1); // true

// 피해야 할 타입 변환
console.log(typeof 0); // number
const num = new Number("123"); // Number {123}
const str = new String(123); // String {'123'}
const bool = new Boolean(1); // Boolean {true}

let zero = new Number(0);
if (zero) {   // 변수 zero는 객체이므로, 조건문이 참이 된다.
  console.log("zero가 참이라는 것에 동의하시나요?!"); 
}



null/undefined는 메서드가 없다.

  • 특수 자료형인 null과 undefined의 원시값(null/undefined)은 위와 같은 법칙을 따르지 않는다.
  • 이 자료형과 연관되는 "래퍼 객체"도 없고, 메서드도 제공하지 않는다.
  • 어떤 의미에서는 두 자료형이 "가장 원시적"이라 할 수 있을 것 같다.

두 자료형에 속한 값의 프로퍼티에 접근하려 하면 에러가 발생한다.

alert(null.test); // error





메모리 구조

콜 스택, 메모리 힙

  • 글의 V8 자바스크립트 엔진을 비롯한 대부분의 자바스크립트 엔진은 크게 2개의 영역으로 구분할 수 있다.
  • 스크립트는 싱글 스레드(single thread) 방식으로 동작한다. (멀티가 되지 않고, 하나씩 처리한다.)
  • 힙과 스택은 자바스크립트 엔진에서 메모리가 저장되는 메모리 영역이다.

콜 스택(call stack)

원시 타입 데이터가 저장되는 공간이다.
고정된 메모리를 할당하기 때문에 정적 메모리 할당이라고도 한다.

  • boolean: 1bit
  • 숫자: 8byte(64bit)
  • 문자: 16bit

소스코드(전역 코드나 함수 코드 등) 평가 과정에서 생성된 실행 컨텍스트(Execution Context)가 추가되고 제거되는 스택 자료구조인 실행 컨택스트 스택이 바로 콜 스택이다.

함수를 호출하면 함수 실행 컨텍스트가 순차적으로 콜 스택에 푸시되어 순차적으로 실행된다.
자바스크립트 엔진은 단 하나의 콜 스택을 사용하기 때문에 최상위 실행 컨텍스트(실행 중인 실행 컨텍스트)가 종료되어 콜 스택에서 제거되기 전까지는 다른 어떤 태스크도 실행되지 않는다.

  1. 변수 식별자(이름) 저장
  2. 스코프 체인 및 this 관리
  3. 코드 실행 순서 관리 등을 수행
  4. 주솟값의 경우 스택에 저장 -> 변수의 스코프, 실행 컨텍스트, 렉시컬 환경과 연관있음

메모리 힙(Memory Heap)

힙은 객체가 저장되는 메모리 공간이다.
참조값을 필요한 만큼 많은 메모리를 할당한다. 동적 메모리할당이라고 한다.
콜 스택의 요소인 실행 컨텍스트는 힙에 저장된 객체를 참조한다.

메모리에 값을 저장하려면 먼저 값을 저장할 메모리 공간의 크기를 결정해야 한다.
객체는 원시 값과는 달리 크기가 정해져 있지 않으므로 할당해야 할 메모리 공간의 크기를 런타임에 결정(동적 할당)해야 한다.
따라서 객체가 저장되는 메모리 공간인 힙은 구조화 되어있지 않다는 특징이 있다.



콜 스택과 메모리 힙의 데이터 저장 구조

  1. 원시 타입 데이터: 변수 a
  • 10이라는 값 자체는 원시 타입이므로 콜 스택에 저장된다.
  • 변수 a에는 10이 저장된 콜 스택 메모리의 주소값이 저장된다.
    - 변수 식별자 a는 콜 스택의 요소인 실행 컨텍스트의 렉시컬 환경(Lexical Environment)에 저장된다.

  1. 참조 타입 데이터: b, c, d
  • 배열, 객체, 함수 등은 참조 타입이므로 메모리 힙에 저장된다.
  • 참조 타입 데이터가 저장된 메모리 힙의 주소값은 콜 스택에 각각 저장된다.
  • 메모리 힙의 주소 값이 저장된 콜 스택의 주소값은 각각 변수 b, c, d에 저장된다.
  • 변수 식별자 b, c, d는 콜스택의 실행 컨텍스트의 렉시컬 환경에 저장된다.



원시 타입/참조 타입 데이터가 콜 스택과 메모리 힙에서 어떻게 관리되는지 알아보기

불변값

  • 변수(variable)와 상수(constant)를 구분하는 성질은 "변경 가능성"이다. 바꿀 수 있으면 변수, 바꿀 수 없으면 상수이다.
  • 불변값과 상수를 같은 개념으로 오해하기 쉬운데 이 둘을 명확하게 구분할 필요가 있다.

원시 타입의 변수 선언과 할당

1. 원시 타입 변수 생성

let a = 100;
let b = 200;

  • 원시 타입의 데이터 값은 콜 스택에 저장
  • 데이터 값이 저장된 콜 스택의 주소 값이 변수 a, b에 각각 저장

2. 원시 타입 재할당 1

a = 200;

  • 변수 a에 200을 재할당하면 변수 a의 주소값 메모리에 있는 값을 직접 변경하는 것이 아니라 콜 스택 내에 200이 있는지 찾고, 값이 있을 경우 200을 저장하고 있는 메모리의 주소값으로 교체
  • a에 저장된 주소값은 200을 가리키고 있던 b에 저장된 주소값과 동일해짐

3. 원시 타입 재할당 2

b = 500;

  • 변수 b에 500을 재할당하면 변수 b의 주소값이 가리키는 메모리에 저장된 200을 500으로 변경하는 것이 아니라 500이라는 값이 메모리에 없을 경우 새로운 메모리를 확보하여 500을 저장하고, 변수 b에 저장된 주소값을 500이 저장된 해당 주소값으로 교체

4. 가비지 컬렉터


더이상 참조되지 않는 데이터는 가비지 컬렉터에 의해 적절한 시점에 메모리에서 해제된다.



참조 타입의 변수 선언과 할당

let arr = [];


arr 변수를 선언하고 [ ] 빈 배열 참조 타입을 할당했을 때 메모리에서 일어나는 일

  • 변수의 고유 식별자 arr을 생성
  • 콜 스택의 메모리에 주소를 할당 (런타임에 할당)
  • 메모리 힙에 할당된 메모리 주소를 콜 스택의 값으로 저장 (런타임에 할당)
  • 메모리 힙 주소에 할당된 값(빈 배열)을 저장



let vs const를 언제 사용하는 것이 좋을까

일반적으론 가능한 한 const를 사용해야 하며 변수가 변경될거란 사실을 알 때만 let을 사용해야 된다.

변경은 값(value)의 변경이 아니라 메모리 주소의 변경을 의미한다.

  • let은 메모리 주소 변경을 허락한다.
  • const는 메모리 주소 변경을 할 수 없다.

1. const 사용이 권장되는 경우

// 1. 참조가 변경되지 않는 객체
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

// 2. 배열
const items = ['item1', 'item2'];

// 3. 함수 표현식
const calculateTotal = (items) => {
  return items.reduce((sum, item) => sum + item.price, 0);
};

// 4. 모듈 imports
const React = require('react');

2. let 사용이 권장되는 경우

// 1. 반복문의 카운터
for (let i = 0; i < array.length; i++) {
  // ...
}

// 2. 누적 값
let sum = 0;
array.forEach(item => {
  sum += item.value;
});

// 3. 상태 변경이 필요한 변수
let isLoading = true;
fetchData().then(() => {
  isLoading = false;
});

// 4. 재할당이 필요한 참조
let currentUser = null;
function updateUser(user) {
  currentUser = user;
}

3. 메모리 관리 관점에서의 차이

3.1 메모리 할당

// const: 할당 후 메모리 위치 고정
const fixedArray = [1, 2, 3];  // 메모리 위치 변경 불가

// let: 재할당 시 새로운 메모리 위치 사용 가능
let dynamicArray = [1, 2, 3];  // 초기 메모리 위치
dynamicArray = [4, 5, 6];      // 새로운 메모리 위치

3.2 가비지 컬렉션

function example() {
  // const: 스코프를 벗어나면 즉시 GC 대상
  const tempData = { large: new Array(10000) };

  // let: 재할당으로 인한 이전 값도 GC 대상
  let data = { large: new Array(10000) };
  data = null; // 이전 데이터는 GC 대상이 됨
}

4. 최적화 관점에서의 권장사항

// 좋은 예시
const STATIC_CONFIG = {
  maxRetries: 3,
  timeout: 5000
};

let currentState = 'idle';

// 피해야 할 예시
let config = {  // const를 사용하는 것이 더 적절
  maxRetries: 3,
  timeout: 5000
};

const state = 'idle';  // 변경이 필요하다면 let을 사용하는 것이 더 적절

5. 메모리 누수 방지

// ✅ 메모리 누수 방지를 위한 올바른 사용
function createCache() {
  const cache = new Map(); // 참조가 변경되지 않으므로 const
  let size = 0; // 값이 변경되므로 let

  return {
    add(key, value) {
      size++;
      cache.set(key, value);
    },
    clear() {
      size = 0;
      cache.clear();
    }
  };
}



SessionStack의 블로그 글에 따르면,

메모리 관리의 효율성을 위해서는 변수의 생명주기와 스코프를 명확히 하는 것이 중요하다.

const를 사용하면 개발자의 의도를 더 명확히 표현할 수 있고, 예기치 않은 재할당을 방지할 수 있다.

결론

  • 기본적으로 const 사용을 우선 고려
  • 재할당이 필요한 경우에만 let 사용
  • var는 사용을 피함 (스코프 문제와 호이스팅 이슈)

이러한 접근은 코드의 가독성을 높이고 메모리 관리를 더 예측 가능하게 만든다.







가비지 컬렉션

쓰레기 수집(garbage collection 가비지 컬렉션, GC)은 메모리 관리 기법 중의 하나로, 프로그램이 동적으로 할당했던 메모리 영역 중에서 필요없게 된 영역을 해제하는 기능이다.


Reference Counting

필요없는 메모리를 해제하는 알고리즘을 만들기 어려운데 이러한 메모리 해제 방식을 최대한 구현한 것이 Reference Counting 방식이다.

Reference Counting과 Mark and Sweep에 관한 정리가 잘 된글이다.

Reference Counting(참조 횟수 계산 방식): 메모리를 제어하는 방법 중 하나로 GC(garbage collection)의 한 방식이다.

구성 방식:

  • 어떤 한 동적 단위(객체, Object)가 참조값을 가지고 이 단위 객체가 참조(참조 복사)되면 참조값을 늘리고 참조한 다음 더 이상 사용하지 않게 되면 참조값을 줄이면 된다.
  • 보통 참조값이 0이 되면 더 이상 유효한 단위 객체로 보지 않아 메모리에서 제거된다.

Reference Counting 코드로 살펴보기

1  const referenceCountExam = () => {
2    let a = { name: 'hoho' };
3    let b = {};
4  
5    a = [];
6    b = a;
7  }   

2 Line에서 지역 변수 a에 { name: 'hoho' } 객체가 할당된다. 이때 { name: 'hoho' }에 대한 Reference Count는 1이다.

3 Line에서 지역 변수 b에 {} 객체가 할당된다. 이때 {}에 대한 Reference Count는 1이다.



5 Line에서 a에 새로운 배열 []을 할당한다. 이때 { name: 'hoho' }의 Reference Count는 0이 된다.



6 Line에서 b에 a를 재할당 하면서 []에 대한 Reference Count는 1이 더 증가하여 2가 되고, 이와 동시에 {}는 Reference Count가 0이 된다.

프로그램이 끝나는 시점(가비지 컬렉션(GC)은 특정 시점이나 조건에 따라 자동으로 실행된다.)에서 메모리에 선언되었던 참조 값들중 Reference Count가 1 이상인 것은 []이고, 나머지는 모두 Reference Count가 0이 된다.

Reference Count가 0인 객체는 Garbage Collection의 대상이 된다.





Circular Referencing in Reference Counting

2개 이상의 객체가 서로를 참조하는 경우가 있는데 이러한 경우를 순환 참조라고 부른다.

Circular Referencing in Reference Counting 코드로 살펴보기

1  const referenceCountExam = () => {
2    let a = { name: 'hoho' };
3    let b = {};
4    
5    // 순환 참조 생성    
6    a.otherObj = b;
7    b.otherObj = a;
8    
9    // 참조 해제
10   a = null;
11   b = null;
12   // 순환 참조된 객체들은 GC 대상이 됨
13 }   

2, 3 Line에서 지역 변수 a, b에는 각각 { name: 'hoho' }, {}가 할당되어
{ name: 'hoho' }, {} 모두 Reference Count가 1이 된다.



6 Line에서 a의 otherObj라는 키에 { name: 'hoho' }를 할당하고,
7 Line에서 b의 ohterObj라는 키에 {}를 할당하면서 { name: 'hoho' }, {}는 모두 Reference Count가 2가 된다.



10, 11 Line에서 지역 변수 a, b에 null을 할당하면서 참조를 해제할 때 { name: 'hoho' }, {} 객체는 Reference Count가 2에서 1로 줄어든다.

사실상 코드 내에서 { name: 'hoho' }, {}에 접근할 방법은 없지만 Reference Count가 1이기 때문에 GC의 수거 대상이 되지 않는다.

이렇게 객체가 서로를 참조하는 현상을 Circular Referencing(순환 참조)이라고 하고, 이러한 문제점을 해결하기 위해 Mark and Sweep이라는 기법이 만들어졌다.
이 두 객체는 변수에서 접근할 수 없기 때문에 메모리 힙 영역에 영원히 남게되어 메모리 누수의 원인이 된다.





Mark and Sweep

자바스크립트는 도달 가능성(reachability) 이라는 개념을 사용해 메모리 관리를 수행한다.

‘도달 가능한(reachable)’ 값은 쉽게 말해 어떻게든 접근하거나 사용할 수 있는 값을 의미하고 도달 가능한 값은 메모리에서 삭제되지 않는다.

예시를 통해 알아보기

// user엔 객체 참조 값이 저장
let user = {
  name: "John"
};


이 그림에서 화살표는 객체 참조를 나타낸다. 전역변수 "user"는 { name: "John" } 객체를 참조한다.

user의 값을 다른 값으로 덮어쓰면 참조(화살표)가 사라진다.

user = null;

메모리 생명 주기

1. 메모리 할당

변수, 함수, 객체 등을 만들 때 메모리를 할당

2. 메모리 사용

변수를 읽거나 쓸 때 할당된 메모리를 사용

3. 메모리 해제

더 이상 필요가 없어지면 메모리에서 해제



메모리 누수란?

메모리 누수는 부주의 또는 일부 프로그램 오류로 인해 더 사용되지 않는 메모리를 해제하지 못하는 것이다.
어떤 변수가 100M의 메모리를 점유한다 할 때, 이 변수가 사용되지 않더라도 수동 or 자동으로 해제되지 않아 계속 메모리를 점유하는 것을 말한다.



자바스크립트의 가비지 컬렉션

메모리 누수의 정의에 따르면 변수 or 데이터가 더 필요하지 않을 때 가비지 변수 or 가비지 데이터가 된다.
만약 그런 데이터가 메모리에 계속 쌓인다면, 결국엔 메모리 사용량을 초과하게 되는데 이 시점에서 가비지 데이터를 정리해야 한다.

가비지 컬렉션 메커니즘은 수동과 자동 두 가지 범주로 나뉜다.

  • C, C++는 수동 정리 메커니즘을 사용해 개발자는 변수를 위해 특정 양의 메모리를 할당받고 필요가 없어지면 수동으로 해당 메모리를 비워줘야 한다.
  • 반면 자바스크립트는 자동 정리 메커니즘을 사용한다. 모든 것을 자동으로 처리되기 때문에 우리는 얼마나 많은 메모리를 할당하고 비우든지 신경 쓸 필요가 없다. 하지만, 메모리 관리에 신경을 쓰지 않으면 메모리 누수가 발생할 것이다.



자바스크립트 가비지 컬렉션 메커니즘

일반적으로 전역 변수는 자동으로 정리되지 않는다. 그래서 로컬 스코프 메모리 수집에 초점을 맞출 것이다.

function fn1 () {
  let a = {
    name: 'hoho'
  };
  
  let b = 3;
  
  function fn2 () {
    let c = [1, 2, 3];
  }
  
  fn2();
  
  return a;
}

let res = fn1();

위 코드의 호출 스택은 아래 그림과 같다.

그림의 왼쪽은 스택 영역으로 실행 컨텍스트와 원시 타입의 데이터를 저장하는 데 사용되고, 오른쪽은 힙 영역으로 객체를 저장하는 데 사용된다.

fn2()가 실행될 때 콜 스택 안 실행 컨텍스트는 위에서 아래로 다음과 같이 존재한다.
fn2 함수 실행 컨텍스트 -> fn1 함수 실행 컨텍스트 -> 전역 실행 컨텍스트

함수 fn2가 실행을 완료할 때 화살표가 아래로 이동하며 fn2 실행 컨텍스트를 종료하게 된다.
그럼 아래 그림과 같이 fn2 실행 컨텍스트가 지워지고 스택 메모리 공간이 해제된다.


함수 fn1의 실행이 완료된 후 fn1 실행 컨텍스트를 종료할 때, 화살표가 다시 아래로 이동하며 fn1 실행 컨텍스트가 지워지고 해당 스택 메모리 공간이 해제된다.


이 시점에서 프로그램은 전역 실행 컨텍스트에 있다.

자바스크립트의 가비지 컬렉션은 가끔 호출 스택을 탐색하고 가비지를 수집한다.
이 시점에서 가비지 수집 메커니즘이 수행된다고 가정할 때, 가비지 수집기가 호출 스택을 순회하면서 변수 b, c가 사용되지 않음을 발견하여 가비지 데이터임을 확인하고 표시한다.

fn1 함수는 실행 후 변수 a를 반환하고, 전역 변수 res에 저장하므로 활성 데이터로 식별되고 그에 따라 표시한다.

유휴 시간(컴퓨터가 작동 가능한데도 작업을 하지 아니하는 시간. 주로 컴퓨터의 입력ㆍ출력을 위한 대기 시간을 이른다.)에 가비지 데이터로 표시된 모든 변수는 그림과 같이 해당 메모리를 해제하기 위해 지워진다.

정리

  1. 자바스크립트의 가비지 수집기 메커니즘은 자동으로 실행되고 태그는 가비지 데이터를 식별하고 정리하는 데 사용된다.

  2. 로컬 스코프를 떠난 후 해당 스코프의 변수가 외부 스코프에서 참조되지 않으면 나중에 지워진다.







profile
🌿

0개의 댓글