함수 호출 방식

Call by value & Call by reference

Call by value (값에 의한 호출)

  • 함수가 호출될 때, 메모리 공간 안에서는 함수를 위한 별도의 임시 공간이 생성된다. 함수가 종료되면 해당 공간은 사라진다
  • call-by-value 값에 의한 호출방식은 함수 호출시 전달되는 변수의 값을 복사하여 함수의 인자로 전달한다.
  • 함수 안에서 인자의 값이 변경되어도, 외부의 변수의 값은 변경되지 않는다.
  • 원시 자료형 (primitive type) : call-by-value 로 동작 (int, short, long, float, double, char, boolean )
  • 참조 자료형 (reference type): call-by-reference 로 동작 (Array, Class Instance)

Call by reference (참조에 의한 호출)

  • 함수가 호출될 때, 메모리 공간 안에서는 함수를 위한 별도의 임시 공간이 생성된다. 함수가 종료되면 해당 공간은 사라진다.
  • call-by-reference 참조에 의한 호출방식은 함수 호출시 인자로 전달되는 변수의 레퍼런스를 전달한다.
  • 따라서 함수 안에서 인자의 값이 변경되면, 아규먼트로 전달된 객체의 값도 함께 변경된다.

재할당 (Reassigning of Reference Type)

참조값 비교

let obj = { p1: 10 }
let objCopy = obj
console.log(obj === objCopy) // true

objCopy = { p2: 100 }
console.log(obj === objCopy) // false

인자를 함수 내에서 속성만 변경한 경우

function change(obj) {
  obj.p1 = 100
}
const o = { p1: 1 }
change(o)
console.log(o)  //{ p1: 100 };

함수 내에서 인자를 재할당 한 경우

function change(obj) {
  obj = { p1: 100 } //재할당 됨 -> 주소값 변경
}
const o = { p1: 1 }
change(o)
console.log(o)  //{ p1: 1 };

함수 내에서

  • 참조 자료형의 속성만 변경한 경우: 함수 바깥에서도 적용
  • 참조 자료형을 재할당 한 경우: 함수 바깥의 변수와 함수 내 매개변수가 서로 다른 객체를 바라보게 됨. 따라서 바깥에 적용 안됨

호출 스택 (Call Stack)

자바스크립트는 기본적으로 싱글 쓰레드 기반 언어입니다. 호출 스택이 하나라는 소리죠. 따라서 한 번에 한 작업만 처리할 수 있습니다.

만약 함수를 실행하면(실행 커서가 함수 안에 있으면), 해당 함수는 호출 스택의 가장 상단에 위치하는 거죠. 함수의 실행이 끝날 때(리턴 값을 돌려줄 때), 해당 함수를 호출 스택에서 제거합니다.

그러다가 특정 시점에 함수 호출 횟수가 호출 스택(Call Stack)의 최대 허용치를 넘게 되면 브라우저가 아래와 같은 에러를 발생시킵니다.

가비지 컬렉터(garbage collector)

고수준의 언어에는 가비지 컬렉터(garbage collector)라는 소프트웨어가 내장되어 있는데 이것의 역할은 메모리 할당을 추적하고 언제 할당된 메모리가 더 이상 사용되지 않는지 파악해서 자동으로 반환하는 것입니다

가비지 컬렉터 역할

  1. 메모리 할당
  2. 사용 중인 메모리 인식
  3. 사용하지 않는 메모리인식

참조횟수계산 가비지컬렉션

이것은 가장 단순한 형태의 가비지컬렉션 알고리즘입니다. 객체는 만약 그것을 가리키는 참조가 하나도 없는 경우 가비지컬렉션 대상(garbage collectible)으로 간주됩니다.

순환 참조 때문에 생기는 문제

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1은 o2를 참조함
  o2.p = o1; // o2는 o1을 참조함. 이를 통해 순환 참조가 만들어짐.
}
f();

이 객체들은 함수 호출 뒤에 스코프를 벗어나게 되므로 실질적으로는 쓸모가 없게 되고 이들이 차지하던 메모리는 반환될 수 있습니다. 하지만 참조횟수계산 알고리즘에서는 두 객체가 적어도 한 번은 참조한 것으로 간주하므로 둘 다 가비지컬렉션 될 수 없습니다.

마크스위프 알고리즘

  1. 루트(Roots): 일반적으로 루트는 코드에서 참조되는 전역 변수입니다.
  2. 그런 다음 모든 루트와 그 자식들을 검사해서 활성화 여부를 표시합니다(활성상태이면 가비지가 아닙니다). 루트가 닿을 수 없는 것들은 가비지로 표시됩니다.
  3. 마지막으로 가비지컬렉터는 활성으로 표시되지 않은 모든 메모리를 OS에 반환합니다.

Mark and sweep

네 가지 흔한 자바스크립트 메모리누수

  1. 전역 변수
    자바스크립트는 흥미로운 방식으로 선언되지 않은 변수를 처리합니다. 선언되지 않은 변수가 참조되면 전역 객체에 새로운 변수를 생성하는 것입니다. 브라우저상이라면 전역 객체는 window가 됩니다. 따라서,
  function foo(arg) {
      bar = "some text";
  }

는 다음과 동일합니다.

  function foo(arg) {
      window.bar = "some text";
  }

bar의 목적이 foo 함수 내의 어떤 변수를 가리키는 것이었다고 해봅시다. 하지만 var를 사용하여 변수를 선언하지 않으므로써 필요 없는 전역 변수가 생성될 것입니다.

또한 this를 이용해서도 뜻하지 않은 전역 변수를 생성할 수 있습니다.

  function foo() {
      this.var1 = "potential accidental global";
  }
  // 다른 함수 내에 있지 않은 foo를 호출하면 this는 글로벌 객체(window)를 가리킴
  foo();

자바스크립트 파일의 상단에 use strict를 사용하면 위와 같은 모든 것들을 회피할 수 있습니다. 이 모드에서 자바스크립트는 예상치 못한 전역 변수 생성을 방지할 수 있는 훨씬 엄격한 파싱을 시도합니다.

  1. 잊혀진 타이머 혹은 콜백함수
    setInterval과 같이 옵저버를 사용할 때는 그 사용이 종료되었을 때 꼭 명시적으로 그것을 제거해야 합니다(해당 옵저버가 더 이상 필요하지 않거나 닿을 수 없는 상태일 때).

운이 좋게도 대부분의 현대적인 브라우저에서는 이러한 일을 대신 해줍니다. 이들은 개발자가 리스너를 제거하는 것을 잊었다고 하더라도 객체가 닿을 수 없는 상태가 되면 자동으로 옵저버 핸들러를 가져갑니다. 예전에는 이러한 경우에 대처를 하지 못했습니다(IE6 같은 경우).

  var element = document.getElementById('launch-button');
  var counter = 0;
  function onClick(event) {
    counter++;
    element.innerHtml = 'text ' + counter;
  }
  element.addEventListener('click', onClick);
  // 필요한 작업 수행
  element.removeEventListener('click', onClick);
  element.parentNode.removeChild(element);
  // 이제 요소들이 스코프를 벗어나게 되면
  // 순환참조를 잘 처리하지 못 하는 구형 웹브라우저에서도
  // 해당 요소와 onClick 콜백을 가비지컬렉터가 가져감

더 이상 노드를 닿을 수 없는 상태로 만들기 전에 removeEventListener를 호출할 필요가 없습니다. 왜냐하면 현대적 브라우저들은 이러한 순환참조를 탐지하고 적절히 처리하는 가비지컬렉터를 지원하기 때문입니다.

  1. 클로져
    클로져는 자신을 감싸고 있는 바깥 함수의 변수에 접근할 수 있는 내부의 함수를 말합니다.
    기억할 점은 한 번 동일한 부모 스코프에 있는 클로져들에 대한 스코프가 생성되고 나면 이것은 공유된다는 점입니다.
  var theThing = null;
  var replaceThing = function () {
    var originalThing = theThing;
    var unused = function () {
      if (originalThing) // 'originalThing'에 대한 참조
        console.log("hi");
    };
    theThing = {
      longStr: new Array(1000000).join('*'),
      someMethod: function () {
        console.log("message");
      }
    };
  };
  setInterval(replaceThing, 1000);

위의 경우 someMethod 클로져를 위해 생성된 스코프는 unused와 공유되었습니다. unused는 originalThing에 대한 참조를 갖고 있습니다. unused가 다시 사용되지 않는다 해도 someMethod는 theThing을 통해 replaceThing의 스코프 바깥에서 사용될 수 있습니다(글로벌하게). 그리고 someMethod는 unused와 클로져 스코프를 공유하기 때문에 unused가 originalThing에 대해 갖고 있는 참조 때문에 강제로 활성 상태가 유지됩니다(두 클로져 사이에 공유된 전체 스코프). 이 때문에 가비지컬렉션이 작동하지 않습니다.


  1. DOM에서 벗어난 요소 참조
    DOM 노드를 데이터 구조 속에 저장하는 경우가 있습니다. 테이블 내 몇 열의 내용을 빠르게 업데이트하고 싶은 상황이라고 가정해 봅시다. 각 열에 대한 참조를 딕셔너리나 배열에 저장하면 동일한 DOM 요소에 대해 두 개의 참조가 존재하는 셈입니다. 하나는 DOM 트리에, 하나는 딕셔너리에. 이 열들을 제거하고자 결정한다면 이 두개의 참조 모두가 닿을 수 없도록 해야하는 것을 잊지 말아야 합니다.
  var elements = {
      button: document.getElementById('button'),
      image: document.getElementById('image')
  };
  function doStuff() {
      elements.image.src = 'http://example.com/image_name.png';
  }
  function removeImage() {
      // image는 body 요소의 바로 아래 자식임
      document.body.removeChild(document.getElementById('image'));
      // 이 순간까지 #button 전역 요소 객체에 대한 참조가 아직 존재함
      // 즉, button 요소는 아직도 메모리 상에 있고 가비지컬렉터가 가져갈 수 없음
  }

참고 링크

[javascript] call by value
자바스크립트의 동작원리: 엔진, 런타임, 호출 스택
강의노트 12. 함수 호출방식(call-by-value, call-by-reference, call-by-assignment)
[성능튜닝] 가비지 컬렉터(GC) 이해하기
Java Garbage Collection
자바스크립트는 어떻게 작동하는가: 메모리 관리 + 4가지 흔한 메모리 누수 대처법