ทำความเข้าใจกับการรวบรวมขยะใน. NET


170

พิจารณารหัสด้านล่าง:

public class Class1
{
    public static int c;
    ~Class1()
    {
        c++;
    }
}

public class Class2
{
    public static void Main()
    {
        {
            var c1=new Class1();
            //c1=null; // If this line is not commented out, at the Console.WriteLine call, it prints 1.
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(Class1.c); // prints 0
        Console.Read();
    }
}

ถึงแม้ว่าตัวแปร c1 ในวิธีการหลักจะไม่อยู่ในขอบเขตและไม่ได้อ้างอิงเพิ่มเติมโดยวัตถุอื่นใดเมื่อGC.Collect()เรียกว่าทำไมมันไม่ได้สรุปที่นั่น?


8
GC ไม่ได้เพิ่มอินสแตนซ์ให้ทันทีเมื่ออยู่นอกขอบเขต มันทำเช่นนั้นเมื่อคิดว่าจำเป็น คุณสามารถอ่านทุกอย่างเกี่ยวกับ GC ได้ที่นี่: msdn.microsoft.com/en-US/library/vstudio/0xy59wtx.aspx
1908061

@ user1908061 (Pssst. ลิงก์ของคุณเสีย)
Dragomok

คำตอบ:


352

คุณกำลังสะดุดที่นี่และทำข้อสรุปที่ผิดมากเพราะคุณใช้ดีบักเกอร์ คุณจะต้องเรียกใช้รหัสในแบบที่มันรันบนเครื่องของผู้ใช้ของคุณ สลับไปยัง Release build ก่อนด้วย Build + Configuration manager เปลี่ยนคำสั่งผสม "Active solution configuration" ที่มุมซ้ายบนเป็น "Release" จากนั้นไปที่เครื่องมือ + ตัวเลือกการดีบักทั่วไปและยกเลิกการเลือกตัวเลือก "ระงับการเพิ่มประสิทธิภาพ JIT"

ตอนนี้เรียกใช้โปรแกรมของคุณอีกครั้งและคนจรจัดกับซอร์สโค้ด โปรดสังเกตว่าการจัดฟันแบบพิเศษไม่มีผลกระทบเลย และให้สังเกตว่าการตั้งค่าตัวแปรให้เป็นโมฆะนั้นไม่แตกต่างกันอย่างไร มันจะพิมพ์ "1" เสมอ ตอนนี้ใช้งานได้ตามที่คุณคาดหวังและคาดว่าจะใช้ได้

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

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

ตารางนี้มีความสำคัญต่อตัวรวบรวมข้อมูลขยะจำเป็นต้องทราบตำแหน่งที่จะค้นหาการอ้างอิงวัตถุเมื่อทำการรวบรวม ค่อนข้างง่ายที่จะทำเมื่อการอ้างอิงเป็นส่วนหนึ่งของวัตถุในกอง GC ไม่ง่ายอย่างแน่นอนเมื่อทำการอ้างอิงวัตถุถูกเก็บไว้ใน CPU register ตารางบอกตำแหน่งที่จะมอง

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

วิธีเวทมนต์เกือบที่เกี่ยวข้องกับตารางนั้นคือ GC.KeepAlive () มันเป็นวิธีที่พิเศษมากมันไม่ได้สร้างรหัสใด ๆ เลย หน้าที่เพียงอย่างเดียวคือแก้ไขตารางนั้น มันขยายอายุการใช้งานของตัวแปรโลคัลป้องกันการอ้างอิงที่เก็บจากการรวบรวมขยะ ครั้งเดียวที่คุณจำเป็นต้องใช้มันคือการหยุด GC ไม่ให้ไปมากเกินไปด้วยการรวบรวมการอ้างอิงซึ่งสามารถเกิดขึ้นได้ในสถานการณ์ interop ที่การอ้างอิงถูกส่งผ่านไปยังโค้ดที่ไม่มีการจัดการ ตัวรวบรวมขยะไม่สามารถมองเห็นการอ้างอิงดังกล่าวที่ใช้โดยรหัสดังกล่าวได้เนื่องจากไม่ได้รวบรวมโดย jitter ดังนั้นจึงไม่มีตารางที่ระบุตำแหน่งที่จะค้นหาการอ้างอิง การส่งวัตถุผู้รับมอบสิทธิ์ไปยังฟังก์ชันที่ไม่มีการจัดการเช่น EnumWindows () เป็นตัวอย่างสำเร็จรูปเมื่อคุณต้องการใช้ GC.KeepAlive ()

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

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

การตั้งค่าตัวแปรเป็น null จะมีผลทันทีเนื่องจาก GC จะตรวจสอบตัวแปรและจะไม่เห็นการอ้างอิงอีกต่อไป แต่ให้แน่ใจว่าคุณจะไม่ตกหลุมพรางที่โปรแกรมเมอร์ C # หลายคนตกลงไปจริง ๆ แล้วการเขียนโค้ดนั้นไม่มีประโยชน์ มันไม่ได้ทำให้ความแตกต่างใด ๆ ไม่ว่าจะมีคำสั่งนั้นหรือไม่เมื่อคุณเรียกใช้รหัสในรุ่นที่วางจำหน่าย ในความเป็นจริงแล้วเครื่องมือเพิ่มประสิทธิภาพของ jitter จะลบคำสั่งนั้นเนื่องจากจะไม่มีผลกระทบใด ๆ ดังนั้นอย่าเขียนรหัสเช่นนั้นแม้ว่าจะดูเหมือนว่าจะมีผลกระทบ


สิ่งสุดท้ายที่ทราบเกี่ยวกับหัวข้อนี้คือสิ่งที่ทำให้โปรแกรมเมอร์มีปัญหาในการเขียนโปรแกรมขนาดเล็กเพื่อทำอะไรกับแอป Office โปรแกรมดีบั๊กมักจะได้รับพวกเขาบนเส้นทางที่ผิดพวกเขาต้องการให้โปรแกรม Office ออกตามความต้องการ วิธีที่เหมาะสมในการทำเช่นนั้นคือการโทร GC.Collect () แต่พวกเขาจะค้นพบว่ามันไม่ทำงานเมื่อพวกเขาดีบั๊กแอปของพวกเขานำพวกเขาไปสู่ดินแดนที่ไม่เคยมีมาก่อนโดยเรียก Marshal.ReleaseComObject () การจัดการหน่วยความจำแบบแมนนวลมันใช้งานไม่ค่อยได้เพราะพวกมันจะมองข้ามการอ้างอิงอินเตอร์เฟสที่มองไม่เห็นได้อย่างง่ายดาย GC.Collect () ใช้งานได้จริงไม่ใช่แค่เมื่อคุณดีบักแอป


1
ดูคำถามของฉันที่ฮันส์ตอบอย่างดีสำหรับฉัน stackoverflow.com/questions/15561025/…
เดฟเนย์

1
@HansPassant ฉันเพิ่งพบคำอธิบายที่ยอดเยี่ยมนี้ซึ่งตอบคำถามของฉันได้ที่นี่: stackoverflow.com/questions/30529379/…เกี่ยวกับ GC และการซิงโครไนซ์เธรด คำถามหนึ่งที่ฉันยังมี: ฉันสงสัยว่า GC จะบีบอัดและอัปเดตที่อยู่ที่ใช้ในการลงทะเบียนจริง (เก็บไว้ในหน่วยความจำขณะที่ถูกระงับ) หรือข้ามไปหรือไม่ กระบวนการที่อัปเดตการลงทะเบียนหลังจากระงับเธรด (ก่อนที่จะดำเนินการต่อ) ให้ความรู้สึกกับฉันเหมือนกับชุดความปลอดภัยที่ร้ายแรงซึ่งถูกบล็อกโดยระบบปฏิบัติการ
atlaste

ทางอ้อมใช่ เธรดถูกหยุดชั่วคราว GC จะอัปเดตที่เก็บข้อมูลสำรองสำหรับการลงทะเบียน CPU เมื่อเธรดเริ่มทำงานต่อตอนนี้จะใช้ค่าการลงทะเบียนที่อัพเดตแล้ว
ฮันส์แพสแทนท์

1
@HansPassant ฉันจะขอบคุณถ้าคุณเพิ่มการอ้างอิงสำหรับรายละเอียดที่ไม่ชัดเจนของตัวเก็บขยะ CLR ที่คุณอธิบายไว้ที่นี่?
denfromufa

ดูเหมือนว่าการกำหนดค่าอย่างชาญฉลาดจุดสำคัญคือ " เปิดใช้งานรหัสเพิ่มประสิทธิภาพ" ( <Optimize>true</Optimize>ใน.csproj) นี่เป็นค่าเริ่มต้นในการกำหนดค่า "Release" แต่ในกรณีที่หนึ่งใช้การกำหนดค่าแบบกำหนดเองจะต้องทราบว่าการตั้งค่านี้มีความสำคัญ
Zero3

34

[แค่ต้องการเพิ่มเพิ่มเติมเกี่ยวกับกระบวนการ Internals of Finalization]

ดังนั้นคุณสร้างวัตถุและเมื่อรวบรวมวัตถุFinalizeวิธีการของวัตถุควรจะเรียกว่า แต่มีการสรุปมากกว่าสมมติฐานง่าย ๆ นี้

แนวคิดสั้น ::

  1. วัตถุที่ไม่ได้ใช้Finalizeวิธีการนั้นจะมีการเรียกคืนหน่วยความจำทันทีเว้นแต่จะไม่สามารถเข้าถึงได้ด้วย
    รหัสแอปพลิเคชันอีกต่อไป

  2. วัตถุการใช้Finalizeวิธีการแนวคิด / การดำเนินงานของApplication Roots, Finalization Queue, Freacheable Queueมาก่อนที่พวกเขาสามารถเรียกคืน

  3. วัตถุใด ๆ ถือว่าเป็นขยะหากไม่สามารถเข้าถึงได้โดยรหัสแอปพลิเคชัน

สมมติว่า :: คลาส / วัตถุ A, B, D, G, H ไม่ใช้Finalizeวิธีการและ C, E, F, I, J ใช้Finalizeวิธีการ

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

ดังนั้นตัวชี้ไปยังวัตถุ C, E, F, I, J ได้รับการเพิ่มในคิวการสรุป คิวสรุปเป็นโครงสร้างข้อมูลภายในที่ควบคุมโดยการเก็บขยะ แต่ละรายการในคิวชี้ไปยังวัตถุที่ควรมีวิธีการที่เรียกว่าก่อนที่หน่วยความจำของวัตถุสามารถเรียกคืนได้ รูปด้านล่างแสดงฮีปที่มีหลายวัตถุ บางส่วนของวัตถุเหล่านี้สามารถเข้าถึงได้จากรากของแอปพลิเคชัน

Finalizeและบางคนไม่ เมื่อวัตถุ C, E, F, I, J และถูกสร้างขึ้นสุทธิตรวจกรอบที่วัตถุเหล่านี้มีFinalizeวิธีการและตัวชี้ไปยังวัตถุเหล่านี้จะถูกเพิ่มลงในคิวการสรุป

ป้อนคำอธิบายรูปภาพที่นี่

เมื่อ GC เกิดขึ้น (คอลเล็กชันที่ 1) วัตถุ B, E, G, H, I และ J ถูกกำหนดให้เป็นขยะ เนื่องจาก A, C, D, F ยังสามารถเข้าถึงได้โดยรหัสแอปพลิเคชันที่แสดงผ่านลูกศรจากกล่องสีเหลืองด้านบน

ตัวรวบรวมขยะจะสแกนคิวการสรุปสุดท้ายเพื่อค้นหาตัวชี้ไปยังวัตถุเหล่านี้ เมื่อพบตัวชี้ตัวชี้จะถูกลบออกจากคิวการสรุปสุดท้ายและต่อท้ายคิวที่สามารถเข้าถึงได้ ("เข้าถึงได้ F") คิว freachableเป็นโครงสร้างข้อมูลอื่นภายในการควบคุมโดยการเก็บขยะ ตัวชี้แต่ละตัวในคิวที่สามารถเข้าถึงได้ระบุวัตถุที่พร้อมที่จะมีวิธีการที่เรียกว่า

Finalize

หลังจากคอลเล็กชัน (คอลเล็กชันที่ 1) ฮีปที่ได้รับการจัดการมีลักษณะคล้ายกับรูปด้านล่าง คำอธิบายด้านล่าง ::
1. ) หน่วยความจำที่ครอบครองโดยวัตถุ B, G และ H ถูกเรียกคืนทันทีเนื่องจากวัตถุเหล่านี้ไม่มีวิธีการสรุปที่จำเป็นต้องเรียกใช้

2. ) อย่างไรก็ตามหน่วยความจำที่ครอบครองโดยวัตถุ E, I และ J ไม่สามารถเรียกคืนได้เนื่องจากFinalizeวิธีการของพวกเขายังไม่ได้เรียก การเรียกเมธอด Finalize ทำได้โดยคิวที่น่าปวดหัว

3. ) A, C, D, F ยังคงเข้าถึงได้โดยรหัสแอปพลิเคชันที่แสดงผ่านลูกศรจากกล่องสีเหลืองด้านบนดังนั้นพวกเขาจะไม่ถูกรวบรวมในกรณีใด ๆ

ป้อนคำอธิบายรูปภาพที่นี่

มีเธรดรันไทม์พิเศษที่ทุ่มเทให้กับการเรียกวิธีการสุดท้าย เมื่อคิวที่สามารถเข้าถึงได้ว่างเปล่า (ซึ่งโดยปกติจะเป็นกรณี) เธรดนี้จะพัก แต่เมื่อรายการปรากฏขึ้นเธรดนี้จะเอาออกแต่ละรายการออกจากคิวและเรียกวิธีการสุดท้ายของแต่ละวัตถุ ตัวรวบรวมขยะจะบีบอัดหน่วยความจำที่สามารถเรียกคืนได้และเธรดรันไทม์พิเศษจะทำการล้างคิวที่สามารถเข้าถึงได้ดำเนินการFinalizeวิธีการของแต่ละวัตถุ ดังนั้นที่นี่ในที่สุดก็คือเมื่อวิธีสุดท้ายของคุณได้รับการดำเนินการ

ครั้งต่อไปที่ตัวรวบรวมขยะถูกเรียกใช้ (ชุดที่ 2) จะเห็นว่าวัตถุที่สรุปแล้วเป็นขยะจริงเนื่องจากรากของแอปพลิเคชันไม่ได้ชี้ไปที่มันและคิวที่สามารถเข้าถึงได้จะไม่ชี้ไปที่มันอีกต่อไป หน่วยความจำสำหรับวัตถุ (E, I, J) เป็นเพียงการเรียกคืนจากกองดูรูปด้านล่างและเปรียบเทียบกับรูปด้านบน

ป้อนคำอธิบายรูปภาพที่นี่

สิ่งสำคัญที่ต้องเข้าใจว่านี่คือสอง GCs จะต้องเรียกคืนหน่วยความจำที่ใช้โดยวัตถุที่จำเป็นต้องมีการสรุป ในความเป็นจริงจะต้องมีคอลเลกชันรถแท็กซี่มากกว่าสองคันเนื่องจากวัตถุเหล่านี้อาจได้รับการเลื่อนระดับเป็นรุ่นเก่ากว่า

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

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.