[ JavaScript ] 메모리 관리 (feat. 가비지 컬렉터)

exceed_96·2024년 2월 12일

JavaScript

목록 보기
14/18
post-thumbnail

JavaScript로 코드를 짜다 보면 메모리에 직접적으로 접근할 일이 별로 없다. 그러다 보니 가벼운 마음으로 변수, 객체, 함수 등을 생성하거나 이벤트 등을 등록하게 된다. 하지만 메모리를 신경 쓰지 않고 코드를 작성할 경우 크나큰 문제에 직면할 수 있는데 이를 방지하기 위해서 JavaScript에서는 메모리를 어떻게 관리하는지 또한 많이 들어본 "가비지 컬렉션(GC)"는 어떤 식으로 동작하는지 알아보자.


1. 언어에 따른 메모리 관리

c, c++ 같은 로우 레벨의 언어를 먼저 공부하고 JavaScript를 접하다 보면 메모리 관련 작업에서 신경 써야 할 부분이 생각(?)보다 없어서 코드 짜기에 굉장히 수월하다고 느낄 수 있다.

물론 필자도 c, c++을 처음으로 접해본 프로그래밍 언어여서 그런지 JavaScript로 코드를 짜다 보면 malloc, free, new, delete와 같은 메서드를 사용하지 않고 동적으로 메모리를 할당할 수 있어서 메모리 혹은 메모리 누수에 대해 신경을 많이 안 쓰고 있었다.

위와 같이 "Low-Level Language(c, c++)
"에서는 메모리를 직접 관리해 줘야 하는데 이러한 언어를 managed language라고 부른다.

이와 반대로 JavaScript, Java, Python 같은 "High-Level Language"에서는 메모리를 직접 관리하는 게 아닌 알아서 관리해 주는 언어를 unManaged Language라고 부른다.


#include <stdio.h>
#include <stdlib.h>

int main() {
    // 포인터를 사용하여 메모리를 동적으로 할당합니다.
    int *ptr = (int *)malloc(sizeof(int));

    // 할당된 메모리에 값을 저장합니다.
    *ptr = 10;

    // 메모리를 해제합니다.
    free(ptr);

    return 0;
}

c에서는 위와 같이 malloc를 통해서 내가 사용하고자 싶은 크기의 메모리를 직접 할당해 줘야 하고 더 이상 해당 메모리를 사용하지 않을 때free를 통해서 사용한 메모리를 반납해 줘야 한다.



2. JavaScript에서의 메모리 할당


JavaScript는 Stack, Heap 두 영역에 메모리가 저장된다.

2.1 원시값

const a = 10;
let b = 20;

"원시값"의 경우 Stack에 저장되며 고정된 메모리를 할당하게 된다.

원시값의 경우 사전에 공간을 얼마나 사용할 수 있을지 정의할수 있어서 고정된 메모리를 할당하게 된다.


고정된 메모리를 할당하기 때문에 정적 메모리 할당이라고 한다.

여기서 "원시 값"이란 기본 데이터로 객체를 제외한 모든 것을 말한다.

예를 들면 Number, String, Boolean, null, undefined, Symbol 등이 원시 값에 해당된다.

만약 선언되어 있는 변수에 새로운 값을 할당하는 경우 어떻게 메모리에 올라갈까?

let a = 10;
let b = 20;
a = 20;

위 코드를 실행하면 "a"와 "b"에 저장된 메모리 주소값이 동일해진다.

즉, "a", "b"에 20이 저장된 메모리 주소값이 할당되는 것이다.


let a = 10;
let b = 20;
a = 20;
b = 30;

위 코드를 실행하면 "b"의 주소값이 가르키는 값인 20을 30으로 교체하는게 아닌 새로운 메모리 공간을 확보하여 30을 저장한다. 그 후, 원래의 주소 값을 새롭게 생긴 주소값으로 교체한다.

그 후, 더이상 사용하지 않는 메모리는 가비지 컬렉터에 의해서 수거된다.


2.2 참조값

"참조값"의 경우 Heap에 저장된다.

여기서 "참조값"이란 "객체, 배열"과 같은 참조 타입의 값을 가르키는 값을 의미한다.

즉, Stack에 실제 할당한 값을 가르키는게 아닌 해당 값이 저장된 메모리 위치를 가르키는 주소를 저장한다.

다만 "참조값" 데이터가 저장된 메모리 Heap의 주소값은 Stack에 저장된다.

위와같이 "원시값"인 "a"변수는 Stack에만 저장하고 "참조값"인 배열, 객체, 함수 등은 해당 값을 Heap에 저장하고 해당 메모리 주소를 Stack에 저장한다.

참고로 Stack에 있는 "원시값"이나 "참조값"은 해당 변수가 정의된 함수 실행 컨텍스트의 Lexical Environment에 정의된다.

JavaScript 실행 컨텍스트
만약 실행컨텍스트의 Lexical Environment에 관해 궁금증이 생긴다면 위 링크를 참고하길 권장한다.



3. 가비지 컬렉터(GC)

가비지 컬렉터는 주기적으로JavaScript 엔진이 메모리를 해제하는 방식이다.

가비지 컬렉터에서 해당 메모리 할당이 더 이상 필요없어 지게 된다면 메모리에서 해제해준다.

여기에서 생각해야 할 점은 컴퓨터 자체로 해당 메모리가 여전히 필요한지 아닌지를 결정할 수 없다.

즉, 메모리를 해제하는 알고리즘을 만들어서 해당 알고리즘이 사용하지 않는 메모리를 찾는 방법밖에 없다.

3.1 Reference Counting

Reference Counting방식은 자기자신을 참조하는 객체수를 카운트 하는 방식이다.

즉, 자기자신을 참조하고 있다는건 로직상 특정부분에서 사용되고 있다는 것이고 메모리에서 해제하지 않는 것이다.

만약 자기자신을 참조하는 값이 0이라면 해당 메모리를 해제하는 방식이다.

하지만 위 방식은 순환참조문제가 발생한다.

let foo = {};
let bar = {};

foo.bar = bar;
bar.foo = foo;

foo = null;
bar = null;

위 코드는 "foo"객체와 "bar"객체를 생성하고 각각의 객체 안에 서로를 참조하게 만드는 로직을 구성했다.

위와같이 하면 "foo"의 주소값은 Heap영역에 있는 해당 데이터를 참조하고 있고 "bar"도 마찬가지로 Heap영역에 있는 데이터를 가르키게 된다.

여기서 foo.bar = bar;를 정의하면 "foo"의 객체값은 "bar"를 가르키게 되고 bar.foo = foo;는 "foo"를 가르키게 된다.

즉, null로 선언한다 하여도 해당 선언은 Stack에서 Heap을 참조하고 있는 연결만 끊어버리기 때문에 메모리 영역이 해제되지 못하고 메모리 누수가 발생하게 된다.


3.2 Mark and Sweep

Reference Counting의 순환참조 문제를 해결하기 위해서 Mark and Sweep방식이 도입되었다.

해당 방식은 루트에서 도달 가능한 객체를 표시하고 도달 불가능한 객체를 수거하는 방식이다.

여기서 "Global"로 표시된 부분은 JavaScript에서 Window객체이다.

위와같이 오른쪽에 루트(Global)에서 도달 가능하지 못한 부분은 가비지 컬렉터의 대상이 되어서 메모리를 수거하게 된다.

순환참조문제를 해결한 Mark and Sweep이지만 이 방식도 문제점이 존재한다.

해당 방식으로 가비지 컬렉션을 동작하기 위해서는 실행 중인 프로그램을 일시 중단해야 한다.

이러한 일시 중지는 대규모 프로젝트에서 메모리를 크게 사용하고 있을 때 성능 문제를 발생시킨다.

또한 실행 중인 프로그램의 모든 객체를 스캔하여 표시해야 하는데 이 작업은 전체 메모리를 순회해야 하므로 자원 소비가 많을 수 있다.

마찬가지로 대규모 프로젝트에서 메모리를 크게 사용하고 있을 때오버 헤드문제가 발생한다.



4. 마치며

이번 포스팅에서는 JavaScript에서 동작하는 가비지 컬렉터에 대해서 알아보았다.

가비지 컬렉터는 JavaScript 말고도 Python, Java 등등 많은 언어에서 메모리 관리에 쓰이는 방식이다.

개발자가 필요한 양만큼 메모리를 할당해서 사용하고 사용한 메모리는 꼭 해제시켜줘야 누수가 안 나는데 위 일련의 과정을 가비지 컬렉터가 대신해주는 것이다.

다만 알아서 해준다고 해서 무작정 편한 것은 아니다.

Reference Counting의 순환 참조 문제, Mark and Sweep의 자원 소모 문제와 실행프로그램 일시 중지 문제 등 완벽한 방식은 없으므로 지속적인 최적화가 반드시 필요한 방식들이다.



5. Reference

https://fe-developers.kakaoent.com/2022/220519-garbage-collection/
https://developer.mozilla.org/ko/docs/Web/JavaScript/Memory_management
https://www.youtube.com/watch?v=24f2-eJAeII
https://www.youtube.com/watch?v=Rp_-WJlXqHU

profile
개발진행형

0개의 댓글