สวัสดี! ในบทเรียนวันนี้ เราจะพูดถึงรายละเอียดเกี่ยวกับ PhantomReferences ใน Java ลิงก์เหล่านี้คืออะไร ทำไมจึงเรียกว่า "phantom" และใช้งานอย่างไร อย่างที่คุณจำได้ มีลิงก์ 4 ประเภทใน Java:
-
StrongReference (การอ้างอิงปกติที่เราสร้างเมื่อสร้างวัตถุ):
Cat cat = new Cat()
catในตัวอย่างนี้เป็นการอ้างอิงที่รัดกุม
-
SoftReference (การอ้างอิงแบบอ่อน) เรามีการบรรยายเกี่ยวกับลิงค์เหล่านี้
-
WeakReference (การอ้างอิงที่อ่อนแอ) มีการบรรยายเกี่ยวกับพวกเขาที่นี่ ด้วย
-
PhantomReference (การอ้างอิง Phantom)
SoftReference
และWeakReference
สืบทอดPhantomReference
จากReference
คลาส วิธีการที่สำคัญที่สุดเมื่อทำงานกับคลาสเหล่านี้คือ:
-
get()
- ส่งคืนอ็อบเจ็กต์ที่ลิงก์นี้อ้างถึง; clear()
— ลบการอ้างอิงไปยังวัตถุ
SoftReference
และWeakReference
. สิ่งสำคัญคือต้องจำไว้ว่าลิงก์เหล่านี้ทำงานแตกต่างกันกับลิงก์ประเภทต่างๆ วันนี้เราจะไม่พิจารณารายละเอียดสามประเภทแรก แต่จะพูดถึงลิงก์ Phantom เราจะพูดถึงลิงก์ประเภทอื่น ๆ ด้วย แต่เฉพาะในส่วนที่เราจะพูดถึงว่าลิงก์ Phantom แตกต่างจากลิงก์เหล่านั้นอย่างไร ไป! :) มาเริ่มกันที่ว่าทำไมเราถึงต้องการลิงค์ Phantom ตั้งแต่แรก ดังที่คุณทราบ ตัวรวบรวมขยะ (Garbage Collector หรือ gc) มีหน้าที่รับผิดชอบในการเพิ่มหน่วยความจำจากอ็อบเจ็กต์ Java ที่ไม่จำเป็น นักสะสมจะลบวัตถุออกเป็นสอง "ผ่าน" ในการผ่านครั้งแรก ระบบจะดูเฉพาะวัตถุ และหากจำเป็น จะทำเครื่องหมายว่า “ไม่จำเป็นและจะถูกลบ” หากวัตถุนี้มีเมธอดที่ถูกแทนที่finalize()
ก็จะถูกเรียก หรือไม่เรียกว่า - ขึ้นอยู่กับโชคของคุณ คุณคงจำได้ว่านี่finalize()
เป็นสิ่งที่ไม่แน่นอน :) ในการผ่านครั้งที่สองของตัวสะสม วัตถุจะถูกลบและหน่วยความจำจะถูกปลดปล่อย พฤติกรรมที่คาดเดาไม่ได้ของคนเก็บขยะนี้สร้างปัญหามากมายให้เรา เราไม่รู้แน่ชัดว่าคนเก็บขยะจะเริ่มทำงานเมื่อใด เราไม่รู้ว่าจะเรียก method นี้หรือfinalize()
เปล่า นอกจากนี้ ในระหว่างการดำเนินการfinalize()
คุณสามารถสร้างลิงก์ที่รัดกุมไปยังออบเจ็กต์ได้ จากนั้นจะไม่ถูกลบเลย บนระบบที่ต้องใช้หน่วยความจำมาก สิ่งนี้สามารถนำไปสู่OutOfMemoryError
. ทั้งหมดนี้ผลักดันให้เราใช้ลิงก์Phantom ประเด็นก็คือสิ่งนี้จะเปลี่ยนพฤติกรรมของคนเก็บขยะ หากมีลิงก์หลอกเหลืออยู่ที่ออบเจ็กต์ มันก็จะมี:
-
เรียกเมธอดนี้
finalize()
(หากถูกแทนที่) -
หากหลังเลิกงาน
finalize()
ไม่มีอะไรเปลี่ยนแปลงและยังสามารถลบอ็อบเจ็กต์ได้ การอ้างอิง phantom ไปยังอ็อบเจ็กต์จะถูกวางไว้ในคิวพิเศษReferenceQueue
-
clear()
เท่านั้น ลองดูตัวอย่าง ขั้นแรก เรามาสร้างคลาสทดสอบที่จะเก็บข้อมูลบางส่วนกันก่อน
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("У an object TestClass вызван метод finalize!!!");
}
}
เราจงใจ "โหลด" วัตถุด้วยข้อมูลเมื่อสร้างวัตถุเหล่านั้น (เพิ่มอักขระ "x" 50 ล้านตัวให้กับแต่ละวัตถุ) เพื่อใช้หน่วยความจำมากขึ้น นอกจากนี้ เรายังแทนที่วิธีการนี้โดยเฉพาะfinalize()
เพื่อดูว่าได้ผลหรือไม่ ต่อไปเราต้องการคลาสที่จะสืบทอดจากPhantomReference
. ทำไมเราถึงต้องการชั้นเรียนเช่นนี้? มันง่ายมาก วิธีนี้ทำให้เราสามารถเพิ่มตรรกะเพิ่มเติมให้กับวิธีการclear()
เพื่อดูว่าการอ้างอิง Phantom ได้รับการล้างแล้วจริง ๆ (และดังนั้นวัตถุจึงถูกลบไปแล้ว)
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("Очистка фантомной ссылки! Удаление an object из памяти!");
clear();
}
}
ต่อไป เราจะต้องมีเธรดแยกต่างหากที่จะรอให้ตัวรวบรวมขยะทำสิ่งนั้น และReferenceQueue
ลิงก์แฝงจะปรากฏในคิวของเรา ทันทีที่ลิงก์ดังกล่าวเข้าสู่คิว วิธีการของมันจะถูกเรียกcleanup()
:
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("Поток, отслеживающий очередь, стартовал!");
Reference ref = null;
//ждем, пока в очереди появятся ссылки
while ((ref = referenceQueue.poll()) == null) {
try {
Thread.sleep(50);
}
catch (InterruptedException e) {
throw new RuntimeException("Поток " + getName() + " был прерван!");
}
}
//How только в очереди появилась фантомная link - очистить ее
((MyPhantomReference) ref).cleanup();
}
}
และสุดท้าย เราต้องการ method main()
: แยกมันไปไว้ในคลาสที่แยกจากMain
กัน ในนั้นเราจะสร้าง object TestClass
การอ้างอิง phantom และคิวสำหรับการอ้างอิง phantom หลังจากนั้นเราจะโทรหาคนเก็บขยะดูว่าจะเกิดอะไรขึ้น :)
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("Вызывается сборка мусора!");
System.gc();
Thread.sleep(300);
System.out.println("ref = " + ref);
Thread.sleep(5000);
System.out.println("Вызывается сборка мусора!");
System.gc();
}
}
เอาต์พุตคอนโซล: ref = MyPhantomReference@4554617c การตรวจสอบเธรดคิวได้เริ่มต้นแล้ว! เรียกว่าเก็บขยะ! มีการเรียกใช้วิธีการสรุปบนวัตถุ TestClass !!! ref = MyPhantomReference@4554617c กำลังเรียกการรวบรวมขยะ! ทำความสะอาดลิงค์แฟนธอม! การลบวัตถุออกจากหน่วยความจำ! เราเห็นอะไรที่นี่? ทุกอย่างเกิดขึ้นตามที่เราวางแผนไว้! คลาสอ็อบเจ็กต์ของเรามีเมธอดที่ถูกแทนที่finalize()
และมันถูกเรียกในขณะที่ตัวรวบรวมกำลังทำงานอยู่ ถัดไป ลิงก์ Phantom ถูกจัดReferenceQueue
คิว ที่นั่นเธอมีวิธีการที่เรียกว่าclear()
(ซึ่งเราทำcleanup()
เพื่อเพิ่มเอาต์พุตไปยังคอนโซล) เป็นผลให้วัตถุถูกลบออกจากหน่วยความจำ ตอนนี้คุณคงได้เห็นแล้วว่ามันทำงานอย่างไร :) แน่นอนว่าคุณไม่จำเป็นต้องจำทฤษฎีทั้งหมดที่เกี่ยวข้องกับลิงค์ Phantom แต่คงจะดีถ้าคุณจำประเด็นหลักได้อย่างน้อยที่สุด ประการแรกสิ่งเหล่านี้คือลิงค์ที่อ่อนแอที่สุด สิ่งเหล่านี้จะเข้ามามีบทบาทก็ต่อเมื่อไม่มีการอ้างอิงอื่นถึงวัตถุนั้น รายการลิงก์ที่เราให้ไว้ข้างต้นนั้นเรียงลำดับตามความแข็งแกร่งจากมากไปน้อย: StrongReference
-> SoftReference
-> WeakReference
-> PhantomReference
ลิงก์ Phantom จะเข้าสู่การต่อสู้เฉพาะเมื่อไม่มีลิงก์ Strong, Soft หรือ Weak ไปยังวัตถุของเรา :) ประการ ที่สองวิธีการget()
สำหรับลิงค์ phantom จะส่งกลับ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()
การส่งคืนออบเจ็กต์ที่ค่อนข้างปกติสำหรับซอฟต์ลิงก์และลิงก์ที่อ่อนแอ แต่ส่งคืนnull
สำหรับ phantom หนึ่ง ประการที่สามพื้นที่หลักของการใช้การอ้างอิง phantom คือขั้นตอนที่ซับซ้อนในการลบวัตถุออกจากหน่วยความจำ นั่นคือทั้งหมด! :) นี่เป็นการสรุปบทเรียนของเราสำหรับวันนี้ แต่ทฤษฎีเพียงอย่างเดียวไม่สามารถพาคุณไปได้ไกล ดังนั้นถึงเวลากลับไปสู่การแก้ปัญหา! :)
GO TO FULL VERSION