วิธีการปิด JavaScript ถูกเก็บขยะ


168

ฉันได้บันทึกข้อผิดพลาดของ Chromeต่อไปนี้ซึ่งนำไปสู่การรั่วไหลของหน่วยความจำที่ร้ายแรงและไม่ชัดเจนในรหัสของฉัน:

(ผลลัพธ์เหล่านี้ใช้เครื่องมือสร้างโปรไฟล์หน่วยความจำของ Chrome Dev Tools ซึ่งรัน GC และจากนั้นใช้ภาพรวมกองของทุกสิ่งที่ไม่ได้เก็บรวบรวมไว้)

ในรหัสด้านล่างนี้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) ฉันค่อนข้างคุ้นเคยกับเครื่องมือของ webkit เช่น JavaScript กองโปรไฟล์ แต่ฉันรู้เครื่องมือเบราว์เซอร์อื่น ๆ น้อยดังนั้นฉันจึงไม่สามารถทดสอบได้

กรณีใดบ้างในสามกรณีนี้ IE9 + และ Firefox จะรวบรวมขยะsomeClass ใช่หรือไม่


4
สำหรับผู้ที่ไม่ได้ฝึกหัด Chrome จะให้คุณทดสอบว่าตัวแปร / วัตถุใดบ้างที่ถูกเก็บขยะและเมื่อใดที่เกิดขึ้น
nnnnnn

1
บางทีคอนโซลก็กำลังอ้างอิงอยู่ มันจะได้รับ GCed เมื่อคุณล้างคอนโซล?
david

1
@david ในตัวอย่างที่ผ่านมาunreachableฟังก์ชั่นที่ไม่เคยดำเนินการเพื่อให้ไม่มีอะไรถูกบันทึกจริง
James Montagne

1
ฉันมีปัญหาที่จะเชื่อว่าข้อผิดพลาดที่มีความสำคัญนั้นผ่านแม้ว่าเราจะต้องเผชิญกับข้อเท็จจริง อย่างไรก็ตามฉันกำลังดูรหัสอีกครั้งและอีกครั้งและฉันไม่พบคำอธิบายปันส่วนอื่น ๆ คุณพยายามที่จะไม่เรียกใช้รหัสในคอนโซลเลย (หรือให้เบราว์เซอร์เรียกใช้จากสคริปต์ที่โหลดมาตามธรรมชาติ) ใช่ไหม
plalx

1
@ บางฉันได้อ่านบทความนั้นมาก่อน มันมีชื่อว่า "การจัดการการอ้างอิงแบบวงกลมในแอปพลิเคชัน JavaScript" แต่ข้อกังวลของการอ้างอิงแบบวงกลมของ JS / DOM นั้นไม่มีผลกับเบราว์เซอร์สมัยใหม่ มันกล่าวถึงการปิด แต่ในตัวอย่างทั้งหมดตัวแปรในคำถามยังคงเป็นไปได้ในการใช้งานโดยโปรแกรม
พอลเดรเปอร์

คำตอบ:


78

เท่าที่ฉันสามารถบอกได้นี่ไม่ใช่ข้อผิดพลาด แต่เป็นพฤติกรรมที่คาดหวัง

จากหน้าการจัดการหน่วยความจำของ Mozilla : "ตั้งแต่ปี 2012 เบราว์เซอร์ที่ทันสมัยทั้งหมดจัดส่งตัวเก็บขยะที่มีเครื่องหมายและกวาด" "ข้อ จำกัด : วัตถุต้องมีการทำไม่สามารถเข้าถึงได้อย่างชัดเจน "

ในตัวอย่างของคุณที่มันล้มเหลวsomeยังคงสามารถเข้าถึงได้ในการปิด ฉันลองสองวิธีเพื่อให้ไม่สามารถเข้าถึงและทำงานได้ทั้งคู่ ไม่ว่าคุณจะตั้งค่าsome=nullเมื่อคุณไม่ต้องการใช้อีกต่อไปหรือคุณตั้งค่าwindow.f_ = null;และมันจะหายไป

ปรับปรุง

ฉันได้ลองใน Chrome 30, FF25, Opera 12 และ IE10 บน Windows แล้ว

มาตรฐานไม่ได้พูดอะไรเกี่ยวกับการเก็บขยะ แต่ให้เบาะแสของสิ่งที่ควรจะเกิดขึ้นบางส่วน

  • ส่วนที่ 13 นิยามฟังก์ชั่นขั้นตอนที่ 4: "ให้การปิดเป็นผลมาจากการสร้างวัตถุฟังก์ชั่นใหม่ตามที่ระบุใน 13.2"
  • ส่วนที่ 13.2 "สภาพแวดล้อมคำศัพท์ที่ระบุโดยขอบเขต" (scope = closure)
  • ส่วนที่ 10.2 สภาพแวดล้อมของคำศัพท์:

"การอ้างอิงภายนอกของ Lexical Environment (ภายใน) คือการอ้างอิงไปยัง Lexical Environment ที่ล้อมรอบด้วยเหตุผลด้านสิ่งแวดล้อม Lexical

แน่นอนศัพท์สภาพแวดล้อมภายนอกอาจมีสภาพแวดล้อมคำศัพท์ภายนอกของตัวเอง สภาพแวดล้อมคำศัพท์อาจทำหน้าที่เป็นสภาพแวดล้อมภายนอกสำหรับสภาพแวดล้อมคำศัพท์ภายในหลาย ตัวอย่างเช่นถ้ามีการประกาศฟังก์ชั่นที่มีสองซ้อนกันประกาศฟังก์ชันแล้วคำศัพท์สภาพแวดล้อมของแต่ละฟังก์ชั่นที่ซ้อนกันจะต้องเป็นคำศัพท์สิ่งแวดล้อมด้านนอกของพวกเขาคำศัพท์สิ่งแวดล้อมของการดำเนินการปัจจุบันของฟังก์ชั่นโดยรอบ."

ดังนั้นฟังก์ชั่นจะสามารถเข้าถึงสภาพแวดล้อมของผู้ปกครอง

ดังนั้นsomeควรมีให้ในการปิดฟังก์ชั่นการส่งคืน

ถ้าเช่นนั้นทำไมมันไม่สามารถใช้งานได้ตลอดเวลา?

ดูเหมือนว่า Chrome และ FF นั้นฉลาดพอที่จะกำจัดตัวแปรในบางกรณี แต่ใน Opera และ IE someตัวแปรนั้นมีให้ในการปิด (NB: เพื่อดูชุดเบรกพอยต์นี้return nullและตรวจสอบดีบักเกอร์)

GC อาจได้รับการปรับปรุงเพื่อตรวจสอบว่าsomeมีการใช้งานหรือไม่อยู่ในฟังก์ชั่น แต่จะมีความซับซ้อน

ตัวอย่างที่ไม่ดี:

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
@ บางอย่างมันไม่ควร ฟังก์ชันไม่ควรปิดตัวแปรที่ไม่ได้ใช้เป็นการภายใน
plalx

2
มันสามารถเข้าถึงได้โดยฟังก์ชั่นที่ว่างเปล่า แต่มันไม่ได้ดังนั้นจึงไม่มีการอ้างอิงที่แท้จริงกับมันดังนั้นจึงควรมีความชัดเจน การรวบรวมขยะติดตามการอ้างอิงจริง ไม่ควรยึดติดกับทุกสิ่งที่สามารถอ้างอิงได้ แต่สิ่งที่อ้างอิงจริงเท่านั้น เมื่อมีการf()เรียกใช้ครั้งสุดท้ายจะไม่มีการอ้างอิงจริงsomeอีกต่อไป มันไม่สามารถเข้าถึงได้และควรเป็น GCed
jfriend00

1
@ jfriend00 ฉันไม่พบสิ่งใดใน (มาตรฐาน) [ ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf]พูดอะไรเกี่ยวกับเฉพาะตัวแปรที่ใช้ภายในเท่านั้น ในส่วนที่ 13 ขั้นตอนการผลิตที่ 4: ให้การปิดเป็นผลมาจากการสร้างฟังก์ชั่นวัตถุใหม่ตามที่ระบุใน 13.2 , 10.2 "การอ้างอิงสภาพแวดล้อมด้านนอกจะใช้ในการสร้างแบบจำลองการทำรังตรรกะของค่า Lexical Environment การอ้างอิงภายนอกของ ) Lexical Environment คือการอ้างอิงถึง Lexical Environment ที่ล้อมรอบด้วยเหตุผลศัพท์แวดล้อมภายใน "
ประมาณ

2
ดีevalเป็นกรณีพิเศษจริงๆ ยกตัวอย่างเช่นevalไม่สามารถ aliased ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/... ) var eval2 = evalเช่น หากevalมีการใช้ (และเนื่องจากไม่สามารถเรียกโดยใช้ชื่ออื่นนั่นเป็นเรื่องง่ายที่จะทำ) ดังนั้นเราต้องสมมติว่ามันสามารถใช้อะไรก็ได้ในขอบเขต
พอลเดรเปอร์

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'ed) ของตัวเลขหนึ่งล้าน

ในที่สุด Chrome ก็หยุดทำงาน Firefox ทำสิ่งต่าง ๆ ให้เสร็จหลังจากใช้ RAM ขนาด 4GB และ IE ก็จะช้าลงเรื่อย ๆ จนกระทั่งมันแสดงว่า "Out of memory"

การลบหนึ่งในบรรทัดที่คอมเม้นต์จะแก้ไขทุกอย่าง

ดูเหมือนว่าเบราว์เซอร์ทั้งสามนี้ (Chrome, Firefox และ IE) จะเก็บบันทึกสภาพแวดล้อมต่อบริบทไม่ใช่ต่อการปิด Boris ตั้งสมมติฐานว่าเหตุผลของการตัดสินใจครั้งนี้คือประสิทธิภาพการทำงานและดูเหมือนว่าจะเป็นไปได้ แต่ฉันไม่แน่ใจว่าจะสามารถเรียกนักแสดงได้อย่างไรเนื่องจากการทดลองด้านบน

หากจำเป็นต้องมีการอ้างอิงปิดsome(อนุญาตฉันไม่ได้ใช้ที่นี่ แต่ลองนึกภาพฉัน) ถ้าแทน

function g() { some; }

ฉันใช้

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

มันจะแก้ไขปัญหาหน่วยความจำโดยการย้ายการปิดไปยังบริบทที่แตกต่างจากฟังก์ชั่นอื่น ๆ ของฉัน

นี่จะทำให้ชีวิตของฉันน่าเบื่อมากขึ้น

PS ด้วยความอยากรู้อยากเห็นฉันลองใน Java (ใช้ความสามารถในการกำหนดชั้นเรียนภายในฟังก์ชั่น) GC ทำงานอย่างที่ฉันเคยหวังไว้สำหรับ Javascript


ฉันคิดว่าวงเล็บปิดไม่ได้รับฟังก์ชั่นด้านนอก var g = (ฟังก์ชั่น (บางส่วน) {ฟังก์ชั่นคืน () {บางคน;};}) (บางคน);
HCJ

15

การวิเคราะห์พฤติกรรมแตกต่างกัน แต่เป็นวิธีการทั่วไปในการดำเนินการจัดเรียงของสิ่งนี้คือการสร้างการบันทึกสภาพแวดล้อมสำหรับการโทรแต่ละครั้งจะf()ในกรณีของคุณและมีเพียงชาวบ้านเก็บของfที่มีการปิดจริงมากกว่า (โดยบางปิด) ในการบันทึกสภาพแวดล้อมที่ จากนั้นการปิดใด ๆ ที่สร้างขึ้นในการโทรเพื่อfรักษาชีวิตของบันทึกสภาพแวดล้อม ฉันเชื่อว่านี่เป็นวิธีที่ Firefox จะปิดตัวลงอย่างน้อย

สิ่งนี้มีประโยชน์ในการเข้าถึงตัวแปรปิดและความง่ายในการใช้งาน มันมีข้อเสียเปรียบของเอฟเฟกต์ที่สังเกตซึ่งการปิดระยะสั้นในตัวแปรบางตัวทำให้มันถูกทำให้มีชีวิตโดยการปิดที่ยาวนาน

เราสามารถลองสร้างบันทึกสภาพแวดล้อมหลาย ๆ แบบสำหรับการปิดที่แตกต่างกันขึ้นอยู่กับสิ่งที่พวกเขาปิดจริง ๆ แต่มันสามารถซับซ้อนได้อย่างรวดเร็วมากและอาจทำให้เกิดปัญหาด้านประสิทธิภาพและหน่วยความจำของตัวเอง ...


ขอบคุณสำหรับความเข้าใจของคุณ ฉันมาสรุปว่านี่เป็นวิธีที่ Chrome ใช้ในการปิดเช่นกัน ฉันมักจะคิดว่ามันถูกนำไปใช้ในทางหลังซึ่งการปิดแต่ละครั้งจะรักษาสภาพแวดล้อมที่ต้องการเท่านั้น แต่ไม่ใช่ในกรณีนี้ ฉันสงสัยว่ามันซับซ้อนหรือไม่ที่จะสร้างเร็กคอร์ดสภาพแวดล้อมที่หลากหลาย แทนที่จะรวมการอ้างอิงของการปิดให้ทำราวกับว่าแต่ละคนเป็นคนเดียวที่ปิด ฉันเดาว่าการพิจารณาประสิทธิภาพเป็นเหตุผลที่นี่แม้ว่าฉันจะได้รับผลกระทบจากการมีสภาพแวดล้อมบันทึกที่ใช้ร่วมกันดูแย่ยิ่งขึ้น
พอลเดรเปอร์

วิธีหลังในบางกรณีนำไปสู่การระเบิดในจำนวนระเบียนสิ่งแวดล้อมที่ต้องสร้าง เว้นแต่คุณจะพยายามแบ่งปันฟังก์ชั่นต่าง ๆ เมื่อคุณทำได้ แต่คุณต้องใช้เครื่องจักรที่ซับซ้อนเพื่อทำเช่นนั้น เป็นไปได้ แต่ฉันบอกว่าการแลกเปลี่ยนผลการปฏิบัติงานสนับสนุนวิธีการปัจจุบัน
Boris Zbarsky

จำนวนของระเบียนเท่ากับจำนวนของการปิดที่สร้างขึ้น ฉันอาจอธิบายO(n^2)หรือO(2^n)เป็นการระเบิด แต่ไม่ใช่การเพิ่มสัดส่วน
พอลเดรเปอร์

O (N) คือการระเบิดเมื่อเทียบกับ O (1) โดยเฉพาะเมื่อแต่ละคนสามารถใช้หน่วยความจำได้พอสมควร ... อีกครั้งฉันไม่ใช่ผู้เชี่ยวชาญในเรื่องนี้ การถามช่อง #jsapi บน irc.mozilla.org มีแนวโน้มที่จะทำให้คุณได้คำอธิบายที่ดีและมีรายละเอียดมากขึ้นกว่าที่ฉันจะได้รับจากการแลกเปลี่ยน
Boris Zbarsky

1
@Esailija จริงๆแล้วมันเป็นเรื่องธรรมดาที่น่าเสียดาย สิ่งที่คุณต้องมีคือชั่วคราวขนาดใหญ่ในฟังก์ชั่น (โดยทั่วไปคืออาเรย์ที่พิมพ์ขนาดใหญ่) ที่การโทรกลับแบบสั้น ๆ แบบสุ่มและการปิดใช้งานระยะยาว มันเกิดขึ้นหลายครั้งเมื่อเร็ว ๆ นี้สำหรับคนที่เขียนเว็บแอป ...
บอริส Zbarsky

0
  1. รักษาสถานะระหว่างการเรียกใช้ฟังก์ชันสมมติว่าคุณมี function add () และคุณต้องการให้เพิ่มค่าทั้งหมดที่ส่งไปยังการเรียกหลาย ๆ ครั้งและส่งคืนผลรวม

ชอบเพิ่ม (5); // ส่งคืน 5

เพิ่ม (20); // ส่งคืน 25 (5 + 20)

เพิ่ม (3) // ส่งคืน 28 (25 + 3)

สองวิธีที่คุณสามารถทำได้ก่อนอื่นคือกำหนดตัวแปรทั่วโลก แน่นอนคุณสามารถใช้ตัวแปรส่วนกลางเพื่อเก็บผลรวม แต่พึงระลึกไว้เสมอว่าเพื่อนคนนี้จะกินคุณหากยังใช้ชีวิตอยู่

วิธีล่าสุดในการใช้การปิดโดยไม่ต้องกำหนดตัวแปรทั่วโลก

(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.