สวัสดี! ในการสนทนาวันนี้ เราจะพูดถึงรายละเอียดเกี่ยวกับ "การอ้างอิงหลอน" (PhantomReference) ใน Java สิ่งเหล่านี้เป็นการอ้างอิงประเภทใด? ทำไมพวกเขาถึงเรียกว่า "การอ้างอิงผี"? พวกเขาใช้อย่างไร? อย่างที่คุณจำได้ Java มีการอ้างอิง 4 ประเภท:
-
StrongReference (การอ้างอิงทั่วไปที่เราสร้างขึ้นเมื่อสร้างวัตถุ):
Cat cat = new Cat()
ในตัวอย่างนี้catเป็นข้อมูลอ้างอิงที่ชัดเจน
-
SoftReference (การอ้างอิงแบบอ่อน) เราได้บทเรียนเกี่ยวกับการอ้างอิงดังกล่าว
-
WeakReference (การอ้างอิงที่อ่อนแอ) นอกจากนี้ยังมีบทเรียนเกี่ยวกับพวกเขาที่นี่
-
PhantomReference (การอ้างอิงผี)
-
รับ () - ส่งคืนวัตถุอ้างอิง
- clear() — ลบการอ้างอิงไปยังวัตถุ
finalize()
วิธีการถูกแทนที่สำหรับวัตถุ มันถูกเรียก หรืออาจจะไม่ถูกเรียก — ทั้งหมดขึ้นอยู่กับว่าคุณโชคดีหรือไม่ คุณอาจจำได้ว่าfinalize()
มันไม่แน่นอน :) ในการผ่านครั้งที่สองของตัวเก็บขยะ วัตถุจะถูกลบและหน่วยความจำจะถูกปลดปล่อย พฤติกรรมที่คาดเดาไม่ได้ของคนเก็บขยะสร้างปัญหามากมายให้กับเรา เราไม่รู้แน่ชัดว่าตัวเก็บขยะจะเริ่มทำงานเมื่อใด เราไม่รู้ว่าfinalize()
เมธอดจะถูกเรียกใช้หรือไม่ นอกจากนี้ยังสามารถสร้างการอ้างอิงที่ชัดเจนถึงวัตถุได้ในขณะที่finalize()
กำลังดำเนินการวิธีของมัน ซึ่งในกรณีนี้วัตถุจะไม่ถูกลบเลย สำหรับโปรแกรมที่ต้องการหน่วยความจำที่มีจำนวนมาก สิ่งนี้สามารถนำไปสู่ไฟล์OutOfMemoryError
. ทั้งหมดนี้ผลักดันให้เราใช้การอ้างอิงแบบผี. ความจริงก็คือสิ่งนี้เปลี่ยนพฤติกรรมของคนเก็บขยะ หากออบเจ็กต์มีเพียงการอ้างอิงแบบหลอน ดังนั้น:
-
เมธอด Finalize()ของมันถูกเรียก (หากมันถูกแทนที่)
-
หากไม่มีอะไรเปลี่ยนแปลงเมื่อ เมธอด finalize()เสร็จสิ้นและยังสามารถลบออบเจกต์ได้ การอ้างอิงแฝงไปยังออบเจ็กต์จะถูกวางในคิวพิเศษ: ReferenceQueue
public class TestClass {
private StringBuffer data;
public TestClass() {
this.data = new StringBuffer();
for (long i = 0; i < 50000000; i++) {
this.data.append('x');
}
}
@Override
protected void finalize() {
System.out.println("The finalize method has been called on the TestClass object");
}
}
เมื่อเราสร้างออบเจกต์ เราจงใจให้ "โหลด" จำนวนมากแก่ออบเจ็กต์ (โดยการเพิ่มอักขระ "x" 50 ล้านตัวในแต่ละออบเจ็กต์) เพื่อให้ใช้หน่วยความจำมากขึ้น นอกจากนี้ เราลบล้างเมธอดFinalize()เพื่อดูว่ารันอยู่ ต่อ ไปเราต้องการคลาสที่จะสืบทอดจากPhantomReference ทำไมเราถึงต้องการคลาสดังกล่าว? มันตรงไปตรงมาทั้งหมด วิธี นี้จะช่วยให้เราสามารถเพิ่มตรรกะเพิ่มเติมให้กับเมธอดclear()เพื่อตรวจสอบว่าการอ้างอิงแฝงนั้นถูกล้างจริงๆ (ซึ่งหมายความว่าวัตถุนั้นถูกลบไปแล้ว)
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class MyPhantomReference<TestClass> extends PhantomReference<TestClass> {
public MyPhantomReference(TestClass obj, ReferenceQueue<TestClass> queue) {
super(obj, queue);
Thread thread = new QueueReadingThread<TestClass>(queue);
thread.start();
}
public void cleanup() {
System.out.println("Cleaning up a phantom reference! Removing an object from memory!");
clear();
}
}
ต่อไป เราต้องการเธรดแยกต่างหากที่จะ รอ ให้ตัวรวบรวมขยะทำงาน และลิงก์หลอนจะปรากฏในReferenceQueue ของเรา ทันทีที่การอ้างอิงดังกล่าวจบลงในคิว วิธี การล้างข้อมูล ()จะถูกเรียกใช้:
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
public class QueueReadingThread<TestClass> extends Thread {
private ReferenceQueue<TestClass> referenceQueue;
public QueueReadingThread(ReferenceQueue<TestClass> referenceQueue) {
this.referenceQueue = referenceQueue;
}
@Override
public void run() {
System.out.println("The thread monitoring the queue has started!");
Reference ref = null;
// Wait until the references appear in the queue
while ((ref = referenceQueue.poll()) == null) {
try {
Thread.sleep(50);
}
catch (InterruptedException e) {
throw new RuntimeException("Thread " + getName() + " was interrupted!");
}
}
// As soon as a phantom reference appears in the queue, clean it up
((MyPhantomReference) ref).cleanup();
}
}
และสุดท้าย เราต้องการ เมธอด main()ซึ่งเราจะใส่ไว้ในคลาสหลัก แยกต่างหาก ในวิธีการนั้น เราจะสร้าง ออบเจกต์ TestClassการอ้างอิงผี และคิวสำหรับการอ้างอิงผี หลังจากนั้นเราจะโทรหาคนเก็บขยะและดูว่าเกิดอะไรขึ้น :)
import java.lang.ref.*;
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(10000);
ReferenceQueue<TestClass> queue = new ReferenceQueue<>();
Reference ref = new MyPhantomReference<>(new TestClass(), queue);
System.out.println("ref = " + ref);
Thread.sleep(5000);
System.out.println("Collecting garbage!");
System.gc();
Thread.sleep(300);
System.out.println("ref = " + ref);
Thread.sleep(5000);
System.out.println("Collecting garbage!");
System.gc();
}
}
เอาต์พุตคอนโซล:
ref = MyPhantomReference@4554617c
The thread monitoring the queue has started!
Collecting garbage!
The finalize method has been called on the TestClass object
ref = MyPhantomReference@4554617c
Collecting garbage!
Cleaning up a phantom reference!
Removing an object from memory!
เราเห็นอะไรที่นี่? ทุกอย่างเป็นไปตามที่เราวางแผนไว้! เมธอด Finalize()ของออบเจกต์ของเราถูกแทนที่และถูกเรียกในขณะที่ตัวรวบรวมขยะกำลังทำงาน ถัดไป การอ้างอิงหลอนถูกใส่ลงในReferenceQueue ในขณะนั้น มีการเรียกใช้เมธอด clear() (ซึ่งเราเรียกว่าcleanup()เพื่อส่งออกไปยังคอนโซล) ในที่สุดวัตถุก็ถูกลบออกจากหน่วยความจำ ตอนนี้คุณคงเห็นแล้วว่ามันทำงานอย่างไร :) แน่นอน คุณไม่จำเป็นต้องจำทฤษฎีทั้งหมดเกี่ยวกับการอ้างอิงภาพหลอน แต่จะเป็นการดีถ้าคุณจำประเด็นหลักได้อย่างน้อย ประการแรก สิ่งเหล่านี้เป็นข้อมูลอ้างอิงที่อ่อนแอที่สุดของทั้งหมด พวกเขาเข้ามาเล่นก็ต่อเมื่อไม่มีการอ้างอิงถึงวัตถุอื่น ๆ รายการอ้างอิงที่เราให้ไว้ด้านบนนั้นเรียงลำดับจากมากไปหาน้อยจากมากไปน้อย: StrongReference -> SoftReference -> WeakReference -> PhantomReference การอ้างอิง Phantom เข้าสู่การต่อสู้ก็ต่อเมื่อไม่มีการอ้างอิงที่แข็งแกร่ง นุ่มนวล หรืออ่อนแอต่อวัตถุของเรา: ) ประการที่สอง เมธอด get()คืนค่าnull เสมอ สำหรับการอ้างอิงผี นี่คือตัวอย่างง่ายๆ ที่เราสร้างข้อมูลอ้างอิงสามประเภทสำหรับรถยนต์สามประเภท:
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
public class Main {
public static void main(String[] args) {
Sedan sedan = new Sedan();
HybridAuto hybrid = new HybridAuto();
F1Car f1car = new F1Car();
SoftReference<Sedan> softReference = new SoftReference<>(sedan);
System.out.println(softReference.get());
WeakReference<HybridAuto> weakReference = new WeakReference<>(hybrid);
System.out.println(weakReference.get());
ReferenceQueue<F1Car> referenceQueue = new ReferenceQueue<>();
PhantomReference<F1Car> phantomReference = new PhantomReference<>(f1car, referenceQueue);
System.out.println(phantomReference.get());
}
}
เอาต์พุตคอนโซล:
Sedan@4554617c
HybridAuto@74a14482
null
เมธอดget()ส่งคืนออบเจกต์ธรรมดาทั้งหมดสำหรับการอ้างอิงแบบซอฟต์และอ่อนแอ แต่มันกลับเป็นค่าว่างสำหรับการอ้างอิงแบบหลอน ประการที่สาม การอ้างอิงแบบ Phantom ส่วนใหญ่จะใช้ในขั้นตอนที่ซับซ้อนสำหรับการลบออบเจกต์ออกจากหน่วยความจำ แค่นั้นแหละ! :) นั่นคือการสรุปบทเรียนของเราในวันนี้ แต่คุณไม่สามารถไปได้ไกลจากทฤษฎีเพียงอย่างเดียว ดังนั้นได้เวลากลับไปแก้ไขงานแล้ว! :)