우선 두 차이점을 알기 위해서 몇 가지 스텝을 통해 먼저 기본 지식을 정리하고 가자
이전 포스트에서 말 했듯이 프로세스에서 실행 되는 코드는 해당 프로세스가 돌아가는 스레드 내부에서 정의된 레지스터들의 값과 다양한 연산기들을 통해 메모리에 저장된 값들을 조회, 저장 해나가며 실행 된다. [1]
코드에서 필요한 데이터가 1000개가 존재한다고 가정 했을 때 1000개의 변수 모두가 레지스터에 저장 되는 것이 아닌
메모리에 저장되어 있다가 레지스터에서 호출되는 컨텍스트에 맞춰 변수가 저장 된 주소를 계산하여 레지스터로 값을 옮겨 담고 (load)
사용이 완료되어 값이 변경된 값은 레지스터에서 메모리로 다시 저장 (store) 해나가며 사용 된다.
결국 데이터는 메모리에 저장되고 레지스터에 옮겨와 사용된다.
일반적으로 인간이 알아들을 수 있는 수준의 언어 (고수준) 로 작성된 프로그래밍 언어를 컴퓨터가 알아 들을 수 있는 바이너리 코드 (저수준) 로 변경하기 위해선 컴파일 작업이 들어간다.
우선 자바스크립트 언어가 아닌 일반적인 형태의 컴파일 순서를 알아보자
고수준의 언어들을 기계어 혹은 기계어와 유사한 저수준 레벨까지 변환해가는 과정을 컴파일이라고 한다.[2]
이 단계에선 모듈형식으로 생성되어 있는 파일들을 읽어 나가며 저수준 언어로 이뤄진 오프젝트 파일들로 생성해 나간다.
이 때 오브젝트 파일에는 데이터들이 저장 되어야 할 가상의 메모리 주소 공간들과 해당 파일이 가지고 있는 종속성(다른 오브젝트 파일과의 import , export 관계) 등이 담겨 있다.
이렇게 만들어진 오브젝트 파일들을 링킹 과정을 통해 종속성 있는 파일들을 묶어 하나의 실행 파일로 생성해나간다.
이 때 컴파일 방식은 인터프리트 방식과 정적 컴파일 방식으로 나눌 수 있다.
정적 컴파일 방식은 고수준의 언어를 컴파일 할 때 기계어 수준까지 컴파일 하지만
인터프리트 방식은 기계어 수준까지 컴파일 하지 않고, 기계어 수준 전인 바이트 코드 수준까지만 컴파일 한 후 실행 해나가며 바이트 코드에 해당하는 기계어로 변경해 시행한다.
바이트 코드란 가상 머신을 통해 기계어로 손쉽게 변환 될 수 있는 코드를 의미 한다.
JIT 방식은 런타임 때 바이트 코드로 컴파일 된 코드를 실행해나가며
바이트 코드에 해당하는 기계어를 찾아 실행해나가는 방식을 의미 한다.
JIT 방식은 실행에 필요한 부분만 인터프리팅 하고, 인터프리팅 시 사용된 기계어들은 캐싱되기 때문에 실행되었던 동일한 코드에 대해서는 정적 컴파일만큼이나 빠르게 실행 하는 것이 가능하다.
브라우저 언어인 javascript 가 JIT를 택한 이유는 다양한 플랫폼에 맞춰 구동 될 수 있도록 컴파일 언어를 바이트 코드 수준으로만 컴파일 하고
플랫폼에 맞춰 바이트 코드를 기계어로 변경해 실행 시키기 위함이다. ## JIT (Just Intime compilation)
JIT 방식은 런타임 때 바이트 코드로 컴파일 된 코드를 실행해나가며
바이트 코드에 해당하는 기계어를 찾아 실행해나가는 방식을 의미 한다.
JIT 방식은 실행에 필요한 부분만 인터프리팅 하고, 인터프리팅 시 사용된 기계어들은 캐싱되기 때문에 실행되었던 동일한 코드에 대해서는 정적 컴파일만큼이나 빠르게 실행 하는 것이 가능하다. [3]
브라우저 언어인 javascript 가 JIT를 택한 이유는 다양한 플랫폼에 맞춰 구동 될 수 있도록 컴파일 언어를 바이트 코드 수준으로만 컴파일 하고
플랫폼에 맞춰 바이트 코드를 기계어로 변경해 실행 시키기 위함이다.
만약 정적 컴파일을 선택했다면 구동되는 플랫폼 별로 코드를 컴파일 한 후 배포해야 했을 것이다.
let a = 1
이라는 코드가 존재 할 때 컴파일부터 런타임까지 해당 변수가 저장되는 순서는 다음과 같다.
컴파일 단계에서 해당 코드를 구문 분석하여 AST
형태로 변환한 후 a
라는 변수를 호출된 컨텍스트에 맞춰 상수로 식별한다.
해당 코드가 호출된 컨텍스트와 정보를 담은 바이트 코드로 컴파일 한다.
런타임 시 바이트 코드를 기계어로 변경해 평가해 나가며 a
라는 변수를 메모리에 저장하며, 아직 값이 할당되지 않은 uninitialized
상태로 저장
이렇게 런타임 전 실행 할 바이트코드를 통해 값을 할당하기 전 선언된 변수들을 메모리에 저장하는 단계를 호이스팅이라고 한다.
var
선언문의 경우엔 평가와 함께 값 할당이 일어난다. 하지만 대부분의 우리는let , const
를 사용하기에 해당 부분에 대한 설명은 하지 않는다.
let a = 1
이라는 바이트 코드 구문을 만나면 1
값을 메모리에 저장 한 후 a
라는 변수가 1
이 저장된 메모리를 참조하도록 연결한다. 오 마이 갓
나는 일반적인 컴파일 방식 처럼 자바스크립트의 실행 파일에서도 각 변수들이 저장 될 메모리 주소가 컴파일 시점에 가상 메모리 값으로 저장 되는 줄 알고 위에서 컴파일에 대해 작성하였는데
쓰면서 알아보니 자바스크립트의 메모리 주소 할당은 런타임 시 일어나는거더라
자바스크립트의 변수 선언은 런타임 시 초기화와 평가 단계를 거치기 때문에 자바스크립트는 undefined
라는 값을 가지고 있다.
만약 let a
코드를 선언하고 a
의 값을 참조하려 하면 a
는 undefined
라는 값을 가지게 되는데
이는 해당 변수가 평가 과정에서 메모리에 적재 되었으나 참조하고 있는 값이 없음을 나타낸다.
즉, undefined
란 변수가 선언 되었으나 값이 아직 할당되지 않은 상태 를 의미 한다.
const
선언문의 경우엔 사용 시 선언문과 함께 초기화 할 값을 함께 사용해줘야 한다.
const
선언문의 경우 값 재할당이 불가하기 때문에 선언문 이후에는 값을 할당해주는 것이 불가능 하기 때문이다.만약 선언문만 사용하게 된다면 syntaxError 가 나타나게 된다.
Uncaught SyntaxError: Missing initializer in const declaration
null
값은 말 그대로 어떤 변수가 가리키는 메모리의 주소(포인터)가 없다는 것을 의미 한다.[4]
예를 들어
let obj = { key: "value" };
다음과 같이 할당 했을 때 obj
변수는 {key : 'value'}
객체가 저장된 메모리 주소를 가리키고 있으나
obj = null; // object is no longer referenced
다음과 같이 null
값을 할당하게 되면 더 이상 가리키고 있는 객체가 없음을 의미 한다.
이를 이후 있을 내용을 위해 다른 단어를 통해 설명하면
더 이상 {key : 'value'}
객체를 참조 하고 있는 포인터가 없어, 참조를 끊는다고 이야기 할 수 있다.
자바스크립트는 동적 메모리 언어로 메모리 할당 및 해제를 개발자가 할 필요 없이
가비지컬렉터가 동적으로 메모리 할당과 해제를 해주는 아주 맘 편한 언어이다.
자바스크립트는 도달 가능성(reachability
) 개념을 사용해 메모리 관리를 시행한다.[5]
도달 가능한이란 어떻게든 접근하거나 사용 할 수 있는 값을 의미 한다.
밑의 예시들은 명백한 이유 없이 할당 이후 메모리에서 해제 되지 않는다.
현재 함수의 지역 변수와 매개변수
중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
전역 변수
루트가 참조하는 값이나 체이닝으로 루트에서 참조할 수 있는 값
도달 할 수 없는 값이 되는 가장 직관적인 예시를 살펴보자
출처 : 가비지 컬렉션
이처럼 전역 컨텍스트에서 {name : ...}
객체를 선언하면 해당 객체가 저장된 메모리의 주소를 user
변수가 가리키기에 해당 메모리에 있는 값은 도달 할 수 있는 값이 된다.
출처 : 가비지 컬렉션
하지만 이 처럼 user
의 포인터를 null
로 처리하게 된다면 더 이상 {name : ...}
객체는 어떤 변수와도 참조 관계가 없기에 도달 할 수 없는 값이 되어버린다.
이렇게 도달 할 수 없는 값이 되어버린 값은 가비지 컬렉터에 의해서 메모리에서 해제 된다.
좀 더 자세한 설명은 위에 표기한 가비지 컬렉션에서 읽어보면 좋을 거 같다.
따로 시간 내서 나도 따로 공부해야지 ..
가비지 컬렉터를 이야기 한 이유는 null
은 포인터의 참조를 해제하는 의미를 가지기 때문에 참조를 즉각적으로 해제하지만
undefined
는 평가 되지 않았음을 의미하는 것이기에 참조 관계를 즉각적으로 해제 할 수 없기 때문이다.
물론 가비지 컬렉터가 시간이 지남에 따라 오랫동안 참조 되지 않은 변수는 메모리에서 할당하기도 하기에 꼭 해제를 위해
null
을 써야 하는 것은 아니다.
1. [컴퓨터 구조] - 레지스터와 메모리 차이
2. JavaScript는 어떻게 컴파일될까?
3. JIT 컴파일 - 위키백과, 우리 모두의 백과사전
4. 널 문자 - 위키백과, 우리 모두의 백과사전
5. 가비지 컬렉션