2024-07-18 (TIL)

SanE·2024년 7월 18일
0

컴퓨터공학

목록 보기
13/23

📚 오늘 학습 내용


1. 메모리 구조

JavaScript 메모리 공간

  • Heap (힙): JavaScript 객체 인스턴스가 할당되는 메모리 영역. V8 엔진에서는 힙을 관리하기 위해 각 객체의 할당과 해제를 추적, 필요할 때 가비지 컬렉션을 수행하여 사용하지 않는 객체를 해제.

  • Stack (스택): 함수 호출 시 지역 변수, 함수 매개변수 및 함수 호출 스택을 저장하는 영역. 각 함수가 호출될 때마다 해당 함수의 실행 컨텍스트가 스택에 push, 함수가 종료되면 pop.

2. 메모리 관리 기법

  • 가비지 컬렉션 : 자바스크립트 엔진에서 가비지 컬렉션은 사용하지 않는 객체를 자동으로 식별해 해제하는 방식으로 진행된다.
  • 메모리 리미트 설정 : node js는 기본적으로 메모리 사용에 대한 제한 X. 하지만 --max-old-space-size 와 같은 CLI 옵션을 통해 제한 설정 가능.

3. 메모리 관련 도구

3 - 1. heapdump

메모리 덤프를 생성해 현재 힙 상태 분석하는 도구.

설치

npm install heapdump

사용법

const heapdump = require('heapdump');

// 메모리 덤프 생성
heapdump.writeSnapshot('/tmp/heapdump.heapsnapshot');

3 - 2. node-inspect

Chrome 개발자 도구와 연계해 실시간으로 node js 메모리 상태를 모니터링할 수 있다.

사용법

  1. Node.js 프로세스를 디버그 모드로 실행합니다.
  node --inspect app.js
  1. Chrome 브라우저에서 chrome://inspect로 접속하여 연결합니다.

3 - 3. profiler

CPU, 메모리 사용량 등을 분석하는 도구.
다음과 같은 기능 제공.

  • CPU 프로파일링
  • 메모리 프로파일링
  • 이벤트 추적 및 분석

예를 들어, Chrome 개발자 도구의 프로파일러를 사용하거나, Node.js의 --prof 옵션을 활용하여 프로파일링 데이터를 수집할 수 있습니다.

👨🏻‍💻 가비지 컬렉터 (GC)


V8에서 GC는 힙 영역의 new space와 old space에서 일어나고 각각의 영역에서 GC는 서로 다르게 동작한다.

  • New space : 마이너 GC
  • Old space : 메이저 GC

본격적으로 GC에 대해 설명하기 전에 The Generational Hypothesis 에 대해 알아야 한다.

The Generational Hypothesis는 새로 만들어진 객체가 오래된 객체보다 쓸모없어질 가능성이 높다는 가설이다. 따라서 오래된 객체가 쓸모 없어질 가능성이 낮은데 GC가 모든 객체를 매번 검사하는 것은 비효율적이다.

마이너 GC

New space 에는 2개의 semi space 가 있다. 그 중 하나를 From space, 다른 하나를 To space 라고 한다.

위의 그림처럼 From space에서 새로운 객체들이 있다가 마이너 GC에서 살아남은 객체들은 아래에 있는 To space로 대피를 하게 됩니다.

이렇게 To space로 이동하는 과정을 통해 메모리 단편화가 줄어들게 됩니다.

객체 복사 과정에서 참조 업데이트가 함께 이루어져 접근 속도가 빨라진다.

이렇게 마이너 GC에서 살아남은 객체들이 다 살아남았다면 From space의 모든 객체들을 비우고 To space가 된다. 그리고 기존의 To space는 From space가 된다.

이제 다시 새로운 객체를 할당했을 때, From space의 다음 빈주소에 할당된다.

마이너GC에서 2번 생존한 2개의 객체는 Old space로 이동된다.

새로 수집된 객체는 1번 생존하면 To space로 이동한다.

메이저 GC

Old space에서 진행되는 메이저 GC는 아래와 같이 2가지 알고리즘을 이용한다.

  • Mark-Sweep-Compact 알고리즘
  • Tri-color 알고리즘

기본적인 로직은 참조되지 않는 객체를 쓸모없는 객체로 간주하며 진행된다.

메이저 GC는 크게 보면 3가지 단계를 걸쳐서 동작한다.

  1. 마킹.
  2. 스위핑.
  3. 압축.

이제 이 단계를 알아보자

마킹

마킹 과정은 Roots 라는 실행 스택과 전역 객체를 담고 있는 객체의 set에서 시작되어 DFS를 이용해 순회하면 Tri-color로 마킹한다.

  • 흰색 : 탐색하지 못한 상태
  • 회색 : 탐색은 했으나 해당 객체가 참조하는 객체가 있는지 확인을 못한 상태
  • 검정색 : 객체가 참조하는 객체까지 확인 완료
  1. 모든 객체를 흰색으로 초기화.
  2. roots에 회색으로 마킹 후 Deque 에 push_front

  1. Deque에서 pop_front를 통해 꺼냄.
  2. 꺼낸 값을 검정으로 마킹.
  3. 꺼낸 객체가 참조하는 객체를 탐색.
    1. 흰색일 경우.
      1. 회색으로 마킹.
      2. Deque에 push_front
    2. 회색 or 검정색일 경우
      1. 이미 방문한 객체이므로 건너뜀.

  1. 위의 과정 반복.
  2. 최종 결과물 : 모든 객체가 흰색 or 검정색.

스위핑

흰색의 객체들을 free-list라는 자료구조에 추가. ( 이 주소 공간에 다른 값을 저장할 수 있도록함.)

압축

메모리 단편화가 심한 페이지들을 재배치 → 추가적인 메모리 확보.

문제점

위에서 말하는 마이너 GC, 메이저 GC를 통해 가비지 컬렉션이 수행할 때 프로그램이 멈추게 된다.

이를 stop-the-world라고 한다. 이렇게 멈추는 시간이 길어질수록 UX가 매우 나빠지게 된다.

이를 해결하기 위해 Orinoco 프로젝트를 통해 GC이 발전했고 아래는 간단하게 설명한 최신 GC들이다.

  1. 병렬 마킹(Parallel Marking):
    • 다중 스레드를 사용하여 객체의 도달 가능성을 빠르게 마킹합니다.
    • 기존에 혼자하던 일을 Helper Thread와 균등하게 나누어서 일한다.
    • Thread 간 동기화를 해야하기 때문에 오버헤드가 발생하지만, 그래도 stop-the-world 시간 크게 감소.
  2. 동시성 컴팩션(Concurrent Compaction):
    • Main Thread는 가비지 컬렉션을 하지 않는다.
    • 가비지 컬렉션은 Helper Thread가 진행.
    • 가비지 컬렉션이 백그라운드에서 실행되어 stop-the-world 시간이 전혀 없다.
    • 하지만, 기술적으로 구현이 어렵다.
  3. 점진적 백업(Incremental Fallback):
    • 가비지 컬렉션 작업을 한꺼번에 하지 않고 여러 단계로 나눠서 진행.
    • 더 부드러운 실행이 가능.

🧐 후기


그 동안 JavaScript 가비지 컬렉팅에 대해 '엔진에서 자동으로 진행하기 때문에 따로 메모리를 해제할 필요가 없다' 이정도만 알고 있었는데 이번 기회에 V8엔진에서 진행되는 가비지 컬렉팅에 대해 확실하게 이해할 수 있었고, 그것과 더불어 전에 블로그 글에서 작성했듯 참조형 변수와 기본형 변수가 어떻게 저장되는지는 알았는데 그것을 스택과 힙 구조와 합쳐서 이해하지는 못했는데 이번 가비지 컬렉팅을 정리하며 기존에 알고 있던 지식과 합쳐서 확실하게 이해할 수 있었다.

profile
완벽을 찾는 프론트엔드 개발자

0개의 댓글