JavaScript 클로저가 가비지 수집되는 방법


168

다음 Chrome 버그를 기록했습니다 . 이로 인해 코드에서 심각하고 분명하지 않은 메모리 누수가 많이 발생했습니다.

(이 결과 는 GC를 실행하고 가비지 수집되지 않은 모든 것의 힙 스냅 샷을 생성하는 Chrome Dev Tools의 메모리 프로파일 러 를 사용합니다.)

아래 코드에서 someClass인스턴스는 가비지 수집됩니다 (양호).

var someClass = function() {};

function f() {
  var some = new someClass();
  return function() {};
}

window.f_ = f();

그러나이 경우 가비지 수집되지 않습니다 (나쁜).

var someClass = function() {};

function f() {
  var some = new someClass();
  function unreachable() { some; }
  return function() {};
}

window.f_ = f();

그리고 해당 스크린 샷 :

Chromebug의 스크린 샷

클로저 (이 경우 function() {})는 객체가 동일한 컨텍스트에서 다른 클로저에 의해 참조되는 경우 클로저 자체에 도달 할 수 있는지 여부에 관계없이 모든 객체를 "생존"상태로 유지 하는 것으로 보입니다 .

내 질문은 다른 브라우저 (IE 9 이상 및 Firefox)의 가비지 수집에 관한 것입니다. JavaScript 힙 프로파일 러와 같은 웹킷 도구에 익숙하지만 다른 브라우저 도구는 거의 알지 못하므로 테스트 할 수 없습니다.

이 세 가지 경우 중 IE9 +와 Firefox 가비지가 인스턴스를 수집 someClass 합니까?


4
시작하지 않은 경우 Chrome에서 가비지 수집 된 변수 / 객체와 언제 발생하는지 테스트하는 방법은 무엇입니까?
nnnnnn

1
콘솔이 콘솔을 참조하고있을 수도 있습니다. 콘솔을 지우면 GC가 표시됩니까?
david

1
@david 마지막 예제에서 unreachable함수는 절대 실행되지 않으므로 실제로 기록되는 것은 없습니다.
James Montagne

1
우리가 사실에 직면 한 것처럼 보이지만 그 중요성의 버그가 발생했다고 믿기가 어렵습니다. 그러나 코드를 반복해서보고 있으며 다른 합리적인 설명을 찾지 못했습니다. 콘솔에서 코드를 전혀 실행하지 않으려 고했습니다 (일명 브라우저가로드 된 스크립트에서 자연스럽게 코드를 실행하도록하십시오)?
plalx

1
@ some, 나는 그 기사를 전에 읽었습니다. "자바 스크립트 응용 프로그램에서 순환 참조 처리"라는 제목이 있지만 JS / DOM 순환 참조의 문제는 최신 브라우저에는 적용되지 않습니다. 클로저를 언급하지만 모든 예제에서 문제의 변수는 여전히 프로그램에서 사용 가능했습니다.
Paul Draper

답변:


78

내가 알 수있는 한, 이것은 버그가 아니라 예상되는 동작입니다.

Mozilla의 메모리 관리 페이지에서 : "2012 년 현재 모든 최신 브라우저는 마크 앤 스윕 가비지 수집기를 제공합니다." "제한 : 객체는 명시 적으로 도달 할 수 없어야 합니다. "

귀하의 예에서 실패한 부분 some은 여전히 ​​폐쇄에 도달 할 수 있습니다. 나는 그것을 도달 할 수 없도록 만들고 두 가지 방법을 시도했습니다. some=null더 이상 필요하지 않을 때 설정 하거나 설정하면 window.f_ = null;사라집니다.

최신 정보

Windows의 Chrome 30, FF25, Opera 12 및 IE10에서 시도했습니다.

표준은 가비지 컬렉션에 대해 아무 말도 있지만, 어떻게해야하는지의 몇 가지 단서를 제공하지 않습니다.

  • 섹션 13 함수 정의, 4 단계 : "13.2에 지정된 새 Function 객체를 만든 결과를 닫습니다."
  • 섹션 13.2 "범위에 의해 지정된 어휘 환경"(범위 = 폐쇄)
  • 섹션 10.2 어휘 환경 :

"(내부) 어휘 환경의 외부 참조는 내부 어휘 환경을 논리적으로 둘러싼 어휘 환경에 대한 참조입니다.

물론 외부 어휘 환경에는 자체 외부 어휘 환경이있을 수 있습니다. 어휘 환경은 여러 내부 어휘 환경의 외부 환경으로 사용될 수 있습니다. 예를 들어, 함수 선언이 두 중첩 포함 함수 선언 후 중첩 된 각 기능의 사전 환경은 외부 환경 어휘 주변 기능의 현재의 사전 실행 환경으로 할 것이다. "

따라서 함수는 부모의 환경에 액세스 할 수 있습니다.

따라서 some반환 기능을 닫을 때 사용할 수 있어야합니다.

그렇다면 왜 항상 사용할 수 없습니까?

Chrome과 FF는 경우에 따라 변수를 제거하기에 충분히 똑똑한 것으로 보이지만 Opera와 IE 모두 some변수를 클로저에서 사용할 수 있습니다 (NB :이 세트를 중단 점을 return null보고 디버거를 확인하십시오).

some기능에 사용 되는지 여부를 감지하도록 GC를 개선 할 수 는 있지만 복잡합니다.

나쁜 예 :

var someClass = function() {};

function f() {
  var some = new someClass();
  return function(code) {
    console.log(eval(code));
  };
}

window.f_ = f();
window.f_('some');

위의 예에서 GC는 변수의 사용 여부를 알 수있는 방법이 없습니다 (코드 테스트 및 Chrome30, FF25, Opera 12 및 IE10에서 작동).

에 다른 값을 할당하여 객체에 대한 참조가 손상되면 메모리가 해제됩니다 window.f_.

제 생각에는 이것은 버그가 아닙니다.


4
그러나 setTimeout()콜백이 실행 되면 콜백의 해당 기능 범위 setTimeout()가 수행되고 전체 범위가 가비지 수집되어에 대한 참조가 해제됩니다 some. some클로저에서 인스턴스에 도달 할 수있는 코드가 더 이상 없습니다 . 가비지 수집해야합니다. 마지막 예제는 unreachable()호출되지 않았으며 참조가 없기 때문에 더 나쁩니다 . 범위도 GCed 여야합니다. 둘 다 버그처럼 보입니다. JS에서 함수 범위 내에서 "자유로운"것을 요구하는 언어 요구 사항은 없습니다.
jfriend00

1
@some해서는 안됩니다. 함수는 내부적으로 사용하지 않는 변수를 닫아서는 안됩니다.
plalx

2
빈 함수로 액세스 할 수는 있지만 실제 참조가 없으므로 명확해야합니다. 가비지 콜렉션은 실제 참조를 추적합니다. 그것은 참조 될 수있는 모든 것, 실제로 참조 된 것만을 붙잡고 있지는 않습니다. 마지막 f()이 호출 되면 some더 이상 실제 참조가 없습니다 . 도달 할 수 없으며 GCed 여야합니다.
jfriend00

1
@ jfriend00 (표준) [ ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf] 에서 아무것도 찾을 수 없습니다. 내부적으로 사용하는 변수에 대해서만 사용할 수 있어야합니다. 섹션 13, 생산 단계 4 : 13.2 , 10.2 "에 지정된대로 새 Function 객체를 생성 한 결과를 클로저로 설정하십시오. "외부 환경 참조는 Lexical Environment 값의 논리적 중첩을 모델링하는 데 사용됩니다. ) Lexical Environment는 내부 Lexical 환경을 논리적으로 둘러싼 Lexical Environment에 대한 참조입니다. "

2
글쎄, eval정말 특별한 경우입니다. 예를 들어 eval별명을 지정할 수 없습니다 ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ) var eval2 = eval. eval를 사용하는 경우 (다른 이름으로 호출 할 수 없으므로 쉽게 수행 할 수 있음) 범위 내에서 모든 것을 사용할 수 있다고 가정해야합니다.
Paul Draper

49

IE9 +와 Firefox에서 이것을 테스트했습니다.

function f() {
  var some = [];
  while(some.length < 1e6) {
    some.push(some.length);
  }
  function g() { some; } //removing this fixes a massive memory leak
  return function() {};   //or removing this
}

var a = [];
var interval = setInterval(function() {
  var len = a.push(f());
  if(len >= 500) {
    clearInterval(interval);
  }
}, 10);

여기에 라이브 사이트가 있습니다 .

나는 function() {}최소한의 메모리를 사용하여 500의 배열로 마무리하고 싶었습니다 .

불행히도, 그렇지 않았습니다. 각 빈 함수는 백만 개의 숫자로 구성된 (영원히 도달 할 수 없지만 GC가 아닌) 배열을 유지합니다.

Chrome은 결국 중단되고 죽고 Firefox는 거의 4GB의 RAM을 사용한 후 모든 것을 끝내고 IE는 "메모리 부족"을 표시 할 때까지 점진적으로 느려집니다.

주석 처리 된 행 중 하나를 제거하면 모든 것이 수정됩니다.

이 세 가지 브라우저 (Chrome, Firefox 및 IE)는 모두 폐쇄가 아니라 컨텍스트별로 환경 레코드를 유지하는 것으로 보입니다. Boris는이 결정의 원인이 성능이라는 가설을 세웠으며, 위 실험에 비추어 얼마나 성능이 좋은지 잘 모르겠습니다.

대신 클로저 참조가 필요한 경우 some(여기서 사용하지 않았지만 상상해보십시오)

function g() { some; }

나는 사용한다

var g = (function(some) { return function() { some; }; )(some);

클로저를 다른 함수와 다른 컨텍스트로 이동하여 메모리 문제를 해결합니다.

이것은 내 인생을 훨씬 더 지루하게 만들 것입니다.

추신 : 호기심으로, Java에서 이것을 시도했습니다 (함수 내부의 클래스를 정의하는 기능 사용). GC는 원래 Javascript를 원했던대로 작동합니다.


외부 함수에 대해 닫는 괄호가 누락되었다고 생각합니다. var g = (function (some) {return function () {some;};}) (some);
HCJ

15

휴리스틱은 다양하지만, 이런 종류의 물건을 구현하는 일반적인 방법은 각 통화에 대한 환경 레코드를 생성하는 것입니다 f()귀하의 경우, 단지의 주민들 저장 f실제로 (에 의해 이상 닫혀 일부 해당 환경 레코드에 폐쇄). 그런 다음 호출에서 생성 된 폐쇄 f는 환경 레코드 를 유지합니다. 나는 이것이 Firefox가 최소한 클로저를 구현하는 방법이라고 생각합니다.

이는 닫힌 변수에 빠르게 액세스 할 수 있고 구현이 간편하다는 이점이 있습니다. 일부 변수에 대한 단기 폐쇄 폐쇄로 인해 장기 폐쇄에 의해 생존이 유지되는 관측 된 효과의 단점이 있습니다.

실제로 닫은 내용에 따라 다른 클로저에 대해 여러 환경 레코드를 만들려고 시도 할 수 있지만 매우 복잡해져 자체 성능 및 메모리 문제를 일으킬 수 있습니다.


통찰력에 감사드립니다. 이것이 Chrome이 클로저를 구현하는 방법이기도하다는 결론을 내 렸습니다. 저는 항상 폐쇄 방식이 각 환경에서 필요한 환경 만 유지하는 방식으로 구현되었다고 생각했지만, 그렇지 않습니다. 여러 환경 레코드를 만드는 것이 실제로 그렇게 복잡한 지 궁금합니다. 클로저의 참조를 집계하는 것이 아니라 각 클로저가 유일한 클로저 인 것처럼 행동하십시오. 공유 환경 레코드를 갖는 결과가 더 나빠 보이지만 성능 고려 사항이 여기에서 추론이라고 생각했습니다.
Paul Draper

후자의 경우 어떤 경우에는 작성해야하는 환경 레코드 수가 폭발적으로 증가합니다. 가능하면 여러 기능을 통해 공유하려고 노력하지 않는 한 그렇게하려면 복잡한 기계가 필요합니다. 가능하지만 성능 상충 관계가 현재 접근 방식을 선호한다고 들었습니다.
보리스 즈 바르 스키

레코드 수는 생성 된 클로저 수와 같습니다. 나는 설명 할 수 O(n^2)또는 O(2^n)비례 증가를 폭발 같은 아니지만.
Paul Draper

글쎄, O (N)은 O (1)에 비해 폭발적인 현상입니다. 특히 각 사람이 상당한 양의 메모리를 차지할 수있을 때 ... 다시 말하지만, 나는 이것에 대해 전문가가 아닙니다. irc.mozilla.org에서 #jsapi 채널을 요청하면 트레이드 오프가 무엇인지에 대해 더 잘 설명 할 수있을 것입니다.
보리스 즈 바르 스키

1
@Esailija 불행히도 실제로는 일반적입니다. 임의의 짧은 수명의 콜백이 사용하는 긴 임시 함수 (일반적으로 큰 유형의 배열)와 긴 수명의 클로저가 필요합니다. 이 웹 응용 프로그램을 작성하는 사람 ...을 위해 최근에 여러 번 올입니다
보리스 Zbarsky

0
  1. 함수 호출 사이의 상태 유지 add () 함수가 있고 여러 호출에서 전달 된 모든 값을 추가하고 합계를 반환한다고 가정합니다.

add (5)처럼; // 5를 반환

추가 (20); // 25를 반환합니다 (5 + 20)

추가 (3); // 28을 반환합니다 (25 + 3)

전역 변수를 정의하는 것이 일반적입니다. 물론 총계를 유지하기 위해 전역 변수를 사용할 수 있습니다. 그러나 만약 당신이 (ab) 글로벌을 사용한다면이 친구는 당신을 살아있게 먹을 것입니다.

전역 변수를 정의하지 않고 클로저사용하는 최신 방법

(function(){

  var addFn = function addFn(){

    var total = 0;
    return function(val){
      total += val;
      return total;
    }

  };

  var add = addFn();

  console.log(add(5));
  console.log(add(20));
  console.log(add(3));
  
}());


0

function Country(){
    console.log("makesure country call");	
   return function State(){
   
    var totalstate = 0;	
	
	if(totalstate==0){	
	
	console.log("makesure statecall");	
	return function(val){
      totalstate += val;	 
      console.log("hello:"+totalstate);
	   return totalstate;
    }	
	}else{
	 console.log("hey:"+totalstate);
	}
	 
  };  
};

var CA=Country();
 
 var ST=CA();
 ST(5); //we have add 5 state
 ST(6); //after few year we requare  have add new 6 state so total now 11
 ST(4);  // 15
 
 var CB=Country();
 var STB=CB();
 STB(5); //5
 STB(8); //13
 STB(3);  //16

 var CX=Country;
 var d=Country();
 console.log(CX);  //store as copy of country in CA
 console.log(d);  //store as return in country function in d


대답을 설명하십시오
janith1024

0

(function(){

   function addFn(){

    var total = 0;
	
	if(total==0){	
	return function(val){
      total += val;	 
      console.log("hello:"+total);
	   return total+9;
    }	
	}else{
	 console.log("hey:"+total);
	}
	 
  };

   var add = addFn();
   console.log(add);  
   

    var r= add(5);  //5
	console.log("r:"+r); //14 
	var r= add(20);  //25
	console.log("r:"+r); //34
	var r= add(10);  //35
	console.log("r:"+r);  //44
	
	
var addB = addFn();
	 var r= addB(6);  //6
	 var r= addB(4);  //10
	  var r= addB(19);  //29
    
  
}());

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.