เมื่อไม่นานมานี้ ฉันได้สัมภาษณ์ผู้สมัครบางคนที่กำลังมองหางานในตำแหน่ง Senior Java Development Engineer ฉันมักจะสัมภาษณ์พวกเขาและพูดว่า "คุณช่วยแนะนำฉันเกี่ยวกับการอ้างอิงที่อ่อนแอในภาษา Java ได้ไหม" ถ้าผู้สัมภาษณ์พูดว่า "มันเกี่ยวข้องกับการเก็บขยะหรือไม่" โดยพื้นฐานแล้วฉันจะพอใจ และฉันจะไม่ทำ อย่าคาดหวังว่าคำตอบจะเป็นคำอธิบายของบทความที่จะเจาะลึกลงไปถึงก้นบึ้งของสิ่งต่างๆ
อย่างไรก็ตาม ตรงกันข้ามกับที่คาดไว้ ฉันรู้สึกประหลาดใจที่พบว่าในบรรดาผู้สมัครเกือบ 20 คนที่มีประสบการณ์การพัฒนาโดยเฉลี่ย 5 ปีและมีภูมิหลังที่มีการศึกษาสูง มีเพียงสองคนเท่านั้นที่รู้เกี่ยวกับการมีอยู่ของการอ้างอิงที่อ่อนแอ แต่มีเพียงหนึ่งในสองคนนี้จริงๆ เรียนรู้เกี่ยวกับเรื่องนี้ ในระหว่างขั้นตอนการสัมภาษณ์ ฉันยังพยายามถามบางอย่างเพื่อดูว่ามีใครจู่ๆ ก็พูดว่า "เอาล่ะ" แต่ผลลัพธ์ที่ได้กลับน่าผิดหวังมาก ฉันเริ่มสงสัยว่าเหตุใดความรู้ชิ้นนี้จึงถูกละเลย ท้ายที่สุดแล้ว การอ้างอิงที่อ่อนแอถือเป็นฟีเจอร์ที่มีประโยชน์มากและฟีเจอร์นี้ถูกนำมาใช้เมื่อ Java 1.2 เปิดตัวเมื่อ 7 ปีที่แล้ว
ฉันไม่ได้คาดหวังให้คุณเป็นผู้เชี่ยวชาญด้านการอ้างอิงที่อ่อนแอหลังจากอ่านบทความนี้ แต่ฉันคิดว่าอย่างน้อยคุณควรเข้าใจว่าการอ้างอิงที่อ่อนแอคืออะไร วิธีใช้งาน และสถานการณ์ที่ใช้ เนื่องจากเป็นแนวคิดที่ไม่รู้จัก ผมจะอธิบายคำถามสามข้อก่อนหน้านี้โดยย่อ
การอ้างอิงที่แข็งแกร่ง
การอ้างอิงที่รัดกุมคือการอ้างอิงที่เรามักใช้และเขียนไว้ดังนี้:
คัดลอกรหัสรหัสดังต่อไปนี้:
StringBuffer buffer = StringBuffer ใหม่ ();
ข้างต้นสร้างวัตถุ StringBuffer และจัดเก็บการอ้างอิง (ที่รัดกุม) ไปยังวัตถุนี้ในบัฟเฟอร์ตัวแปร ใช่ นี่คือการผ่าตัดในเด็ก (ขออภัยที่ต้องพูดแบบนี้) สิ่งที่สำคัญที่สุดเกี่ยวกับการอ้างอิงที่รัดกุมก็คือ สามารถทำให้การอ้างอิงนั้นรัดกุมได้ ซึ่งจะกำหนดการโต้ตอบกับตัวรวบรวมขยะ โดยเฉพาะอย่างยิ่ง หากวัตถุสามารถเข้าถึงได้ผ่านสตริงที่มีลิงก์อ้างอิงอย่างมาก (สามารถเข้าถึงได้อย่างมาก) วัตถุนั้นจะไม่ถูกรีไซเคิล นี่คือสิ่งที่คุณต้องการอย่างแน่นอน หากคุณไม่ต้องการให้สิ่งของที่คุณทำงานด้วยถูกรีไซเคิล
แต่คำพูดที่แข็งแกร่งนั้นแข็งแกร่งมาก
ในโปรแกรม เป็นเรื่องปกติที่จะตั้งค่าคลาสให้ไม่สามารถขยายได้ แน่นอนว่าสามารถทำได้โดยการทำเครื่องหมายคลาสว่าเป็นคลาสสุดท้าย หรืออาจซับซ้อนกว่านี้ได้ ซึ่งก็คือ การส่งคืนอินเทอร์เฟซ (Interface) ผ่านวิธีการแบบโรงงานซึ่งมีการใช้งานเฉพาะจำนวนที่ไม่ทราบจำนวน ตัวอย่างเช่น เราต้องการใช้คลาสที่เรียกว่า Widget แต่คลาสนี้ไม่สามารถสืบทอดได้ ดังนั้นจึงไม่สามารถเพิ่มฟังก์ชันใหม่ได้
แต่เราควรทำอย่างไรหากเราต้องการติดตามข้อมูลเพิ่มเติมเกี่ยวกับวัตถุ Widget? สมมติว่าเราจำเป็นต้องบันทึกหมายเลขซีเรียลของแต่ละอ็อบเจ็กต์ แต่เนื่องจากคลาส Widget ไม่มีแอ็ตทริบิวต์นี้และไม่สามารถขยายได้ เราจึงไม่สามารถเพิ่มแอ็ตทริบิวต์นี้ได้ ในความเป็นจริงไม่มีปัญหาเลย HashMap สามารถแก้ไขปัญหาข้างต้นได้อย่างสมบูรณ์
คัดลอกรหัสรหัสดังต่อไปนี้:
serialNumberMap.put (วิดเจ็ต, widgetSerialNumber);
เมื่อดูเผินๆ อาจดูเหมือนปกติ แต่การอ้างอิงที่ชัดเจนกับอ็อบเจ็กต์วิดเจ็ตอาจทำให้เกิดปัญหาได้ เรามั่นใจได้ว่าเมื่อไม่จำเป็นต้องใช้หมายเลขซีเรียลของวิดเจ็ตอีกต่อไป เราควรลบรายการออกจากแผนที่ หากเราไม่ลบออก อาจทำให้หน่วยความจำรั่วไหล หรือเราอาจลบวิดเจ็ตที่เราใช้เมื่อเราลบออกด้วยตนเอง ซึ่งอาจทำให้ข้อมูลที่ถูกต้องสูญหายได้ จริงๆ แล้วปัญหาเหล่านี้คล้ายกันมาก นี่เป็นปัญหาที่ภาษาที่ไม่มีกลไกการเก็บขยะมักพบเมื่อจัดการหน่วยความจำ แต่เราไม่ต้องกังวลกับปัญหานี้ เนื่องจากเราใช้ภาษา Java กับกลไกการรวบรวมขยะ
ปัญหาอีกประการหนึ่งที่การอ้างอิงที่รัดกุมอาจทำให้เกิดการแคช โดยเฉพาะอย่างยิ่งสำหรับไฟล์ขนาดใหญ่ เช่น รูปภาพ สมมติว่าคุณมีโปรแกรมที่ต้องประมวลผลรูปภาพที่ผู้ใช้ให้มา วิธีการทั่วไปคือการแคชข้อมูลรูปภาพ เนื่องจากการโหลดรูปภาพจากดิสก์มีราคาแพงมาก และในขณะเดียวกัน เราก็ต้องการหลีกเลี่ยงการมีสำเนาข้อมูลรูปภาพเดียวกันสองชุดด้วย ในความทรงจำไปพร้อมๆ กัน
วัตถุประสงค์ของการแคชคือเพื่อป้องกันไม่ให้เราโหลดไฟล์ที่ไม่จำเป็นอีกครั้ง คุณจะพบว่าแคชมักจะมีการอ้างอิงถึงข้อมูลรูปภาพในหน่วยความจำเสมอ การใช้การอ้างอิงที่รัดกุมจะบังคับให้ข้อมูลรูปภาพอยู่ในหน่วยความจำ ซึ่งคุณต้องตัดสินใจว่าเมื่อใดจึงไม่จำเป็นต้องใช้ข้อมูลรูปภาพอีกต่อไป และนำข้อมูลรูปภาพออกจากแคชด้วยตนเองเพื่อให้ตัวรวบรวมขยะสามารถเรียกคืนข้อมูลได้ ดังนั้นคุณจึงถูกบังคับให้ทำสิ่งที่คนเก็บขยะทำอีกครั้งและตัดสินใจด้วยตนเองว่าจะทำความสะอาดวัตถุใด
การอ้างอิงที่อ่อนแอ
การอ้างอิงที่อ่อนแอเป็นเพียงการอ้างอิงที่ไม่ชัดเจนนักในการเก็บวัตถุไว้ในหน่วยความจำ การใช้ WeakReference ตัวรวบรวมขยะจะช่วยคุณตัดสินใจว่าเมื่อใดที่วัตถุอ้างอิงจะถูกรีไซเคิลและลบวัตถุออกจากหน่วยความจำ สร้างการอ้างอิงที่ไม่รัดกุมดังนี้:
คัดลอกรหัสรหัสดังต่อไปนี้:
eakReference<Widget>อ่อนแอWidget = ใหม่ WeakReference<Widget>(วิดเจ็ต);
คุณสามารถรับอ็อบเจ็กต์ Widget จริงได้โดยใช้ WeakWidget.get() เนื่องจากการอ้างอิงที่อ่อนแอไม่สามารถป้องกันตัวรวบรวมขยะจากการรีไซเคิลได้ คุณจะพบว่า (เมื่อไม่มีการอ้างอิงที่ชัดเจนไปยังอ็อบเจ็กต์วิดเจ็ต) จะมีการส่งคืนค่า null ทันทีเมื่อใช้ รับ.
วิธีที่ง่ายที่สุดในการแก้ปัญหาข้างต้นของการบันทึกหมายเลขลำดับวิดเจ็ตคือการใช้คลาส WeakHashMap ในตัวของ Java WeakHashMap เกือบจะเหมือนกับ HashMap ข้อแตกต่างเพียงอย่างเดียวคือคีย์ของมัน (ไม่ใช่ค่า!!!) ถูกอ้างอิงโดย WeakReference เมื่อคีย์ WeakHashMap ถูกทำเครื่องหมายว่าเป็นขยะ รายการที่สอดคล้องกับคีย์นี้จะถูกลบออกโดยอัตโนมัติ วิธีนี้จะหลีกเลี่ยงปัญหาข้างต้นด้วยการลบออบเจ็กต์ Widget ที่ไม่จำเป็นด้วยตนเอง WeakHashMap สามารถแปลงเป็น HashMap หรือ Map ได้อย่างง่ายดาย
คิวอ้างอิง
เมื่อวัตถุอ้างอิงที่อ่อนแอเริ่มส่งคืนค่าว่าง วัตถุที่อ้างอิงถึงที่อ่อนแอจะถูกทำเครื่องหมายว่าเป็นขยะ และวัตถุอ้างอิงที่อ่อนแอนี้ (ไม่ใช่วัตถุที่มันชี้ไป) ก็ไม่มีประโยชน์ โดยปกติแล้วการล้างข้อมูลบางอย่างจะต้องดำเนินการในเวลานี้ ตัวอย่างเช่น WeakHashMap จะลบรายการที่ไม่มีประโยชน์ออกในขณะนี้ เพื่อหลีกเลี่ยงการจัดเก็บการอ้างอิงที่ไม่ชัดเจนที่ไม่มีความหมายซึ่งเพิ่มขึ้นอย่างไม่มีกำหนด
คิวการอ้างอิงทำให้ง่ายต่อการติดตามการอ้างอิงที่ไม่ต้องการ เมื่อคุณส่งวัตถุ ReferenceQueue เมื่อสร้าง WeakReference เมื่อวัตถุที่อ้างอิงชี้ไปถูกทำเครื่องหมายว่าเป็นขยะ วัตถุอ้างอิงจะถูกเพิ่มลงในคิวอ้างอิงโดยอัตโนมัติ ถัดไป คุณสามารถประมวลผลคิวอ้างอิงขาเข้าในช่วงเวลาที่กำหนด เช่น การทำงานล้างข้อมูลเพื่อจัดการกับออบเจ็กต์อ้างอิงที่ไม่มีประโยชน์เหล่านี้
การอ้างอิงสี่ประเภท
จริงๆ แล้วมีการอ้างอิงสี่ประเภทที่มีจุดแข็งที่แตกต่างกันใน Java จากการอ้างอิงที่รัดกุมไปจนถึงการอ้างอิงที่อ่อนแอ ส่วนด้านบนจะแนะนำการอ้างอิงที่รัดกุมและการอ้างอิงที่อ่อนแอ และส่วนต่อไปนี้จะอธิบายส่วนที่เหลืออีกสองรายการคือการอ้างอิงแบบ soft และการอ้างอิงเสมือน
การอ้างอิงแบบอ่อน
โดยพื้นฐานแล้วการอ้างอิงแบบอ่อนนั้นเหมือนกับการอ้างอิงแบบอ่อน ยกเว้นว่าจะมีความสามารถที่แข็งแกร่งกว่าในการป้องกันไม่ให้ระยะเวลาการรวบรวมขยะรีไซเคิลออบเจ็กต์ที่อ้างอิงนั้นชี้ไปมากกว่าการอ้างอิงแบบอ่อน หากวัตถุสามารถเข้าถึงได้โดยการอ้างอิงที่อ่อนแอ วัตถุจะถูกทำลายโดยตัวรวบรวมขยะในรอบการรวบรวมถัดไป แต่ถ้าสามารถเข้าถึงการอ้างอิงแบบซอฟต์ได้ วัตถุก็จะอยู่ในหน่วยความจำเป็นเวลานานขึ้น ตัวรวบรวมขยะจะเรียกคืนออบเจ็กต์ที่สามารถเข้าถึงได้โดยการอ้างอิงแบบซอฟต์เหล่านี้เมื่อมีหน่วยความจำไม่เพียงพอ
เนื่องจากวัตถุที่สามารถเข้าถึงได้โดยการอ้างอิงแบบอ่อนจะอยู่ในหน่วยความจำนานกว่าวัตถุที่สามารถเข้าถึงได้โดยการอ้างอิงแบบอ่อน เราจึงสามารถใช้คุณลักษณะนี้สำหรับการแคชได้ ด้วยวิธีนี้ คุณสามารถบันทึกสิ่งต่างๆ ได้มากมาย ตัวรวบรวมขยะจะสนใจว่าประเภทใดที่สามารถเข้าถึงได้ในปัจจุบันและระดับการใช้หน่วยความจำในการประมวลผล
การอ้างอิงผี
ต่างจากการอ้างอิงแบบซอฟต์และการอ้างอิงแบบอ่อน วัตถุที่อ้างอิงถึงเสมือนนั้นเปราะบางมากและเราไม่สามารถรับวัตถุที่พวกมันชี้ไปผ่านเมธอด get ได้ ฟังก์ชันเดียวของมันคือหลังจากที่วัตถุที่วัตถุชี้ไปถูกรีไซเคิลแล้ว มันจะถูกเพิ่มเข้าไปในคิวอ้างอิงเพื่อบันทึกว่าวัตถุที่อ้างอิงนั้นถูกทำลายไปแล้ว
เมื่อวัตถุที่ชี้ไปโดยการอ้างอิงที่อ่อนแอนั้นสามารถเข้าถึงได้โดยการอ้างอิงที่อ่อนแอ การอ้างอิงที่อ่อนแอจะถูกเพิ่มลงในคิวการอ้างอิง การดำเนินการนี้เกิดขึ้นก่อนที่จะมีการทำลายวัตถุหรือการรวบรวมขยะเกิดขึ้นจริง ตามทฤษฎีแล้ว วัตถุที่กำลังจะรีไซเคิลสามารถฟื้นคืนชีพได้ด้วยวิธีการทำลายล้างที่ไม่เป็นไปตามข้อกำหนด แต่การอ้างอิงที่อ่อนแอนี้จะถูกทำลาย การอ้างอิงเสมือนจะถูกเพิ่มลงในคิวการอ้างอิงหลังจากออบเจ็กต์ที่ชี้ไปถูกลบออกจากหน่วยความจำเท่านั้น วิธีการ get จะส่งคืนค่าว่างเพื่อป้องกันไม่ให้วัตถุที่เกือบถูกทำลายซึ่งชี้ไปจากการฟื้นคืนชีพ
มีสองสถานการณ์การใช้งานหลักสำหรับการอ้างอิงเสมือน ช่วยให้คุณทราบได้อย่างชัดเจนว่าเมื่อใดที่วัตถุที่อ้างถึงถูกลบออกจากหน่วยความจำ และจริงๆ แล้วนี่เป็นวิธีเดียวใน Java โดยเฉพาะอย่างยิ่งเมื่อต้องรับมือกับไฟล์ขนาดใหญ่ เช่น รูปภาพ เมื่อคุณตัดสินใจว่าควรรีไซเคิลออบเจ็กต์ข้อมูลรูปภาพ คุณสามารถใช้การอ้างอิงเสมือนเพื่อตรวจสอบว่าออบเจ็กต์นั้นถูกรีไซเคิลหรือไม่ ก่อนที่จะโหลดรูปภาพถัดไปต่อไป วิธีนี้คุณสามารถหลีกเลี่ยงข้อผิดพลาดหน่วยความจำล้นที่แย่มากได้มากที่สุด
ประเด็นที่สองคือการอ้างอิงเสมือนสามารถหลีกเลี่ยงปัญหามากมายในระหว่างการทำลายได้ วิธีการสรุปสามารถฟื้นคืนชีพวัตถุที่กำลังจะถูกทำลายโดยการสร้างการอ้างอิงที่ชัดเจนซึ่งชี้ไปที่วัตถุเหล่านั้น อย่างไรก็ตาม ออบเจ็กต์ที่แทนที่วิธีการสรุปจะต้องผ่านรอบการรวบรวมขยะสองรอบแยกกันหากต้องการรีไซเคิล ในรอบแรก วัตถุจะถูกทำเครื่องหมายว่าสามารถรีไซเคิลได้ จากนั้นจึงถูกทำลายได้ แต่เนื่องจากยังมีความเป็นไปได้เล็กน้อยที่วัตถุนี้จะถูกฟื้นคืนชีพในระหว่างกระบวนการทำลายล้าง ในกรณีนี้ ตัวรวบรวมขยะจำเป็นต้องทำงานอีกครั้งก่อนที่วัตถุจะถูกทำลายจริง เนื่องจากการทำลายล้างอาจไม่ทันเวลานัก จึงต้องผ่านรอบการรวบรวมขยะเป็นจำนวนไม่แน่นอนก่อนจะเรียกการทำลายวัตถุ ซึ่งหมายความว่าอาจมีความล่าช้าอย่างมากในการทำความสะอาดออบเจ็กต์จริงๆ นี่คือเหตุผลที่คุณยังคงได้รับข้อผิดพลาดหน่วยความจำไม่เพียงพอที่น่ารำคาญ เมื่อฮีปส่วนใหญ่ถูกทำเครื่องหมายว่าเป็นขยะ
การใช้การอ้างอิงเสมือน สถานการณ์ข้างต้นจะได้รับการแก้ไข เมื่อมีการเพิ่มการอ้างอิงเสมือนในคิวการอ้างอิง คุณจะไม่มีทางได้รับวัตถุที่ถูกทำลายอย่างแน่นอน เพราะในเวลานี้วัตถุได้ถูกทำลายไปจากความทรงจำแล้ว เนื่องจากไม่สามารถใช้การอ้างอิงเสมือนเพื่อสร้างวัตถุที่อ้างอิงไปใหม่ได้ วัตถุนั้นจะถูกล้างข้อมูลในรอบแรกของการรวบรวมขยะ
แน่นอนว่าไม่แนะนำให้แทนที่วิธีการสรุปผล เนื่องจากเห็นได้ชัดว่าการอ้างอิงเสมือนนั้นปลอดภัยและมีประสิทธิภาพ การลบวิธีการสรุปออกจะทำให้เครื่องเสมือนง่ายขึ้นอย่างมาก แน่นอน คุณสามารถแทนที่วิธีนี้เพื่อให้บรรลุผลสำเร็จมากขึ้นได้ ทั้งหมดนี้ขึ้นอยู่กับทางเลือกส่วนบุคคล
สรุป
ฉันคิดว่าหลายคนเริ่มบ่นว่าทำไมคุณถึงพูดถึง API เก่า ๆ ในช่วงสิบปีที่ผ่านมา จากประสบการณ์ของฉัน โปรแกรมเมอร์ Java หลายคนไม่รู้จักความรู้นี้ดีนัก -จำเป็นต้องมีความเข้าใจอย่างลึกซึ้ง และฉันหวังว่าทุกคนจะได้รับประโยชน์บางอย่างจากบทความนี้