แม้ว่าเรามักจะทำงานกับแอปพลิเคชัน Java จำนวนมาก แต่เราไม่รู้เลยเกี่ยวกับการจัดการที่ JVM ทำด้วยตัวเองเพื่อทำให้สิ่งต่าง ๆ เป็นเรื่องง่ายสำหรับเรา เพื่อให้เราสามารถมุ่งเน้นไปที่สิ่งที่เราทำได้ดีที่สุด พัฒนาแอปพลิเคชันที่ยอดเยี่ยม
ต่างจาก C, C++ ใน Java ที่เรามี Garbage Collector เพื่อช่วยเราไม่ให้ทำความสะอาดโต๊ะหลังจากที่เราทานอาหารเสร็จ ในส่วนนี้จะอธิบายโดยย่อเกี่ยวกับการจัดการหน่วยความจำใน Java
การจัดการหน่วยความจำมีสองส่วน
- หน่วยความจำถูกจัดสรรและอ้างอิงอย่างไร
- หน่วยความจำถูกล้างและพร้อมสำหรับการจัดสรรอีกครั้งอย่างไร
มาดูอันแรกกัน..
โดยรวมแล้ว เราสามารถพูดได้ว่าหน่วยความจำแบ่งออกเป็นสามส่วน ได้แก่ Stack, Heap และ non-Heap มาดูกันตามลำดับ
ซ้อนกัน
- ตัวแปรบนสแต็กเก็บการอ้างอิงถึงออบเจ็กต์บนฮีปและดั้งเดิม
- มีหนึ่งสแต็กเดียวต่อเธรด
- มีขอบเขตสำหรับตัวแปร
- ขอบเขตวิธีการถูกผลักและเปิดขึ้นมา
- สามารถกำหนดขนาดได้ผ่านการกำหนดค่าเมื่อเริ่มต้น
กอง
- หนึ่งรายการต่อกระบวนการ JVM — แชร์ระหว่างเธรด
- สามารถเก็บการอ้างอิงไปยังวัตถุอื่น ๆ ในฮีปได้
- แบ่งออกเป็นส่วนๆ เพื่อง่ายต่อการล้าง และจัดสรรใหม่ (เราจะดูภายหลัง)
- สามารถกำหนดขนาดได้ผ่านการกำหนดค่าเมื่อเริ่มต้น
ไม่ใช่ฮีป
- หน่วยความจำที่จำเป็นสำหรับการประมวลผลภายในของ JVM
- พูลคงที่, ตัวแปรคงที่
- ข้อมูลภาคสนามและวิธีการ
- รหัสสำหรับวิธีการและตัวสร้าง
- คลาสโหลดเดอร์
- PermGen (ฮีป/ไม่ใช่ฮีป) กับ Metaspace (Java 8)
- อาจหรืออาจไม่ได้เป็นส่วนหนึ่งของ Heap - การใช้งานจะแตกต่างกันไป
ประเภทการอ้างอิง
ตอนนี้คุณอาจสงสัยว่าเหตุใดจึงมีเส้นประจากสแต็กหนึ่งไปอีกฮีป มาดูกันว่าตัวแปรใน Stack/Heap อ้างถึงอ็อบเจ็กต์บน Heap อย่างไร สิ่งเหล่านี้เรียกว่าการอ้างอิงและมีสี่รายการ
- การอ้างอิงที่รัดกุม — การอ้างอิงปกติที่เราใช้ใน Java
A a = new A();
- การอ้างอิงที่อ่อนแอ — การอ้างอิงถึงออบเจ็กต์ที่เสี่ยงต่อการรวบรวมขยะครั้งต่อไป (หากไม่ได้อ้างอิงอย่างเข้มงวด)
contextWeakReference = new WeakReference<Context>(context); //Later on..... Context context = contextWeakReference.get(); if(context != null){ //Context is not Gc'ed yet }else{ //Context is Gc'ed already }
- Soft Reference — สิ่งนี้คล้ายกับ WeakReference เพียงแต่ JVM จะ GC สิ่งนี้เฉพาะเมื่อมีหน่วยความจำขัดข้องเท่านั้น และแน่ใจอย่างแน่นอนว่าการอ้างอิงแบบซอฟต์ทั้งหมดนั้นเป็นขยะที่รวบรวมก่อนที่ JVM จะส่ง OOM ใด ๆ
contextSoftReference = new SoftReference<Context>(context); //Later on..... Context context = contextSoftReference.get(); if(context != null){ //Context is not Gc'ed yet }else{ //Context is Gc'ed already }
- การอ้างอิงแบบหลอก — ใช้สำหรับการล้างข้อมูลก่อนการชันสูตรพลิกศพ และแนะนำให้ใช้มากกว่า
finalize()
การสรุปผลเป็นสิ่งที่คาดเดาไม่ได้ ไม่สามารถกำหนดได้ และทำให้แอปพลิเคชันช้าลง ในตัวอย่างต่อไปนี้ เราต้องสำรวจ ReferenceQueue สำหรับการอ้างอิงอย่างต่อเนื่อง จากนั้นทำการล้างข้อมูล
public class PhantomReferenceTest { public static void main(String… args){ ReferenceQueue rq = new ReferenceQueue(); A a = new A(); a.s ="hello"; Reference r = new PhantomReference(a,rq); a =null; System.gc(); Reference ref = (Reference) rq.poll(); while(ref != null){ System.out.println(ref.get()); } } } class A{ String s; }
โครงสร้างหน่วยความจำฮีป
ฮีปถูกแบ่งออกเป็นส่วนต่างๆ ซึ่งช่วยให้สามารถจัดสรรและล้างข้อมูลได้ง่าย เอเดน พื้นที่คือสถานที่ที่มีการจัดสรรวัตถุใหม่ๆ เมื่ออีเดนกำลังจะถึงขีดจำกัด จะมี GC รอง ซึ่งจะเคลียร์วัตถุที่มีอยู่สำหรับ GC จากพื้นที่ Eden ไปยังหนึ่งใน พื้นที่ผู้รอดชีวิต(S0 และ S1) นอกจากนี้ วัตถุที่มีให้สำหรับ GC ในช่องผู้รอดชีวิตช่องใดช่องหนึ่งจะถูกเคลียร์ และวัตถุที่เหลือจะย้ายไปยังช่องผู้รอดชีวิตช่องอื่น ดังนั้นในแต่ละครั้งจะมีพื้นที่ว่างของผู้รอดชีวิตหนึ่งแห่งเสมอ หลังจาก n (ขึ้นอยู่กับการใช้งาน JVM) ออบเจ็กต์การวนซ้ำดังกล่าวซึ่งยังคงอยู่จะถูกย้ายไปยัง หน่วยความจำเก่า เมื่อใดก็ตามที่หน่วยความจำเก่าจวนจะเป็น Major GC เต็มรูปแบบเกิดขึ้น และทรัพยากรที่ไม่ได้ใช้ของหน่วยความจำเก่าจะถูกล้างออกไป
การเก็บขยะเกิดขึ้นได้อย่างไร
เป็นงาน หยุดโลก เธรดแอปพลิเคชันถูกหยุดชั่วคราวในขณะที่ GC อยู่ระหว่างดำเนินการ การดำเนินการ System.gc();
เราทำได้แค่ขอสำหรับ GC เท่านั้น JVM ตัดสินใจภายในเมื่อจะดำเนินการ GC มี GC หลายประเภทซึ่งสามารถกำหนดค่าได้ในระหว่างการเริ่มต้น JVM โดยการให้อาร์กิวเมนต์
ประเภทของการเก็บขยะ
- อัลกอริธึมการทำเครื่องหมายและการกวาดใช้สำหรับ GC ส่วนใหญ่ - ระยะการทำเครื่องหมาย และ ระยะการกวาด
- Serial GC— ทำเครื่องหมายและกวาดสำหรับ Minor และ Major GC เกลียวเดี่ยว
- GC แบบขนาน — เธรด N ถูกสร้างขึ้นสำหรับ GC รอง
- Parallel Old GC— หลายเธรดสำหรับทั้ง Minor และ Major GC
- กวาดล้างเครื่องหมายพร้อมกัน (Parallel New GC) — คล้ายกับ GC แบบขนาน ลดการหยุดชั่วคราวของแอปพลิเคชัน ใช้งานได้ส่วนใหญ่กับเธรดของแอปพลิเคชัน
- G1 GC — ใช้สำหรับฮีปขนาดใหญ่ แยกฮีปออกเป็นขอบเขตและรวบรวมภายในฮีปแบบขนาน
เราสามารถจัดเตรียมอาร์กิวเมนต์ที่จุดเริ่มต้นของ JVM เพื่อระบุ GC ที่จะใช้ ตัวอย่างบางส่วนได้แก่:
ประเด็นที่สำคัญ
- จำกัดขอบเขตของตัวแปร
- เริ่มต้นเมื่อคุณต้องการจริงๆ
- หลีกเลี่ยงการเริ่มต้นแบบวนซ้ำ — ใช้เวลา GC
- อ้างอิงถึง null อย่างชัดเจน
- หลีกเลี่ยงการเข้ารอบสุดท้าย ต้องการการอ้างอิง Phantom สำหรับการล้างข้อมูล
- กำหนดค่า JVM ตามความต้องการโดยการระบุอาร์กิวเมนต์
ไชโย !!!