고수준 언어와 저수준 언어
- C언어가 저수준 언어라고 해서 고수준 언어인 JavaScript에 비해 뒤떨어지는 건 아니다. 프로그래밍 언어가 인간에게 친화적인지, 기계에게 친화적인지에 따라 고수준 언어와 저수준 언어로 갈리는 것이다.
- 저수준 언어는 보다 기계 친화적인 언어로 레지스터 및 메모리와 직접 상호 작용을 할 수 있기 때문에 전반적으로 빠르게 실행되는 응용 프로그램을 빌드하는 데에 사용된다. 또한 저수준 언어는 컴파일러나 인터프리터가 필요하지 않으므로 저수준 언어는 고수준 언어보다 빠른 편이다.
- 반대로 고수준 언어는 인간 친화적인 언어로, 인간이 이해하기 쉽고 다양한 작업을 수행하는 프로그램을 개발할 수 있다. 영어와 유사한 구문이 있기 때문에 컴파일러 또는 인터프리터를 사용하여 컴퓨터가 읽을 수 있는 기계어 코드로 변환해야 하며, 하드웨어와 직접 상호 작용하지는 않는다.
메모리 생존 주기는 어떤 프로그래밍 언어에 관계 없이 비슷하다.
2번의 할당된 메모리를 사용하는 것은 모든 언어에서 명시적으로 사용되는 부분이다. JavaScript를 예시로 들면 개발자가 변수를 선언해 값을 할당하여 사용하는 부분이다. 그러나 1번과 3번은 C언어와 같은 기계친화적인 저수준 언어에서는 명시적이고, JavaScript와 같은 고수준 언어에서는 암묵적으로 작동한다.
let arr = [100, 200, 300, 400]
할당된 메모리가 더이상 필요 없다면 해제를 해야 앱의 성능을 저하시키지 않는다.
이 부분에서, 저수준 언어는 개발자가 직접 결정하고 해제하는 방식을 사용합니다. 개발자가 직접 관여하기 때문에 개발자의 제어 정도가 굉장히 높은 편이다.
그러나 고수준 언어는 앞서 이야기 했듯 가비지 컬렉션이라는 자동 메모리 관리 방법을 내장한 상태이기 때문에 가비지 컬렉션의 목적은 메모리 할당을 추적하고, 할당된 메모리 블록이 더이상 필요하지 않게 되었는지를 “스스로” 판단하여 필요하지 않다고 판단이 된다면 해당 메모리를 해제한다. 하지만 언어 스스로 메모리가 여전히 필요한지 필요하지 않은지 판단하는 것은 비결정적인 영역이므로 고수준 언어에 내장된 가비지 컬렉터들은 제한적인 해결책을 구현한다.
참조(reference)
명시적이든, 암묵적이든 관계없이 메모리 관리 관점에서 어떤 객체가 다른 객체에 접근할 수 있다면 다른 객체를 참조한다고 말한다. 예를 들어서, JavaScript 객체는 자신의 프로토타입(prototype)에 대해 암묵적인 참조를 갖고 있고, 자신의 속성(property) 값에 대한 명시적 참조도 가지고 있다.
렉시컬 스코핑(lexical scoping)
변수 이름이 중첩된 함수에서 해석되는 방식을 정의하는 것으로, 중첩되어 있는 더 안쪽의 함수는 부모 함수가 값을 반환한 다음에도 부모 함수의 스코프를 포함하고 있다.
한 객체를 참조하는 변수의 수를 추적하는 방법으로 가장 단순한 형태의 가비지 컬렉션 알고리즘이다.
객체를 참조하는 변수는 처음에는 특정 메모리에 대해 레퍼런스가 하나뿐이지만, 변수의 레퍼런스가 복사될 때마다 레퍼런스 카운트가 늘어난다. 객체를 참조하고 있던 변수의 값이 바뀌거나, 변수 스코프를 벗어나면 레퍼런스 카운트는 줄어든다. 레퍼런스 카운트가 0이 되면, 그 객체와 관련한 메모리는 비울 수 있다. 레퍼런스 카운트가 0이 된다는 말은 아무도 그 객체에 대한 레퍼런스를 가지고 있지 않다는 말과 같다.
이 방식은 순환 참조로 인한 문제가 생길 가능성이 높다.
function reference() {
var obj1 = {};
var obj2 = {};
obj1.p = obj2;
obj2.p = obj1;
}
reference();
한 객체에 flag를 두고, 가비지 컬렉션 사이클마다 flag에 표시 후 삭제하는 mark and sweep 방법이다.
객체에 in-use flag를 두고, 사이클마다 메모리 관리자가 모든 객체를 추적해서 사용 중인지 아닌지를 표시(mark)한다. 그 후 표시되지 않은 객체를 삭제(sweep)하는 단계를 통해 메모리를 해제한다. 현재 대부분의 가비지 컬렉션이 mark and sweep 알고리즘을 이용한 가비지 컬렉터를 장착하고 있다.
mark and sweep 알고리즘은 객체가 필요한지 결정하기 위해 해당 객체에 닿을 수 있는지 (reachable)을 판단하며 3단계를 거친다.
루트(Roots): 일반적으로 루트는 코드에서 참조되는 전역 변수다. 예를 들어 자바스크립트에서 루트로 동작할 수 있는 전역 변수는 window 객체다. Node.js에서 이와 동일한 객체는 global이다. 가비지컬렉터는 모든 루트의 완전한 목록을 만들어낸다.
그런 다음 모든 루트와 그 자식들을 검사해서 활성화 여부를 표시한다.(활성상태이면 가비지가 아닙니다). 루트가 닿을 수 없는 것들은 가비지로 표시된다.
마지막으로 가비지컬렉터는 활성으로 표시되지 않은 모든 메모리를 OS에 반환한다.
이 방법은 앞선 레퍼런스 카운팅 방법보다는 나은데, ‘참조받지 않는 객체’는 ‘닿을 수 없는 객체’기 때문에 가비지 컬렉션을 통해 메모리를 해제할 수 있기 때문이다.
이 방법은 앞선 레퍼런스 카운팅 방법보다는 나은데, ‘참조받지 않는 객체’는 ‘닿을 수 없는 객체’기 때문에 가비지 컬렉션을 통해 메모리를 해제할 수 있기 때문이다.
Garbage collected 언어에서 메모리 누수의 주요 원인은 예상치 못한 참조이다.
개발자는 더 이상 사용되지 않을 것이라 생각했던 예상치 못한 참조는 어떠한 이유로 활성화 상태인 루트 트리 안에 존재하는 메모리 조각들이다. 자바스크립트에서 예상치 못한 참조는 더이상 사용되지 않지만 코드 상 어딘가에 유지되어 해제되지 못한 변수들이다.
이런 메모리 누수는 일반적으로 3가지의 형태가 있다.