프로세스와 프로세스 메모리
프로세스란?
프로세스(Process)는 ‘실행중인 프로그램’ 이라는 간단한 의미를 가지고 있지만 사실 말만큼 그렇게 간단하지는 않다 프로그램은 보조기억장치에 단순 데이터로 위치하다가 해당 프로그램을 가져와 메모리에 적재하고 실행하면서 프로세스가 되는 것이다. 여기서 프로그램이 실행되면서 프로세스를 생성한다 고 표현한다.
프로세스 확인하기
윈도우에서는 프로세스 탭에서 실행 중인 프로세스를 확인할 수 있고 유닉스 체계 운영체제에서는 ps 명령어로 확인할 수 있다.
리눅스에서 ps를 눌렀을 때 뜨는 프로세스이다
가상 환경이라 현재는 실행중인 프로세스가 거의 없지만, 우리가 주로 사용하는 운영체제에서는 많은 프로세스가 실행되고 있다.
우리가 앞에서 실행하면서 보는 프로그램들의 프로세스는 포그라운드 프로세스라고 하고, 우리가 보지 않는 곳에서 열심히 일처리를 하는 프로세스들을 백그라운드 프로세스, 데몬(UNIX), 서비스(윈도우)등으로 불린다.
프로세스 제어 블록
프로세스는 CPU 자원을 가져와 프로그램을 실행시킨다. 하지만 CPU 자원은 한정되어 있기에 정해진 시간만큼 CPU를 이용하고, 타이머 인터럽트가 발생하면 자기에게 할당받았던 CPU를 돌려주고 다시 자신의 처리를 기다린다.
운영체제는 이 가운데에서 프로세스들의 실행 순서를 관리하고 프로세스에 CPU를 비록한 자원을 배분하는데, 여기서 이용하는 것이 프로세스 제어 블록(Process Control Block, PCB) 이라고 한다. PCB는 메모리의 커널 영역에 생성된다.
운영체제는 이 PCB를 기준으로 프로세스를 식별하고 처리하는데 필요한 정보를 판단하며, PCB는 프로세스가 실행될 시점에 생성되고 종료되면서 폐기된다.
PCB에 담기는 정보는 운영체제마다 차이가 있지만, 대표적인 정보는 아래와 같다.
- 프로세스 ID
- 프로세스를 식별하는 고유한 번호
- 레지스터 값
- 자신의 실행 차례가 돌아오면 원래 진행하던 작업들을 재개하기 위해 임시로 저장하는 값들.
- 이전까지 사용했던 레지스터의 중간값들을 모두 복원해 다시 사용한다.
- 프로세스 상태
- CPU를 기다리는 상태, 이용중인 상태 등
- CPU 스케줄링 정보
- 언제, 어떤 순서로 CPU를 할당받을지에 대한 정보
- 메모리 관리 정보
- 프로세스마다 메모리에 저장된 위치가 다르므로 기록해야 한다.
- 베이스 레지스터
- 한계 레지스터
- 페이지 테이블 정보
- 사용한 파일과 입출력장치 목록
- 프로세스에 할당된 입출력장치
- 이용한 파일들의 정보
문맥 교환
하나의 프로세스에서 다른 프로세스로 순서가 넘어가면 현재까지 계속 실행하고 있던 프로세스는 PCB의 정보들을 백업해두어 나중에 다른 프로세스들에게서 자신의 순서가 넘어왔을 때 이전에 했던 작업을 계속한다.
이러한 프로세스 수행 재개에 필요한 정보를 문맥(Context) 이라고 하는데, 이렇게 기존 프로세스의 문맥을 PCB에 백업하고, 새로운 프로세스를 실행하기 위해 문맥을 PCB로부터 복구하여 새로운 프로세스를 실행하는 것을 문맥 교환(Context Switching)이라고 한다.

프로세스 메모리 구조
프로세스가 커널 영역에서 PCB를 만들어 내고 사용자 영역에도 만들어 내는 메모리가 있다. 메모리 구조는 프로세스가 사용자 영역에 배치될 때의 프로세스 배치 방식이다.

스택 영역
- 프로그램이 자동으로 사용하는 영역으로, 호출된 함수의 수행을 마치고 복귀할 주소 및 데이터(지역변수, 매개변수, 리턴값 등)를 임시로 저장하는 공간
- 프로세스가 메모리에 로드될 때 스택 사이즈는 고정되며 함수 호출 시 생성되고, 함수 반환 시 소멸
- 스택사이즈가 고정이므로 재귀함수가 너무 깊거나, 함수 지역변수가 메모리를 초과하면 Stack Overflow가 발생
- 영역이름대로 스택 자료구조를 사용하여 LIFO(후입선출)의 형식이다.
- 명령 실행 시 자동으로 증가 또는 감소하기 때문에 보통 메모리의 마지막 번지를 지정한다
- data, stack 영역의 크기를 계산해 메모리 영역을 결정
힙 영역
- 프로세스 실행 과정에서 크기가 변할 수 있는 동적 할당 영역이다.
- 런타입에 크기 결정
- 메모리 주소 값에 의해서만 참조되고 사용됨
- 위의 스택과 같은 공간을 공유하며, 힙이 낮은 주소부터 할당되는데 각 영역이 상대공간을 침범하면 Stack Overflow, Heap Overflow가 발생한다.
- stack 에서 변수 할당 → pointer가 가리키는 heap 영역의 임의의 공간부터 원하는 크기만큼 할당해 사용
- 힙 영역은 메모리의 낮은 주소에서 높은 주소의 방향으로 할당
- 더 이상 해당 힙 영역을 참조하는 변수가 없을 경우 소멸되며, free() 함수로 나중에 할당했던 영역을 반납해야 한다.
- 할당한 메모리 공간을 반환하지 않으면 할당한 공간이 메모리 내에 계속 남아 메모리 낭비를 초래하는 메모리 누수(Memory Leak)를 일으킨다.
데이터 영역
- 전역변수, 정적변수, 배열과 같은 구조체 등이 저장되는 공간
- 일시적 데이터가 아닌 프로그램이 실행되는 동안 유지할 데이터들을 저장하는 공간이다.
- BSS영역과 GVAR영역으로 나뉨
- BSS영역
- 초기화되지 않은 데이터 저장되는 영역
- BVAR
- 초기화된 데이터가 저장되는 영역
- BSS영역
- 크기가 고정된 정적 할당 영역이다.
텍스트(코드) 영역
- 코드 영역이라고도 불리며, 말그대로 코드가 저장되는 공간
- 사용자가 작성한 프로그램 코드가 CPU에서 수행할 수 있는 기계어 명령 형태로 변환되어 저장되어 있음
- 컴파일 타임에 결정되고 중간에 코드를 바꿀 수 없게 Read-only로 되어있음
Node.js 에서의 프로세스 메모리
메모리 구조

먼저 V8에서 메모리 구조가 어떻게 구성되어 있는지 살펴보자. JavaScript가 싱글 스레드 언어인만큼, V8 역시 자바스크립트 context당 하나의 프로세스를 사용한다. 단, 서비스 워커를 사용하는 경우에만 워커의 개수만큼 프로세스를 증식한다. 실행중인 프로그램은 V8 프로세스에서 할당된 일정량의 메모리로 표현되고 이를 Resident set이라고 한다. Resident set은 위와 같은 구조로 구성된다.
힙 메모리
힙 메모리는 메모리 영역에서 가장 큰 블록이고 가비지 컬렉션이 발생하는 곳이다. 힙 메모리 전체에서 가비지 컬렉션이 동작하는 것은 아니고 Young generation과 Old generation영역에서만 실행된다. 힙 메모리는 더 세부적으로 나눌 수 있다.
- New Space
- 새로 만들어진 모든 객체를 저장하는 짧은 생명 주기의 객체.
- 안에 두 개의 작은 공간이 있으며
Scavenger라는 마이너 GC를 통해 관리한다. - New space의 크기는
--min_semi_space_size(초기값)와--max_semi_space_size(최대값) V8엔진의 플래그 값을 사용해 조정할 수 있다.
- Old Space
- Old Space는 이러한 두 개의 작은 공간을 마이너 GC가 순회할 동안 “new space”에서 살아남은 객체들이 이동하는 영역
- 메이저 GC(Mark-Sweep & Mark-Compact)가 관리한다.
- 두 개의 공간으로 나누어짐
- Old pointer space
- 살아남은 객체 중 다른 객체를 가리기는 포인터를 가지는 객체
- Old data space
- 데이터를 포함하는 객체
- New space에서 살아남은 문자열, 숫자, 부동 소수점을 가진 숫자 배열 등
- 큰 객체 공간
- 다른 공간들이 들어갈 수 있는 사이즈를 초과한 객체들이 있는 곳
- 가비지컬렉터가 회수해가지 않음
- 코드 공간
- Just In Time(JIT) 컴파일러가 코드 블럭을 저장하는 곳
- Cell Space, property cell space, map space
- 셀, 프로퍼티 셀, Map 자료구조 등이 들어가는 공간
- 모두 같은 크기
- 객체를 가리키는데 일정 제약 조건이 있음
스택
- 스택은 메모리 영역이고 V8마다 하나의 스택 스택은 메서드와 함수 프레임, 원시 값, 객체 포인터를 포함한 정적 데이터가 저장됨
- V8의 Stack영역의 관리는 OS에 위임하여 관리한다.
V8 엔진의 프로세스 메모리 관리
- 전역 스코프는 스택의 전역 프레임 안에서 관리한다.
- 모든 함수호출은 스택 메모리에 프레임 블록 형태로 담긴다.
- 인수와 리턴 값을 포함한 모든 지역 변수는 함수 프레임 블록에 담겨서 스택에 올라간다.
- Number나 String같은 원시 값들은 스택에 바로 담긴다.
- class나 function들은 모두 Object이다. Object는 힙에서 생성되어서 stack에서 stack pointer를 통해 참조된다.
- 함수가 리턴되면, 함수 프레임 블록은 스택에서 제거된다.
- 객체를 명시적으로 복사하지 않는다면, 레퍼런스만 복사된다.
V8 메모리 관리: Garbage Collection
스택 메모리는 V8이 아닌 OS가 관리하는 반면 Heap 영역은 OS가 관리하지 않는다. 더군다나 Heap 메모리는 가장 큰 메모리 영역을 가짐과 동시에 동적인 데이터를 들고 있어야 한다. 프로그램이 커진 경우 메모리가 부족해지는 문제가 발생할 위험 역시 커진다. 가비지 컬렉터가 필요한 이유이다.
힙 영역에서 포인터와 데이터를 구분하는 것은 가비지 컬렉션에서 중요한 부분이다. V8 엔진은 Tagged pointers를 사용해서, 단어의 마지막 비트를 통해 포인터인지 데이터인지를 구분한다.
Minor GC (Scavenger)
Scavenger는 new generation space(객체들이 할당되는 곳)를 깔끔하게 관리하는 역할로, 1-8MB 사이의 새롭게 생긴 공간을 관리한다. 새로운 공간을 할당하는 것은 우리가 공간에 새로운 객체를 넣을 때 포인터를 함께 할당한다. 할당한 포인터가 새로운 공간의 끝에 다다르게 되면 Scaenter 가비지 콜렉터가 작동되고 Cheney’s 알고리즘을 통해 보완한다.
새롭게 생긴 공간은 to-space와 from-space 두 개로 나뉜다.
- to-space
- from-space가 꽉 차면 가비지콜렉터가 보내는 곳
- from-space
- 특정 객체가 할당되는 곳
- 해당 칸이 차게 되면 가비지콜렉터가 작동함
- 해당 공간을 순회하면서 살아있는 객체(사용중인 객체)만을 to-space로 옯기고 포인터 업데이트
- 올기고 남은 객체가 있다면 비운다
Major GC
이 가비지 콜렉터는 기존의 공간들을 관리하는 역할이다. 해당 가비지콜렉터는 V8이 minor GC가 사이클을 돌았음에도 더이상 기존 공간들에 공간이 없다고 판단할 때 실행된다. Scavenger 알고리즘은 큰 공간에 적합하지 않기 떄문에 Mark-Sweep-Compact 알고리즘을 사용한다. 해당 알고리즘은 Mark-Sweep-Compact의 과정을 거치기 때문에 이러한 이름이 붙었다.
- Mark
- 객체들을 돌면서 사용중이지 않은 객체와 사용중인 객체를 구분해낸다.
- 사용중이거나 스택 포인터(GC 루트)를 통해 도달할 수 있는 것들만을 골라낸다.
- Sweeping
- 가비지 컬렉터가 힙을 돌면서 사용중이지 않은, 즉 변수의 참조값이 존재하지 않는 객체들과 사용중인 객체들을 기록한다.
- Compacting
- sweeping이 이루어진 후 필요한 경우를 제외하고 살아남은 객체들을 한 곳에 모아놓는다.
- 단편화를 줄이고 새로운 메모리 할당을 용이하게 하면서 성능을 높이기 위함이다.
메모리 누수
메모리 누수가 발생하기 쉬운 위치
전역변수
Mark-Sweep-Compact 알고리즘에서도 살펴 보았듯이, 전역객채의 Referencing chain에 해당하지 않는 값들이 garbage collection의 대상이 된다. 하지만 전역 변수와 같이 global scope에 존재하는 레퍼런스 값들은 무조건 전역객체가 레퍼런싱하게 되어, 가비지 컬렌션의 대상에 포함되지 않는다.
이러한 동작 때문에 전역변수는 메모리 누수의 위험이 있다.
다중참조
하나의 값을 여러 변수가 참조하는 경우, 하나의 레퍼런스 변수를 해제하더라도, 다른 레퍼런스 변수가 여전히 데이터를 참조하고 있기 때문에, 가비지 컬렉션의 대상이 되지 않는다. 이러한 이유 때문에, 다중참조에서 여러 개의 레퍼런스 변수를 해제하는 경우를 고려해야 한다.
클로저
클로저를 통해 스택 영역에 담긴 지역변수는 힙 영역으로 이동된다. 따라서 클로저가 제거되기 전까지는 가비지 컬렉터의 대상이 되지 않는다.
Callback 함수
callback을 참조하는 함수가 종료되지 않는 한, callback 함수에 존재하는 모든 참조 값들은 가비지 컬렉터의 대상이 되지 않는다.
메모리 누수 방지 방법
- 전역 변수 사용을 최소화한다.
- 비구조화 할당을 통해
heap영역의 데이터를stack영역에서 사용한다. - 클로저를 사용하는 경우 local 변수가
heap영역으로 이동되서 관리된다. 따라서 메모리 누수의 위험을 생긴다 - 다중 참조를 방지하기 위해 객체를 사용할 때,
spread연산자나,Object.assign()메서드를 통해 Deep copy한다.
누수검사
브라우저에 내장된 메모리 프로파일러를 사용하면, 메모리 누수를 쉽게 확인할 수 있다. 메모리 누수는 생성만 되고 반환은 안되는 상태이기 때문에, 메모리 사용량이 상승곡선을 그리면 메모리 누수를 의심 해볼만 하다.
리눅스/유닉스 계열 명령으로 가상 메모리 현황을 확인할 수 있는 방법을 학습하고 정리했다.
가상메모리란?
