
Deepu K Sasidharan님의 🚀 Demystifying memory management in modern programming languages 시리즈를 많이 참고하여 정리하였습니다.
메모리 관리는 소프트웨어 어플리케이션이 실행될 때 컴퓨터 메모리를 제어하는 과정이다. 메모리 관리는 software engineering에서 아주 중요한 주제이다.
소프트웨어가 운영체제에서 실행될 때, 컴퓨터에 RAM(Random-access memory)을 다음을 위해서 필요로한다.
소프트웨어 프로그램이 사용하는 메모리에는 스택과 힙 메모리가 있다.
스택은 정적 메모리 할당을 위해 사용되며, LIFO(선입선출)의 구조이다.
stack overflow 에러가 날 수도 있다.
위는 Javascript에서 사용되는 스택으로, 객체는 힙에 저장되고 필요할 때 참조되는 예시이다.
힙은 동적 메모리 할당을 위해 저장된다. 스택과 달리 데이터 검색이 필요하고, 포인터를 주로 사용한다.
C, C++같은 언어는 개발자를 위해 메모리를 관리하지 않으며, 사용자가 생성한 개체에 대해 메모리를 할당하고 사용 가능하게 하는 것은 사용자에게 달려있다.
이런 언어는 malloc, realloc, calloc, call을 메모리를 관리하기 위한 메소드로 제공하면서, 프로그램에서 힙 메모리를 할당하고 해제하고 메모리 관리를 위해 포인터를 효율적으로 사용하는 것을 개발자에게 맡긴다.
사용되지 않는 메모리를 해제하면서 자동 힙 메모리 관리이다. GC는 현대 언어에서 가장 일반적인 메모리 관리 중 하나인데, 이 과정이 특정 간격으로 실행되는 경우가 많아 pause times라고 부르는 작은 오버헤드가 발생할 수 있다.
JVM(Java/Scala/Groovy/Kotlin), Javascript, C#, Golang, OCaml, Ruby는 기본적으로 메모리 관리에 GC를 사용하는 언어이다.

Mark & Sweep GC (Tracing GC): 이 방식은 2단계로 이루어지며, 처음엔 '활성'으로 참조되는 객체를 표시하고, 다음 단계에서는 활성화되지 않은 객체의 메모리를 해제한다.
Reference counting GC: 이 방식에서 모든 개체는 참조 수가 변경됨에 따라 증가하거나 감소하는 참조 수를 가지고, 카운트가 0이 될때 grabage collect 된다.
V8 Engine은 ECMAScript와 Webassembly를 위한 Google의 오픈 소스 엔진이다.
자바스크립트는 interpreted language이기 때문에 코드를 해석하고 실행하기 위해서 엔진이 필요한데, 이 V8 Engine이 Javascript를 해석해서 네이티브 머신 코드로 컴파일한다.
먼저, V8 engine의 메모리 구조를 보자.
Javscript는 싱글 스레드이기 때문에 Javascript의 컨텍스트 별로 프로세스를 사용한다. 그렇기 때문에 Service woker를 사용하면 각 worker 당 새로운 V8 프로세스가 생성된다.
실행 중인 프로그램은 항상 V8 프로세스에서 할당된 메모리로 표시되고, 이를 Resident Set이라고 한다. 이것은 아래와 같이 다른 세그먼트로 나뉜다.

V8은 힙 메모리에 Object나 동적 데이터를 저장한다. 메모리 영역에서 가장 큰 블록이고, garbage collect가 발생하는 곳이다.
전체 힙 메모리는 garbage collect되지 않고, 오직 Young / Old space만 GC를 통해 관리된다.
--min_semi_space_size(Initial)--max_semi_space_size(Max)--inital_old_space_size(Initial)--max_old_space_size(Max)*mmap: 메모리의 내용을 파일이나 디바이스에 mapping하기 위해서 사용하는 시스템 호출이다.
mmap은 프로세스의 주소 공간을 파일에 대응시킨다. 파일은 운영체제 전역적인 자원이므로 다른 프로세스와 공유해서 사용있게 된다.
Code-space
Call space, property call space, and map space
Cells, PropertyCells, Maps이 각각 포함된다.각 공간들은 페이지의 집합으로 구성된다.
페이지는 운영체제에서 mmap(or MapViewOfFile)으로 할당된 연속 메모리 덩어리이다.
Large object space를 제외하고 각 페이지의 크기는 1MB이다.
V8 프로세스당 하나의 스택이 있다.
메서드와 함수 프레임과 원시 값, object에 대한 포인터를 포함해서 정적 데이터가 저장된다.
--stack_size 플래그를 사용해서 스택 메모리 제한을 할 수 있다.
프로그램이 실행될 때 메모리의 가장 중요한 부분이 어떻게 사용되는지 알아보자.
아래 Javascript 프로그램을 사용해보자. 코드는 정확성보다는 스택과 및 메모리 사용을 시각화하는 것에 초점이 맞춰져 있다.
class Employee {
constructor(name, salary, sales) {
this.name = name;
this.salary = salary;
this.sales = sales;
}
}
const BONUS_PERCENTAGE = 10;
function getBonusPercentage(salary) {
const percentage = (salary * BONUS_PERCENTAGE) / 100;
return percentage;
}
function findEmployeeBonus(salary, noOfSales) {
const bonusPercentage = getBonusPercentage(salary);
const bonus = bonusPercentage * noOfSales;
return bonus;
}
let john = new Employee("John", 5000, 5);
john.bonus = findEmployeeBonus(john.salary, john.sales);
console.log(john.bonus);

Number & String과 같은 원시 값들은 스택에 바로 저장된다.Employee & Function과 같은 객체 유형들은 힙에서 생성되고 스택 포인터를 사용하여 스택에서 참조된다. 함수는 Javascript의 객체일 뿐이다.스택은 자동으로 관리되며 V8이 아니라 OS에 의해 관리된다. 개발자가 스택에 대해 크게 신경쓸 필요는 없다.
그러나, 힙은 OS에 의해 자동으로 관리되지 않고 동적 데이터를 저장하기 때문에 시간이 지나면서 기하급수적으로 커져서 프로그램의 메모리가 부족해질 수 있고, 어플리케이션 속도가 느려지게 된다.
그래서 garbage collection이 필요한 것이다.
heap에서 포인터와 데이터를 구분하는 것은 가비지 수집에 중요하다. V8에서 이걸 위해 "Tagged pointers" 접근 방식을 사용한다. 이 방식에서는 각 단어의 끝에 비트를 예약하여 포인터인지 데이터인지 나타낸다. 이런 방식은 제한적인 컴파일러 지원을 필요하지만, 구현이 간단하면서도 상당히 효율적이다.
V8은 garbage collection을 통해 heap 메모리를 관리한다.
garbage collection은 스택에서 더 이상 직접적이든 간접적이든 참조되지 않는 object에 대해서 메모리를 해제해서 다른 새로운 object를 생성할 수 있는 공간을 만드는 것이다.
+) Orinoco는 garbage collection을 위해 메인 스레드를 해제하는 V8 GC 프로젝트의 코드 이름이다.
V8의 garbage collector는 사용되지 않는 메모리를 회수하는 역할을 한다. 다음과 같은 두 단계와 세 가지 알고리즘이 사용된다.
New Space의 공간을 작고 깨끗하게 유지한다.
Object들은 1~8MB 사이의 크기인 작은 new-space에 할당된다. new space에서의 할당은 메모리를 많이 필요로 하지 않는다. 새로운 object를 위한 공간을 얻고자 할 때마다 증가하는 할당 포인터가 있다.
(다음에 이어서 작성...)
+) 추가로 공부해볼 것, javascript의 변수에 대해서
+) 다음을 위해 keep해놓는 같은 분의 동시성 시리즈 Concurrency in modern programming languages: Introduction