[부스트캠프 웹·모바일 8기] 챌린지 4일차 학습 정리

허지예·2023년 7월 13일
post-thumbnail

Deepu K Sasidharan님의 🚀 Demystifying memory management in modern programming languages 시리즈를 많이 참고하여 정리하였습니다.


🚀 프로그램의 메모리 관리

메모리 관리는 소프트웨어 어플리케이션이 실행될 때 컴퓨터 메모리를 제어하는 과정이다. 메모리 관리는 software engineering에서 아주 중요한 주제이다.

소프트웨어가 운영체제에서 실행될 때, 컴퓨터에 RAM(Random-access memory)을 다음을 위해서 필요로한다.

  • 실행할 프로그램의 실행 코드를 불러오기 위해서
  • 실행되는 프로그램에서 사용하는 데이터 값데이터의 구조를 저장하기 위해서
  • 실행되는 프로그램에서 요구하는 런타임 시스템을 불러오기 위해서

소프트웨어 프로그램이 사용하는 메모리에는 스택과 힙 메모리가 있다.

스택 (Stack)

스택은 정적 메모리 할당을 위해 사용되며, LIFO(선입선출)의 구조이다.

  • 스택에서 데이터를 저장하고 검색하는 프로세스는 탐색이 필요하지 않으므로, 스택의 맨 위 블록에서 데이터를 저장하고 검색하는 속도가 매우 빠르다.
  • 그렇기 때문에 저장된 모든 데이터가 차지하고 있는 공간이 유한하고 정적이어야 한다.
  • 함수의 실행 데이터가 스택 프레임으로 저장되는 곳이기도 하다. 각각의 프레임은 해당 기능에 필요한 데이터가 저장되는 공간 블럭이다.
    • 예를 들어, 함수가 새로운 변수를 선언할 때마다 스택 맨 위에 푸시되고, 함수가 종료될 때마다 맨 위 블록이 지워진다. 즉, 그 함수에 의해 선언된 변수들이 모두 지워진다.
  • 멀티 스레드 어플리케이션은 각 스레드에 스택을 가질 수 있다.
  • 스택의 메모리 관리는 간단하며, OS에 의해서 이루어진다.
  • 스택에 저장되는 것들은 일반적으로 local 변수(값 유형, 원시 값, 상수), 포인터함수 프레임이다.
  • 스택의 크기가 힙에 의해 제한되기 때문에, stack overflow 에러가 날 수도 있다.
  • 대부분의 언어에 대해 스택에 저장될 수 있는 값의 크기에는 제한이 있다.

위는 Javascript에서 사용되는 스택으로, 객체는 힙에 저장되고 필요할 때 참조되는 예시이다.

힙 (Heap)

힙은 동적 메모리 할당을 위해 저장된다. 스택과 달리 데이터 검색이 필요하고, 포인터를 주로 사용한다.

  • 프로세스의 데이터 검색이 필요하고 더 많은 데이터를 저장하기 때문에, 스택에 비해 느리다.
  • 데이터의 사이즈가 변경될 수 있음을 의미한다.
  • 힙은 스레드간에 공유해서 사용한다.
  • 동적 특성으로 인해 힙은 관리하기가 어려워서 언어의 자동 메모리 관리 솔루션이 존재한다.
  • 힙에 저장되는 것들은 일반적으로 전역 변수, 객체, 문자열, 맵 등 복잡한 데이터 구조와 같은 참조 유형이다.
  • 할당된 힙보다 더 많은 메모리를 사용하려고 하면 메모리 부족 오류가 발생할 수 있다. 이 문제를 위해 GC, 압축과 같은 다른 요소가 많이 있다).
  • 일반적으로 힙에 저장할 수 있는 값의 크기에는 제한이 없다. 물론 애플리케이션에 할당되는 메모리 양의 상한이 있다.

수동 메모리 관리

C, C++같은 언어는 개발자를 위해 메모리를 관리하지 않으며, 사용자가 생성한 개체에 대해 메모리를 할당하고 사용 가능하게 하는 것은 사용자에게 달려있다.

이런 언어는 malloc, realloc, calloc, call을 메모리를 관리하기 위한 메소드로 제공하면서, 프로그램에서 힙 메모리를 할당하고 해제하고 메모리 관리를 위해 포인터를 효율적으로 사용하는 것을 개발자에게 맡긴다.

Garbage Collection (GC)

사용되지 않는 메모리를 해제하면서 자동 힙 메모리 관리이다. 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에서의 메모리 관리 시각화

V8 Engine은 ECMAScript와 Webassembly를 위한 Google의 오픈 소스 엔진이다.

자바스크립트는 interpreted language이기 때문에 코드를 해석하고 실행하기 위해서 엔진이 필요한데, 이 V8 Engine이 Javascript를 해석해서 네이티브 머신 코드로 컴파일한다.

V8 engine의 메모리 구조

먼저, V8 engine의 메모리 구조를 보자.

Javscript는 싱글 스레드이기 때문에 Javascript의 컨텍스트 별로 프로세스를 사용한다. 그렇기 때문에 Service woker를 사용하면 각 worker 당 새로운 V8 프로세스가 생성된다.

실행 중인 프로그램은 항상 V8 프로세스에서 할당된 메모리로 표시되고, 이를 Resident Set이라고 한다. 이것은 아래와 같이 다른 세그먼트로 나뉜다.

힙 메모리

V8은 힙 메모리에 Object나 동적 데이터를 저장한다. 메모리 영역에서 가장 큰 블록이고, garbage collect가 발생하는 곳이다.

전체 힙 메모리는 garbage collect되지 않고, 오직 Young / Old space만 GC를 통해 관리된다.

  • New Space (Young gereration)
    • 새로 생성된 개체가 있는 곳이며 이런 Object들은 거의 수명이 짧다.
    • Scavenger(Minor GC)에 의해 관리된다.
    • New Space의 크기는 다음 플래그를 사용해 제어할 수 있다.
      • --min_semi_space_size(Initial)
      • --max_semi_space_size(Max)
    • 2개의 Semi space를 가지고 있다.
  • Old Space (Old gereration)
    • 두 개의 작은 GC 사이클 동안 "New space"에서 살아남은 Object들이 이동하는 곳이다.
    • Major GC(Mark-Sweep & Mark-Compact)에 의해 관리된다.
    • Old space의 크기는 다음 플래그를 사용해 제어할 수 있다.
      • --inital_old_space_size(Initial)
      • --max_old_space_size(Max)
    • 다음과 같이 두 부분으로 나눠진다.
      • Old pointer space: 다른 Object에 대한 포인터가 있는 생존 Object
      • Old data space: 다른 Object에 대한 포인터 없이 데이터만 포함하는 Object
  • Large object space
    • 다른 공간의 크기 제한보다 큰 object들이 있는 곳이다.
    • 각 object는 고유한 *mmap된 메모리 영역을 가진다.
      • garbage collector에 의해 이동되지 않는다.

*mmap: 메모리의 내용을 파일이나 디바이스에 mapping하기 위해서 사용하는 시스템 호출이다.
mmap은 프로세스의 주소 공간을 파일에 대응시킨다. 파일은 운영체제 전역적인 자원이므로 다른 프로세스와 공유해서 사용있게 된다.

  • Code-space

    • Just In Time(JIT)가 컴파일된 코드 블록을 저장하는 곳이다.
    • 실행 메모리가 있는 공간이다.
  • Call space, property call space, and map space

    • Cells, PropertyCells, Maps이 각각 포함된다.
    • 각 공간에는 모두 크기가 동일하고 가리키는 object의 종류에 제한이 있는 object이므로, collection이 단순화된다.

각 공간들은 페이지의 집합으로 구성된다.

페이지는 운영체제에서 mmap(or MapViewOfFile)으로 할당된 연속 메모리 덩어리이다.

Large object space를 제외하고 각 페이지의 크기는 1MB이다.

스택 메모리

V8 프로세스당 하나의 스택이 있다.
메서드와 함수 프레임과 원시 값, object에 대한 포인터를 포함해서 정적 데이터가 저장된다.

--stack_size 플래그를 사용해서 스택 메모리 제한을 할 수 있다.

V8 메모리 사용 (Stack vs Heap)

프로그램이 실행될 때 메모리의 가장 중요한 부분이 어떻게 사용되는지 알아보자.

아래 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);

  • Global scope는 스택의 "Global frame"에 유지된다.
  • 모든 함수 호출은 프레임 블록으로 스택 메모리에 추가된다.
  • 함수의 매개변수와 반환 값을 포함한 모든 local 변수들은 스택의 함수 프레임 내에 저장된다.
  • Number & String과 같은 원시 값들은 스택에 바로 저장된다.
    (global 범위에도 적용)
  • Employee & Function과 같은 객체 유형들은 힙에서 생성되고 스택 포인터를 사용하여 스택에서 참조된다. 함수는 Javascript의 객체일 뿐이다.
    (global 범위에도 적용)
  • 현재 함수에서 호출된 함수는 스택 위에 push 된다.
  • 함수가 반환되면, 프레임이 스택에서 제거된다.
  • 기본 프로세스가 완료되면 heap에는 더 이상 스택의 포인터가 필요없기 때문에 분리된다.
  • 명시적으로 복사하지 않는 한 다른 object 내에 모든 개체 참조는 참조 포인터를 사용하여 수행된다.

스택은 자동으로 관리되며 V8이 아니라 OS에 의해 관리된다. 개발자가 스택에 대해 크게 신경쓸 필요는 없다.

그러나, 힙은 OS에 의해 자동으로 관리되지 않고 동적 데이터를 저장하기 때문에 시간이 지나면서 기하급수적으로 커져서 프로그램의 메모리가 부족해질 수 있고, 어플리케이션 속도가 느려지게 된다.
그래서 garbage collection이 필요한 것이다.

heap에서 포인터와 데이터를 구분하는 것은 가비지 수집에 중요하다. V8에서 이걸 위해 "Tagged pointers" 접근 방식을 사용한다. 이 방식에서는 각 단어의 끝에 비트를 예약하여 포인터인지 데이터인지 나타낸다. 이런 방식은 제한적인 컴파일러 지원을 필요하지만, 구현이 간단하면서도 상당히 효율적이다.

Garbage collection

V8은 garbage collection을 통해 heap 메모리를 관리한다.

garbage collection은 스택에서 더 이상 직접적이든 간접적이든 참조되지 않는 object에 대해서 메모리를 해제해서 다른 새로운 object를 생성할 수 있는 공간을 만드는 것이다.

+) Orinoco는 garbage collection을 위해 메인 스레드를 해제하는 V8 GC 프로젝트의 코드 이름이다.

V8의 garbage collector는 사용되지 않는 메모리를 회수하는 역할을 한다. 다음과 같은 두 단계와 세 가지 알고리즘이 사용된다.

Minor GC (Scanvenger)

New Space의 공간을 작고 깨끗하게 유지한다.
Object들은 1~8MB 사이의 크기인 작은 new-space에 할당된다. new space에서의 할당은 메모리를 많이 필요로 하지 않는다. 새로운 object를 위한 공간을 얻고자 할 때마다 증가하는 할당 포인터가 있다.

(다음에 이어서 작성...)

Major GC

+) Process 노드 모듈



📝memo

+) 추가로 공부해볼 것, javascript의 변수에 대해서
+) 다음을 위해 keep해놓는 같은 분의 동시성 시리즈 Concurrency in modern programming languages: Introduction

profile
대학생에서 취준생으로 진화했다가 지금은 풀스택 개발자로 2차 진화함

0개의 댓글