แอปพลิเคชัน Java ทำงานบน JVM แต่คุณรู้เกี่ยวกับเทคโนโลยี JVM หรือไม่ บทความนี้ (ส่วนแรกของชุดนี้) จะบอกวิธีการทำงานของ Java Virtual Machine แบบคลาสสิก เช่น ข้อดีและข้อเสียของ Java เขียนครั้งเดียว กลไกข้ามแพลตฟอร์ม พื้นฐานการรวบรวมขยะ อัลกอริธึม GC แบบคลาสสิก และการเพิ่มประสิทธิภาพการคอมไพล์ บทความต่อๆ ไปจะพูดถึงการเพิ่มประสิทธิภาพการทำงานของ JVM รวมถึงการออกแบบ JVM ล่าสุด ซึ่งสนับสนุนประสิทธิภาพและความสามารถในการปรับขนาดของแอปพลิเคชัน Java ที่ทำงานพร้อมกันสูงในปัจจุบัน
หากคุณเป็นนักพัฒนา คุณจะต้องพบกับความรู้สึกพิเศษนี้ คุณมีแรงบันดาลใจขึ้นมาทันที ไอเดียทั้งหมดของคุณเชื่อมโยงกัน และคุณสามารถนึกถึงไอเดียก่อนหน้านี้ได้จากมุมมองใหม่ โดยส่วนตัวแล้วฉันชอบความรู้สึกของการเรียนรู้ความรู้ใหม่ๆ ฉันมีประสบการณ์นี้หลายครั้งในขณะที่ทำงานกับเทคโนโลยี JVM โดยเฉพาะอย่างยิ่งกับการรวบรวมขยะและการเพิ่มประสิทธิภาพการทำงานของ JVM ในโลกใหม่ของ Java ฉันหวังว่าจะแบ่งปันแรงบันดาลใจเหล่านี้กับคุณ ฉันหวังว่าคุณจะตื่นเต้นที่ได้เรียนรู้เกี่ยวกับประสิทธิภาพของ JVM ในขณะที่ฉันกำลังเขียนบทความนี้
บทความชุดนี้เขียนขึ้นสำหรับนักพัฒนา Java ทุกคนที่สนใจเรียนรู้เพิ่มเติมเกี่ยวกับความรู้พื้นฐานของ JVM และสิ่งที่ JVM ทำจริง ๆ ในระดับสูง ฉันจะหารือเกี่ยวกับการรวบรวมขยะและการแสวงหาความปลอดภัยและความเร็วของหน่วยความจำฟรีอย่างไม่มีที่สิ้นสุด โดยไม่ส่งผลกระทบต่อการทำงานของแอปพลิเคชัน คุณจะได้เรียนรู้ส่วนสำคัญของ JVM: การรวบรวมขยะและอัลกอริธึม GC การเพิ่มประสิทธิภาพการคอมไพล์ และการเพิ่มประสิทธิภาพบางอย่างที่ใช้กันทั่วไป นอกจากนี้ ฉันจะพูดคุยด้วยว่าเหตุใดมาร์กอัป Java จึงเป็นเรื่องยาก และให้คำแนะนำว่าเมื่อใดที่คุณควรพิจารณาทดสอบประสิทธิภาพ สุดท้ายนี้ ฉันจะพูดถึงนวัตกรรมใหม่ๆ ใน JVM และ GC รวมถึง Zing JVM ของ Azul, IBM JVM และโฟกัสการรวบรวมขยะ Garbage First (G1) ของ Oracle
ฉันหวังว่าคุณจะอ่านซีรี่ส์นี้จบด้วยความเข้าใจที่ลึกซึ้งยิ่งขึ้นเกี่ยวกับธรรมชาติของข้อจำกัดด้านความสามารถในการปรับขนาดของ Java และข้อจำกัดเหล่านี้บังคับให้เราสร้างการปรับใช้ Java ในวิธีที่เหมาะสมที่สุดได้อย่างไร หวังว่าคุณจะมีความรู้สึกรู้แจ้งและมีแรงบันดาลใจดีๆ เกี่ยวกับ Java: หยุดยอมรับข้อจำกัดเหล่านั้นและเปลี่ยนแปลงมัน! หากคุณยังไม่ใช่คนทำงานแบบโอเพ่นซอร์ส ซีรี่ส์นี้อาจสนับสนุนให้คุณพัฒนาในด้านนี้
ประสิทธิภาพของ JVM และความท้าทาย “คอมไพล์ครั้งเดียว เรียกใช้ได้ทุกที่”
ฉันมีข่าวใหม่สำหรับผู้เชื่อที่หัวแข็งว่าแพลตฟอร์ม Java นั้นช้าโดยธรรมชาติ เมื่อ Java กลายเป็นแอปพลิเคชันระดับองค์กรเป็นครั้งแรก ปัญหาด้านประสิทธิภาพของ Java ที่ JVM ถูกวิพากษ์วิจารณ์นั้นมีมานานกว่าสิบปีแล้ว แต่ข้อสรุปนี้ล้าสมัยไปแล้ว เป็นเรื่องจริงที่หากคุณรันงานแบบสแตติกและแบบกำหนดง่ายๆ บนแพลตฟอร์มการพัฒนาต่างๆ ในปัจจุบัน คุณจะพบว่าการใช้โค้ดที่ปรับให้เหมาะสมกับเครื่องจักรจะทำงานได้ดีกว่าการใช้สภาพแวดล้อมเสมือนใดๆ ภายใต้ JVM เดียวกัน อย่างไรก็ตาม ประสิทธิภาพของ Java ได้รับการปรับปรุงอย่างมากในช่วง 10 ปีที่ผ่านมา ความต้องการของตลาดและการเติบโตของอุตสาหกรรม Java ส่งผลให้เกิดอัลกอริธึมการรวบรวมขยะจำนวนหนึ่ง นวัตกรรมการคอมไพล์ใหม่ๆ และโฮสต์ของการศึกษาพฤติกรรมและการเพิ่มประสิทธิภาพที่มีเทคโนโลยี JVM ขั้นสูง ฉันจะกล่าวถึงสิ่งเหล่านี้บางส่วนในบทต่อๆ ไป
ความงามทางเทคนิคของ JVM ยังเป็นความท้าทายที่ยิ่งใหญ่ที่สุด: ไม่มีสิ่งใดที่สามารถถือเป็นแอปพลิเคชัน "คอมไพล์ครั้งเดียว รันได้ทุกที่" แทนที่จะปรับให้เหมาะสมสำหรับกรณีการใช้งานเดียว แอปพลิเคชันเดียว หรือโหลดผู้ใช้เฉพาะรายเดียว JVM ติดตามอย่างต่อเนื่องว่าแอปพลิเคชัน Java กำลังทำอะไรอยู่ในปัจจุบันและปรับให้เหมาะสมตามนั้น การดำเนินการแบบไดนามิกนี้นำไปสู่ปัญหาแบบไดนามิกต่างๆ นักพัฒนาที่ทำงานบน JVM ไม่ต้องพึ่งพาการรวบรวมแบบคงที่และอัตราการจัดสรรที่คาดการณ์ได้เมื่อออกแบบนวัตกรรม (อย่างน้อยก็ไม่ใช่เมื่อเราต้องการประสิทธิภาพในสภาพแวดล้อมการผลิต)
สาเหตุของประสิทธิภาพของ JVM
ในงานแรกๆ ของฉัน ฉันตระหนักว่าการเก็บขยะนั้น "แก้ไข" ได้ยากมาก และฉันก็หลงใหลใน JVM และเทคโนโลยีมิดเดิลแวร์มาโดยตลอด ความหลงใหลใน JVM ของฉันเริ่มต้นเมื่อฉันอยู่ในทีม JRockit โดยเขียนโค้ดวิธีใหม่ในการสอนตัวเองและแก้ไขข้อบกพร่องของอัลกอริธึมการรวบรวมขยะด้วยตัวเอง (ดูแหล่งข้อมูล) โปรเจ็กต์นี้ (ซึ่งกลายเป็นฟีเจอร์ทดลองของ JRockit และกลายเป็นพื้นฐานสำหรับอัลกอริธึม Deterministic Garbage Collection) เริ่มต้นการเดินทางของฉันสู่เทคโนโลยี JVM ฉันเคยทำงานที่ BEA Systems, Intel, Sun และ Oracle (เนื่องจาก Oracle เข้าซื้อกิจการ BEA Systems ฉันจึงทำงานให้กับ Oracle ในช่วงสั้นๆ) จากนั้นฉันก็เข้าร่วมทีมที่ Azul Systems เพื่อจัดการ Zing JVM และตอนนี้ฉันทำงานให้กับ Cloudera
โค้ดที่ปรับให้เหมาะสมกับเครื่องจักรอาจได้รับประสิทธิภาพที่ดีขึ้น (แต่ต้องเสียความยืดหยุ่น) แต่นี่ไม่ใช่เหตุผลที่จะชั่งน้ำหนักสำหรับแอปพลิเคชันระดับองค์กรที่มีการโหลดแบบไดนามิกและฟังก์ชันการทำงานที่เปลี่ยนแปลงอย่างรวดเร็ว สำหรับข้อดีของ Java บริษัทส่วนใหญ่เต็มใจที่จะเสียสละประสิทธิภาพที่แทบจะไม่สมบูรณ์แบบซึ่งมาจากโค้ดที่ปรับให้เหมาะสมกับเครื่องจักร
1. ง่ายต่อการเขียนโค้ดและการพัฒนาฟังก์ชั่น (หมายถึงใช้เวลาในการตอบสนองต่อตลาดสั้นลง)
2.รับโปรแกรมเมอร์ผู้มีความรู้
3. ใช้ Java API และไลบรารีมาตรฐานเพื่อการพัฒนาที่เร็วขึ้น
4. การพกพา - ไม่จำเป็นต้องเขียนแอปพลิเคชัน Java ใหม่สำหรับแพลตฟอร์มใหม่
จากโค้ด Java ไปจนถึง bytecode
ในฐานะโปรแกรมเมอร์ Java คุณอาจคุ้นเคยกับการเขียนโค้ด การคอมไพล์ และการดำเนินการแอปพลิเคชัน Java ตัวอย่าง: สมมติว่าคุณมีโปรแกรม (MyApp.java) และตอนนี้คุณต้องการให้มันทำงาน ในการรันโปรแกรมนี้ คุณต้องคอมไพล์มันด้วย javac (ภาษา Java แบบคงที่ไปจนถึงคอมไพเลอร์ bytecode ที่สร้างไว้ใน JDK) ตามโค้ด Java นั้น javac จะสร้าง bytecode ที่ปฏิบัติการได้ที่เกี่ยวข้องและบันทึกไว้ในไฟล์คลาสที่มีชื่อเดียวกัน: MyApp.class หลังจากคอมไพล์โค้ด Java เป็น bytecode แล้ว คุณสามารถเริ่มไฟล์คลาสที่เรียกใช้งานได้ผ่านคำสั่ง java (ผ่านบรรทัดคำสั่งหรือสคริปต์เริ่มต้น โดยไม่ต้องใช้ตัวเลือกการเริ่มต้น) เพื่อรันแอปพลิเคชันของคุณ ด้วยวิธีนี้ คลาสของคุณจะถูกโหลดเข้าสู่รันไทม์ (หมายถึงการทำงานของเครื่องเสมือน Java) และโปรแกรมจะเริ่มดำเนินการ
นี่คือสิ่งที่ทุกแอปพลิเคชันดำเนินการบนพื้นผิว แต่ตอนนี้เรามาสำรวจว่าจะเกิดอะไรขึ้นเมื่อคุณรันคำสั่ง java เครื่องเสมือน Java คืออะไร? นักพัฒนาส่วนใหญ่โต้ตอบกับ JVM ผ่านการดีบักอย่างต่อเนื่อง หรือที่เรียกว่าการเลือกและกำหนดตัวเลือกการเริ่มต้นระบบเพื่อให้โปรแกรม Java ของคุณทำงานเร็วขึ้นในขณะที่หลีกเลี่ยงข้อผิดพลาด "หน่วยความจำไม่เพียงพอ" ที่น่าอับอาย แต่คุณเคยสงสัยบ้างไหมว่าทำไมเราถึงต้องใช้ JVM เพื่อรันแอปพลิเคชัน Java ตั้งแต่แรก?
เครื่องเสมือน Java คืออะไร?
พูดง่ายๆ ก็คือ JVM คือโมดูลซอฟต์แวร์ที่รันโค้ดไบต์ของแอปพลิเคชัน Java และแปลงโค้ดไบต์เป็นคำสั่งเฉพาะของฮาร์ดแวร์และระบบปฏิบัติการ ด้วยการทำเช่นนี้ JVM จะอนุญาตให้โปรแกรม Java สามารถดำเนินการในสภาพแวดล้อมอื่นหลังจากที่เขียนครั้งแรก โดยไม่ต้องเปลี่ยนแปลงโค้ดต้นฉบับ ความสามารถในการพกพาของ Java เป็นกุญแจสำคัญในภาษาแอปพลิเคชันระดับองค์กร นักพัฒนาไม่จำเป็นต้องเขียนโค้ดแอปพลิเคชันใหม่สำหรับแพลตฟอร์มที่แตกต่างกัน เนื่องจาก JVM จะดูแลการแปลและการเพิ่มประสิทธิภาพแพลตฟอร์ม
โดยพื้นฐานแล้ว JVM นั้นเป็นสภาพแวดล้อมการดำเนินการเสมือนที่ทำหน้าที่เป็นเครื่องคำสั่งไบต์โค้ด และใช้เพื่อจัดสรรงานการดำเนินการและดำเนินการกับหน่วยความจำโดยการโต้ตอบกับเลเยอร์พื้นฐาน
JVM ยังดูแลการจัดการทรัพยากรแบบไดนามิกสำหรับการรันแอปพลิเคชัน Java ซึ่งหมายความว่าสามารถจัดสรรและเพิ่มหน่วยความจำให้ว่างได้อย่างเชี่ยวชาญ รักษาโมเดลเธรดที่สอดคล้องกันในแต่ละแพลตฟอร์ม และจัดระเบียบคำสั่งปฏิบัติการที่แอปพลิเคชันถูกดำเนินการในลักษณะที่เหมาะสมสำหรับสถาปัตยกรรม CPU JVM ช่วยให้นักพัฒนาไม่ต้องติดตามการอ้างอิงถึงอ็อบเจ็กต์และระยะเวลาที่พวกเขาต้องการอยู่ในระบบ ในทำนองเดียวกัน เราก็ไม่ต้องการให้เราจัดการเมื่อจะปล่อยหน่วยความจำ - จุดปวดในภาษาที่ไม่ไดนามิกเช่น C
คุณสามารถนึกถึง JVM ว่าเป็นระบบปฏิบัติการที่ออกแบบมาเพื่อรัน Java โดยเฉพาะ หน้าที่ของมันคือการจัดการสภาพแวดล้อมการทำงานสำหรับแอปพลิเคชัน Java โดยพื้นฐานแล้ว JVM นั้นเป็นสภาพแวดล้อมการดำเนินการเสมือนที่โต้ตอบกับสภาพแวดล้อมพื้นฐานในฐานะเครื่องคำสั่งไบต์โค้ดสำหรับการจัดสรรงานการดำเนินการและการดำเนินการของหน่วยความจำ
ภาพรวมส่วนประกอบ JVM
มีบทความมากมายที่เขียนเกี่ยวกับ JVM ภายในและการเพิ่มประสิทธิภาพการทำงาน เพื่อเป็นพื้นฐานของซีรี่ส์นี้ ฉันจะสรุปและภาพรวมส่วนประกอบ JVM ภาพรวมโดยย่อนี้มีประโยชน์อย่างยิ่งสำหรับนักพัฒนาที่เพิ่งเริ่มใช้ JVM และจะทำให้คุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับการสนทนาเชิงลึกที่ตามมา
จากภาษาหนึ่งไปอีกภาษาหนึ่ง - เกี่ยวกับจาวาคอมไพเลอร์
คอมไพเลอร์ใช้ภาษาหนึ่งเป็นอินพุตแล้วส่งออกคำสั่งปฏิบัติการอื่น คอมไพเลอร์ Java มีสองงานหลัก:
1. ทำให้ภาษา Java พกพาสะดวกยิ่งขึ้น และไม่จำเป็นต้องแก้ไขบนแพลตฟอร์มเฉพาะอีกต่อไปเมื่อเขียนเป็นครั้งแรก
2. ตรวจสอบให้แน่ใจว่ามีการสร้างรหัสปฏิบัติการที่ถูกต้องสำหรับแพลตฟอร์มเฉพาะ
คอมไพเลอร์อาจเป็นแบบคงที่หรือไดนามิก ตัวอย่างของการรวบรวมแบบคงที่คือ javac ใช้โค้ด Java เป็นอินพุตและแปลงเป็น bytecode (ภาษาที่ดำเนินการในเครื่องเสมือน Java) คอมไพเลอร์แบบสแตติกตีความโค้ดอินพุตหนึ่งครั้งและส่งออกรูปแบบที่ปฏิบัติการได้ ซึ่งจะถูกใช้เมื่อมีการรันโปรแกรม เนื่องจากอินพุตเป็นแบบคงที่ คุณจะเห็นผลลัพธ์เดียวกันเสมอ เฉพาะในกรณีที่คุณแก้ไขโค้ดต้นฉบับและคอมไพล์ใหม่ คุณจะเห็นผลลัพธ์ที่แตกต่างออกไป
คอมไพเลอร์แบบไดนามิก เช่น คอมไพเลอร์ Just-In-Time (JIT) จะแปลงภาษาหนึ่งเป็นอีกภาษาหนึ่งแบบไดนามิก ซึ่งหมายความว่าพวกเขาจะทำเช่นนี้ในขณะที่โค้ดกำลังถูกดำเนินการ คอมไพลเลอร์ JIT ช่วยให้คุณรวบรวมหรือสร้างการวิเคราะห์รันไทม์ (โดยการแทรกจำนวนประสิทธิภาพ) โดยใช้การตัดสินใจของคอมไพเลอร์ โดยใช้ข้อมูลสภาพแวดล้อมที่มีอยู่ คอมไพเลอร์แบบไดนามิกสามารถใช้ลำดับคำสั่งที่ดีกว่าในระหว่างกระบวนการคอมไพล์เป็นภาษา แทนที่ชุดคำสั่งด้วยชุดคำสั่งที่มีประสิทธิภาพมากกว่า และแม้กระทั่งกำจัดการดำเนินการที่ซ้ำซ้อน เมื่อเวลาผ่านไป คุณจะรวบรวมข้อมูลการกำหนดค่าโค้ดมากขึ้นและทำการตัดสินใจในการคอมไพล์มากขึ้นเรื่อยๆ กระบวนการทั้งหมดคือสิ่งที่เรามักเรียกว่าการปรับให้เหมาะสมและการคอมไพล์โค้ดใหม่
การคอมไพล์แบบไดนามิกช่วยให้คุณได้เปรียบในการปรับให้เข้ากับการเปลี่ยนแปลงแบบไดนามิกตามลักษณะการทำงาน หรือการเพิ่มประสิทธิภาพใหม่เมื่อจำนวนแอปพลิเคชันเพิ่มขึ้น นี่คือเหตุผลว่าทำไมคอมไพเลอร์แบบไดนามิกจึงสมบูรณ์แบบสำหรับการทำงานของ Java เป็นที่น่าสังเกตว่าคอมไพเลอร์แบบไดนามิกร้องขอโครงสร้างข้อมูลภายนอก ทรัพยากรเธรด การวิเคราะห์วงจร CPU และการเพิ่มประสิทธิภาพ ยิ่งการเพิ่มประสิทธิภาพมีความลึกเท่าไร คุณก็ยิ่งต้องการทรัพยากรมากขึ้นเท่านั้น อย่างไรก็ตาม ในสภาพแวดล้อมส่วนใหญ่ ชั้นบนสุดเพิ่มประสิทธิภาพการทำงานเพียงเล็กน้อย - ประสิทธิภาพเร็วกว่าการตีความที่แท้จริงของคุณถึง 5 ถึง 10 เท่า
การจัดสรรทำให้เกิดการรวบรวมขยะ
จัดสรรในแต่ละเธรดตามแต่ละ "กระบวนการ Java ที่จัดสรรพื้นที่ที่อยู่หน่วยความจำ" หรือเรียกว่าฮีป Java หรือเรียกว่าฮีปโดยตรง ในโลกของ Java การจัดสรรแบบเธรดเดียวเป็นเรื่องปกติในแอปพลิเคชันไคลเอ็นต์ อย่างไรก็ตาม การจัดสรรแบบเธรดเดียวไม่เป็นประโยชน์ในแอปพลิเคชันระดับองค์กรและเซิร์ฟเวอร์เวิร์กโหลด เนื่องจากไม่ได้ใช้ประโยชน์จากการทำงานแบบขนานของสภาพแวดล้อมแบบมัลติคอร์ในปัจจุบัน
การออกแบบแอปพลิเคชันแบบขนานยังบังคับให้ JVM ตรวจสอบให้แน่ใจว่าหลายเธรดไม่ได้จัดสรรพื้นที่ที่อยู่เดียวกันในเวลาเดียวกัน คุณสามารถควบคุมสิ่งนี้ได้โดยวางล็อคบนพื้นที่ที่จัดสรรทั้งหมด แต่เทคนิคนี้ (มักเรียกว่าการล็อกฮีป) ต้องใช้ประสิทธิภาพอย่างมาก และการพักหรือการจัดคิวเธรดอาจส่งผลต่อการใช้ทรัพยากรและประสิทธิภาพการปรับให้เหมาะสมของแอปพลิเคชัน ข้อดีของระบบมัลติคอร์คือระบบเหล่านี้สร้างความต้องการวิธีการใหม่ๆ ที่หลากหลายเพื่อป้องกันปัญหาคอขวดแบบเธรดเดี่ยวในขณะที่จัดสรรทรัพยากรและซีเรียลไลซ์
วิธีการทั่วไปคือการแบ่งฮีปออกเป็นส่วนต่างๆ โดยแต่ละพาร์ติชันมีขนาดที่เหมาะสมสำหรับแอปพลิเคชัน - แน่นอนว่าจำเป็นต้องได้รับการปรับแต่ง อัตราการจัดสรรและขนาดอ็อบเจ็กต์จะแตกต่างกันอย่างมากระหว่างแอปพลิเคชัน และจำนวนเธรดสำหรับแอปพลิเคชันเดียวกันก็แตกต่างกันด้วย Thread Local Allocation Buffer (TLAB) หรือบางครั้งคือ Thread Local Area (TLA) เป็นพาร์ติชันพิเศษที่เธรดสามารถจัดสรรได้อย่างอิสระโดยไม่ต้องประกาศล็อกฮีปแบบเต็ม เมื่อพื้นที่เต็ม ฮีปจะเต็ม ซึ่งหมายความว่ามีพื้นที่ว่างบนฮีปไม่เพียงพอสำหรับวางออบเจ็กต์ และจำเป็นต้องจัดสรรพื้นที่ เมื่อฮีปเต็ม ก็เริ่มเก็บขยะ
เศษ
การใช้ TLAB เพื่อตรวจจับข้อยกเว้นในฮีปเพื่อลดประสิทธิภาพของหน่วยความจำ หากแอปพลิเคชันไม่สามารถเพิ่มหรือจัดสรรพื้นที่ TLAB ได้ทั้งหมดเมื่อทำการจัดสรรออบเจ็กต์ มีความเสี่ยงที่พื้นที่จะเล็กเกินไปที่จะสร้างออบเจ็กต์ใหม่ พื้นที่ว่างดังกล่าวถือเป็น "การกระจายตัว" หากแอปพลิเคชันเก็บการอ้างอิงไปยังวัตถุแล้วจัดสรรพื้นที่ที่เหลืออยู่ ในที่สุดพื้นที่จะว่างเป็นเวลานาน
การกระจายตัวคือเมื่อแฟรกเมนต์กระจัดกระจายไปทั่วฮีป - สิ้นเปลืองพื้นที่ฮีปผ่านส่วนเล็ก ๆ ของพื้นที่หน่วยความจำที่ไม่ได้ใช้ การจัดสรรพื้นที่ TLAB "ผิด" สำหรับแอปพลิเคชันของคุณ (เกี่ยวกับขนาดออบเจ็กต์ ขนาดออบเจ็กต์ผสม และอัตราส่วนการถือครองอ้างอิง) เป็นสาเหตุของการกระจายตัวของฮีปที่เพิ่มขึ้น ขณะที่แอปพลิเคชันทำงาน จำนวนแฟรกเมนต์จะเพิ่มขึ้นและใช้พื้นที่ในฮีป การกระจายตัวทำให้ประสิทธิภาพลดลง และระบบไม่สามารถจัดสรรเธรดและอ็อบเจ็กต์เพียงพอให้กับแอปพลิเคชันใหม่ได้ ตัวรวบรวมขยะจะมีปัญหาในการป้องกันข้อยกเว้นหน่วยความจำไม่เพียงพอ
ของเสีย TLAB ถูกสร้างขึ้นในงาน วิธีหนึ่งในการหลีกเลี่ยงการแตกแฟรกเมนต์โดยสิ้นเชิงหรือชั่วคราวคือการปรับพื้นที่ TLAB ให้เหมาะสมในทุกการดำเนินการพื้นฐาน แนวทางทั่วไปสำหรับแนวทางนี้คือ ตราบใดที่แอปพลิเคชันมีพฤติกรรมการจัดสรร ก็จำเป็นต้องปรับใหม่ ซึ่งสามารถทำได้ผ่านอัลกอริธึม JVM ที่ซับซ้อน อีกวิธีหนึ่งคือการจัดระเบียบพาร์ติชันฮีปเพื่อให้เกิดการจัดสรรหน่วยความจำที่มีประสิทธิภาพมากขึ้น ตัวอย่างเช่น JVM สามารถใช้รายการอิสระ ซึ่งเชื่อมโยงเข้าด้วยกันเป็นรายการบล็อกหน่วยความจำว่างในขนาดเฉพาะ บล็อกหน่วยความจำว่างที่ต่อเนื่องกันเชื่อมต่อกับบล็อกหน่วยความจำที่อยู่ติดกันอีกบล็อกที่มีขนาดเท่ากัน ดังนั้น จึงสร้างรายการเชื่อมโยงจำนวนเล็กน้อย โดยแต่ละรายการมีขอบเขตของตัวเอง ในบางกรณี รายการอิสระส่งผลให้มีการจัดสรรหน่วยความจำที่ดีขึ้น เธรดสามารถจัดสรรออบเจ็กต์เป็นบล็อกที่มีขนาดใกล้เคียงกัน ซึ่งอาจสร้างการกระจายตัวน้อยกว่าการใช้ TLAB ที่มีขนาดคงที่
GC เรื่องไม่สำคัญ
คนเก็บขยะในยุคแรกๆ บางคนมีรุ่นเก่าหลายรุ่น แต่การมีรุ่นเก่ามากกว่าสองรุ่นจะทำให้ค่าใช้จ่ายมีมากกว่ามูลค่า อีกวิธีหนึ่งในการปรับการจัดสรรให้เหมาะสมและลดการกระจายตัวคือการสร้างสิ่งที่เรียกว่าคนรุ่นใหม่ ซึ่งเป็นพื้นที่ฮีปเฉพาะสำหรับการจัดสรรออบเจ็กต์ใหม่โดยเฉพาะ ฮีปที่เหลือจะกลายเป็นสิ่งที่เรียกว่ารุ่นเก่า รุ่นเก่าใช้เพื่อจัดสรรวัตถุที่มีอายุยืนยาวซึ่งถือว่ามีอยู่เป็นเวลานาน ได้แก่ วัตถุที่ไม่ใช่การรวบรวมขยะหรือวัตถุขนาดใหญ่ เพื่อให้เข้าใจวิธีการจัดสรรนี้ได้ดีขึ้น เราต้องพูดถึงความรู้บางประการเกี่ยวกับการเก็บขยะ
การรวบรวมขยะและประสิทธิภาพการใช้งาน
การรวบรวมขยะคือตัวรวบรวมขยะของ JVM เพื่อปล่อยหน่วยความจำฮีปที่ถูกครอบครองซึ่งไม่ได้อ้างอิง เมื่อการรวบรวมขยะถูกกระตุ้นเป็นครั้งแรก การอ้างอิงอ็อบเจ็กต์ทั้งหมดจะยังคงอยู่ และพื้นที่ว่างที่ครอบครองโดยการอ้างอิงก่อนหน้านี้จะถูกปล่อยหรือจัดสรรใหม่ หลังจากรวบรวมหน่วยความจำที่เรียกคืนได้ทั้งหมดแล้ว พื้นที่จะรอเพื่อคว้าและจัดสรรให้กับวัตถุใหม่อีกครั้ง
ตัวรวบรวมขยะไม่สามารถประกาศออบเจ็กต์อ้างอิงอีกครั้งได้ การทำเช่นนี้จะทำให้ข้อกำหนดมาตรฐาน JVM เสียหาย ข้อยกเว้นสำหรับกฎนี้คือการอ้างอิงแบบ soft หรือแบบอ่อนที่สามารถตรวจจับได้หากตัวรวบรวมขยะกำลังจะมีหน่วยความจำไม่เพียงพอ ฉันขอแนะนำอย่างยิ่งให้คุณพยายามหลีกเลี่ยงการอ้างอิงที่อ่อนแอ เนื่องจากความคลุมเครือของข้อกำหนด Java ทำให้เกิดการตีความที่ผิดและข้อผิดพลาดในการใช้งาน ยิ่งไปกว่านั้น Java ยังได้รับการออกแบบสำหรับการจัดการหน่วยความจำแบบไดนามิก เนื่องจากคุณไม่จำเป็นต้องพิจารณาว่าจะปล่อยหน่วยความจำเมื่อใดและที่ไหน
ความท้าทายประการหนึ่งของตัวรวบรวมขยะคือการจัดสรรหน่วยความจำในลักษณะที่ไม่ส่งผลกระทบต่อแอปพลิเคชันที่ทำงานอยู่ หากคุณไม่รวบรวมขยะให้มากที่สุดเท่าที่จะเป็นไปได้ แอปพลิเคชันของคุณจะใช้หน่วยความจำ หากคุณรวบรวมบ่อยเกินไป คุณจะสูญเสียปริมาณงานและเวลาตอบสนอง ซึ่งจะส่งผลเสียต่อแอปพลิเคชันที่ทำงานอยู่
อัลกอริทึม GC
มีอัลกอริธึมการรวบรวมขยะที่แตกต่างกันมากมาย เราจะพูดคุยกันในเชิงลึกหลายประเด็นในซีรีส์นี้ต่อไป ในระดับสูงสุด วิธีการรวบรวมขยะหลักสองวิธีคือการนับอ้างอิงและการติดตามผู้รวบรวม
ตัวรวบรวมการนับการอ้างอิงจะติดตามจำนวนการอ้างอิงที่ออบเจ็กต์ชี้ไป เมื่อการอ้างอิงของวัตถุถึง 0 หน่วยความจำจะถูกเรียกคืนทันที ซึ่งเป็นหนึ่งในข้อดีของแนวทางนี้ ความยากในวิธีการนับข้อมูลอ้างอิงอยู่ที่โครงสร้างข้อมูลแบบวงกลมและการอัปเดตข้อมูลอ้างอิงทั้งหมดแบบเรียลไทม์
ตัวรวบรวมการติดตามทำเครื่องหมายออบเจ็กต์ที่ยังคงถูกอ้างอิง และใช้ออบเจ็กต์ที่ทำเครื่องหมายเพื่อติดตามและทำเครื่องหมายออบเจ็กต์ที่อ้างอิงทั้งหมดซ้ำ ๆ เมื่อออบเจ็กต์ทั้งหมดที่ยังคงอ้างอิงถูกทำเครื่องหมายเป็น "สด" พื้นที่ที่ไม่ได้ทำเครื่องหมายทั้งหมดจะถูกเรียกคืน วิธีนี้จัดการโครงสร้างข้อมูลวงแหวน แต่ในหลายกรณี ตัวรวบรวมควรรอจนกว่าการมาร์กทั้งหมดจะเสร็จสมบูรณ์ก่อนที่จะเรียกคืนหน่วยความจำที่ไม่ได้อ้างอิง
มีหลายวิธีในการทำวิธีการข้างต้น อัลกอริธึมที่มีชื่อเสียงที่สุดคือการทำเครื่องหมายหรือการคัดลอกอัลกอริธึม อัลกอริธึมแบบขนานหรือพร้อมกัน ฉันจะหารือเกี่ยวกับสิ่งเหล่านี้ในบทความต่อ ๆ ไป
โดยทั่วไปแล้ว ความหมายของการรวบรวมขยะคือการจัดสรรพื้นที่ที่อยู่ให้กับอ็อบเจ็กต์ใหม่และเก่าในฮีป "วัตถุโบราณ" คือวัตถุที่รอดพ้นจากการสะสมขยะจำนวนมาก ใช้คนรุ่นใหม่เพื่อจัดสรรวัตถุใหม่และรุ่นเก่าให้กับวัตถุเก่า ซึ่งสามารถลดการกระจายตัวของวัตถุได้โดยการรีไซเคิลวัตถุที่มีอายุสั้นซึ่งครอบครองหน่วยความจำอย่างรวดเร็ว ทั้งหมดนี้ช่วยลดการแตกแฟรกเมนต์ระหว่างออบเจ็กต์ที่มีอายุการใช้งานยาวนานและประหยัดหน่วยความจำฮีปจากการแตกแฟรกเมนต์ ผลเชิงบวกของคนรุ่นใหม่คือทำให้การรวบรวมวัตถุรุ่นเก่าที่มีราคาแพงกว่าล่าช้า และคุณสามารถใช้พื้นที่เดิมซ้ำสำหรับวัตถุชั่วคราวได้ (การรวบรวมพื้นที่เก่าจะมีราคาสูงกว่าเนื่องจากวัตถุที่มีอายุยืนยาวจะมีการอ้างอิงมากกว่าและต้องมีการข้ามผ่านมากขึ้น)
อัลกอริธึมสุดท้ายที่ควรกล่าวถึงคือการบดอัด ซึ่งเป็นวิธีการจัดการการกระจายตัวของหน่วยความจำ โดยทั่วไปการบดอัดจะย้ายวัตถุเข้าด้วยกันเพื่อปล่อยพื้นที่หน่วยความจำที่ต่อเนื่องกันมากขึ้น หากคุณคุ้นเคยกับการกระจายตัวของดิสก์และเครื่องมือที่จัดการกับดิสก์ คุณจะพบว่าการบีบอัดข้อมูลนั้นคล้ายกันมาก ยกเว้นว่าการบีบอัดนี้จะทำงานในหน่วยความจำฮีป Java ฉันจะหารือเกี่ยวกับการบดอัดโดยละเอียดในซีรีส์นี้
สรุป: ทบทวนและไฮไลท์
JVM ช่วยให้สามารถพกพาได้ (ตั้งโปรแกรมครั้งเดียว ทำงานได้ทุกที่) และการจัดการหน่วยความจำแบบไดนามิก ซึ่งเป็นคุณสมบัติหลักทั้งหมดของแพลตฟอร์ม Java ที่มีส่วนทำให้ได้รับความนิยมและเพิ่มประสิทธิภาพการทำงาน
ในบทความแรกเกี่ยวกับระบบเพิ่มประสิทธิภาพการทำงานของ JVM ฉันอธิบายว่าคอมไพเลอร์แปลงโค้ดไบต์เป็นภาษาคำสั่งของแพลตฟอร์มเป้าหมายได้อย่างไร และช่วยเพิ่มประสิทธิภาพการทำงานของโปรแกรม Java แบบไดนามิกได้อย่างไร แอปพลิเคชันที่แตกต่างกันต้องการคอมไพเลอร์ที่แตกต่างกัน
ฉันยังกล่าวถึงการจัดสรรหน่วยความจำและการรวบรวมขยะโดยย่อ และความเกี่ยวข้องกับประสิทธิภาพของแอปพลิเคชัน Java อย่างไร โดยพื้นฐานแล้ว ยิ่งคุณเติมฮีปและทริกเกอร์การรวบรวมขยะได้เร็วเท่าไร อัตราการใช้งานแอปพลิเคชัน Java ของคุณก็จะยิ่งสูงขึ้นเท่านั้น ความท้าทายประการหนึ่งสำหรับตัวรวบรวมขยะคือการจัดสรรหน่วยความจำในลักษณะที่ไม่ส่งผลกระทบต่อแอปพลิเคชันที่ทำงานอยู่ แต่ก่อนที่แอปพลิเคชันจะหน่วยความจำไม่เพียงพอ ในบทความต่อๆ ไป เราจะพูดถึงการรวบรวมขยะแบบดั้งเดิมและแบบใหม่ และการเพิ่มประสิทธิภาพการทำงานของ JVM ในรายละเอียดเพิ่มเติม