ล่าหน่วยความจำ Java รั่ว
เผยแพร่แล้ว: 2022-03-11โปรแกรมเมอร์ที่ไม่มีประสบการณ์มักคิดว่าการรวบรวมขยะอัตโนมัติของ Java ทำให้พวกเขาหมดกังวลเรื่องการจัดการหน่วยความจำ นี่เป็นความเข้าใจผิดทั่วไป: ในขณะที่ตัวรวบรวมขยะพยายามอย่างดีที่สุด เป็นไปได้อย่างยิ่งที่โปรแกรมเมอร์ที่ดีที่สุดจะตกเป็นเหยื่อของการรั่วไหลของหน่วยความจำที่ทำให้หมดอำนาจ ให้ฉันอธิบาย
หน่วยความจำรั่วเกิดขึ้นเมื่อการอ้างอิงวัตถุที่ไม่จำเป็นอีกต่อไปได้รับการบำรุงรักษาโดยไม่จำเป็น การรั่วไหลเหล่านี้ไม่ดี ประการหนึ่ง พวกเขาสร้างแรงกดดันโดยไม่จำเป็นต่อเครื่องของคุณ เนื่องจากโปรแกรมของคุณใช้ทรัพยากรมากขึ้นเรื่อยๆ ในการทำให้แย่ลงไปอีก การตรวจจับการรั่วไหลเหล่านี้อาจเป็นเรื่องยาก: การวิเคราะห์แบบสถิตมักจะมีปัญหาในการระบุข้อมูลอ้างอิงที่ซ้ำซ้อนเหล่านี้อย่างแม่นยำ และเครื่องมือตรวจจับการรั่วไหลที่มีอยู่จะติดตามและรายงานข้อมูลที่ละเอียดเกี่ยวกับวัตถุแต่ละรายการ ทำให้เกิดผลลัพธ์ที่ยากต่อการตีความและขาดความแม่นยำ
กล่าวอีกนัยหนึ่ง การรั่วไหลนั้นยากเกินกว่าจะระบุได้ หรือระบุด้วยคำที่เฉพาะเจาะจงเกินกว่าจะเป็นประโยชน์
จริงๆ แล้วมีปัญหาหน่วยความจำสี่ประเภทที่มีอาการคล้ายคลึงกันและทับซ้อนกัน แต่มีสาเหตุและวิธีแก้ไขที่หลากหลาย:
ประสิทธิภาพ : มักจะเกี่ยวข้องกับการสร้างและการลบอ็อบเจ็กต์มากเกินไป ความล่าช้าในการรวบรวมขยะเป็นเวลานาน การสลับหน้าระบบปฏิบัติการมากเกินไป และอื่นๆ
ข้อจำกัดของทรัพยากร : เกิดขึ้นเมื่อมีหน่วยความจำเหลือน้อยหรือหน่วยความจำของคุณมีการแยกส่วนเกินไปที่จะจัดสรรวัตถุขนาดใหญ่ ซึ่งอาจเป็นแบบเนทีฟหรือโดยทั่วไปแล้วเกี่ยวข้องกับ Java heap
Java heap leaks : หน่วยความจำรั่วแบบคลาสสิกซึ่งวัตถุ Java ถูกสร้างขึ้นอย่างต่อเนื่องโดยไม่ถูกปล่อยออกมา ซึ่งมักเกิดจากการอ้างอิงอ็อบเจ็กต์แฝง
การ รั่วไหลของหน่วยความจำ ภายใน: เกี่ยวข้องกับการใช้หน่วยความจำที่เพิ่มขึ้นอย่างต่อเนื่องซึ่งอยู่นอกฮีปของ Java เช่น การจัดสรรที่สร้างโดยโค้ด JNI ไดรเวอร์ หรือแม้แต่การจัดสรร JVM
ในบทช่วยสอนการจัดการหน่วยความจำนี้ ฉันจะเน้นที่ Java heaps ที่รั่วไหลและร่างแนวทางในการตรวจจับการรั่วไหลดังกล่าวโดยอิงตามรายงาน Java VisualVM และใช้อินเทอร์เฟซแบบภาพเพื่อวิเคราะห์แอปพลิเคชันที่ใช้เทคโนโลยี Java ในขณะที่กำลังทำงาน
แต่ก่อนที่คุณจะสามารถป้องกันและค้นหาหน่วยความจำรั่วได้ คุณควรทำความเข้าใจว่าเกิดขึ้นได้อย่างไรและทำไม ( หมายเหตุ: หากคุณมีความเข้าใจที่ดีเกี่ยวกับความซับซ้อนของหน่วยความจำรั่ว คุณสามารถข้ามไปข้างหน้าได้ )
หน่วยความจำรั่ว: ไพรเมอร์
สำหรับผู้เริ่มต้น ให้คิดว่าหน่วยความจำรั่วไหลเป็นโรค และ OutOfMemoryError
(OOM เพื่อความกระชับ) ของ Java เป็นอาการ แต่เช่นเดียวกับโรคใดๆ OOM ไม่ได้หมายความว่าหน่วยความจำรั่วทั้งหมดเสมอ ไป OOM สามารถเกิดขึ้นได้เนื่องจากการสร้างตัวแปรในเครื่องจำนวนมากหรือเหตุการณ์ดังกล่าว ในทางกลับกัน หน่วยความจำรั่วทั้งหมดไม่จำเป็นต้องปรากฏเป็น OOMs โดยเฉพาะอย่างยิ่งในกรณีของแอปพลิเคชันเดสก์ท็อปหรือแอปพลิเคชันไคลเอนต์ (ซึ่งไม่ได้ทำงานเป็นเวลานานมากโดยไม่ต้องรีสตาร์ท)
ทำไมการรั่วไหลเหล่านี้จึงเลวร้าย? เหนือสิ่งอื่นใด บล็อกของหน่วยความจำที่รั่วไหลระหว่างการทำงานของโปรแกรมมักจะลดประสิทธิภาพของระบบเมื่อเวลาผ่านไป เนื่องจากจะต้องสลับบล็อกหน่วยความจำที่จัดสรรแต่ไม่ได้ใช้ออกเมื่อหน่วยความจำฟิสิคัลว่างของระบบหมด ในที่สุด โปรแกรมอาจใช้พื้นที่ที่อยู่เสมือนที่มีอยู่จนหมด ซึ่งนำไปสู่ OOM
ถอดรหัส OutOfMemoryError
ดังที่ได้กล่าวไว้ข้างต้น OOM เป็นตัวบ่งชี้ทั่วไปของการรั่วไหลของหน่วยความจำ โดยพื้นฐานแล้ว ข้อผิดพลาดจะเกิดขึ้นเมื่อมีพื้นที่ไม่เพียงพอในการจัดสรรวัตถุใหม่ พยายามอย่างที่ควรจะเป็น ตัวรวบรวมขยะไม่พบพื้นที่ที่จำเป็น และไม่สามารถขยายฮีปได้อีก ดังนั้น เกิดข้อผิดพลาดพร้อมกับการติดตามสแต็ก
ขั้นตอนแรกในการวินิจฉัย OOM ของคุณคือการพิจารณาว่าข้อผิดพลาดหมายถึงอะไร ฟังดูชัดเจน แต่คำตอบก็ไม่ชัดเจนเสมอไป ตัวอย่างเช่น: OOM ปรากฏขึ้นเนื่องจาก Java heap เต็ม หรือเพราะ native heap เต็มหรือไม่ เพื่อช่วยคุณตอบคำถามนี้ มาวิเคราะห์ข้อความแสดงข้อผิดพลาดที่เป็นไปได้สองสามข้อ:
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: PermGen space
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?
java.lang.OutOfMemoryError: <reason> <stack trace> (Native method)
“พื้นที่ฮีป Java”
ข้อความแสดงข้อผิดพลาดนี้ไม่ได้หมายความถึงหน่วยความจำรั่วเสมอไป อันที่จริง ปัญหาอาจง่ายพอๆ กับปัญหาการกำหนดค่า
ตัวอย่างเช่น ฉันรับผิดชอบในการวิเคราะห์แอปพลิเคชันที่สร้าง OutOfMemoryError
ประเภทนี้อย่างสม่ำเสมอ หลังจากการสอบสวน ฉันพบว่าผู้กระทำผิดคือการสร้างอินสแตนซ์อาร์เรย์ที่ต้องการหน่วยความจำมากเกินไป ในกรณีนี้ ไม่ใช่ความผิดของแอปพลิเคชัน แต่เซิร์ฟเวอร์แอปพลิเคชันใช้ขนาดฮีปเริ่มต้น ซึ่งมีขนาดเล็กเกินไป ฉันแก้ไขปัญหาด้วยการปรับพารามิเตอร์หน่วยความจำของ JVM
ในกรณีอื่นๆ และโดยเฉพาะอย่างยิ่งสำหรับแอปพลิเคชันที่มีอายุการใช้งานยาวนาน ข้อความอาจเป็นเครื่องบ่งชี้ว่าเรามี การอ้างอิงถึงอ็อบเจ กต์โดยไม่ได้ตั้งใจ ซึ่งทำให้ตัวรวบรวมขยะไม่สามารถทำความสะอาดได้ นี่คือภาษา Java ที่เทียบเท่ากับการรั่วไหลของหน่วยความจำ ( หมายเหตุ: API ที่เรียกใช้โดยแอปพลิเคชันอาจมีการอ้างอิงอ็อบเจ็กต์โดยไม่ได้ตั้งใจ )
อีกแหล่งที่เป็นไปได้ของ "Java heap space" OOMs เกิดขึ้นจากการใช้ Finalizers ถ้าคลาสมีเมธอด finalize
อ็อบเจ็กต์ประเภทนั้นจะไม่มีพื้นที่เรียกคืนในเวลารวบรวมขยะ แต่หลังจากการรวบรวมขยะ ออบเจ็กต์จะถูกจัดคิวเพื่อสรุปผล ซึ่งจะเกิดขึ้นในภายหลัง ในการนำ Sun ไปใช้ Finalizers จะดำเนินการโดยเธรด daemon หากเธรดตัวสุดท้ายไม่สามารถตามคิวการสรุปผล ฮีป Java อาจเต็มและ OOM อาจถูกโยนทิ้ง
“พื้นที่ PermGen”
ข้อความแสดงข้อผิดพลาดนี้ระบุว่ารุ่นถาวรเต็มแล้ว การสร้างถาวรคือพื้นที่ของฮีปที่เก็บอ็อบเจ็กต์คลาสและเมธอด หากแอปพลิเคชันโหลดคลาสจำนวนมาก อาจต้องเพิ่มขนาดของการสร้างถาวรโดยใช้ตัวเลือก -XX:MaxPermSize
ออบเจ็กต์ java.lang.String
ภายในยังถูกเก็บไว้ในรุ่นถาวร คลาส java.lang.String
รักษาพูลของสตริง เมื่อเรียกใช้เมธอด intern เมธอดจะตรวจสอบพูลเพื่อดูว่ามีสตริงที่เทียบเท่าหรือไม่ ถ้าเป็นเช่นนั้น มันจะถูกส่งกลับโดยวิธีการฝึกงาน ถ้าไม่เช่นนั้น สตริงจะถูกเพิ่มลงในพูล ในแง่ที่แม่นยำยิ่งขึ้น เมธอด java.lang.String.intern
จะคืนค่าการแสดงค่ามาตรฐานของสตริง ผลลัพธ์คือการอ้างอิงถึงอินสแตนซ์ของคลาสเดียวกันที่จะถูกส่งกลับหากสตริงนั้นปรากฏเป็นตัวอักษร หากแอปพลิเคชันฝึกสตริงจำนวนมาก คุณอาจต้องเพิ่มขนาดของการสร้างถาวร
หมายเหตุ: คุณสามารถใช้คำสั่ง jmap -permgen
เพื่อพิมพ์สถิติที่เกี่ยวข้องกับการสร้างถาวร รวมถึงข้อมูลเกี่ยวกับอินสแตนซ์สตริงภายใน
“ขนาดอาร์เรย์ที่ร้องขอเกินขีดจำกัด VM”
ข้อผิดพลาดนี้บ่งชี้ว่าแอปพลิเคชัน (หรือ API ที่ใช้โดยแอปพลิเคชันนั้น) พยายามจัดสรรอาร์เรย์ที่ใหญ่กว่าขนาดฮีป ตัวอย่างเช่น หากแอปพลิเคชันพยายามจัดสรรอาร์เรย์ 512MB แต่ขนาดฮีปสูงสุดคือ 256MB OOM จะถูกส่งออกไปพร้อมกับข้อความแสดงข้อผิดพลาดนี้ ในกรณีส่วนใหญ่ ปัญหาอาจเป็นปัญหาการกำหนดค่าหรือจุดบกพร่องที่ส่งผลให้แอปพลิเคชันพยายามจัดสรรอาร์เรย์ขนาดใหญ่
“ขอ <ขนาด> ไบต์สำหรับ <เหตุผล> หมดพื้นที่สวอปแล้วเหรอ?”
ข้อความนี้ดูเหมือนจะเป็น OOM อย่างไรก็ตาม HotSpot VM แสดงข้อยกเว้นที่ชัดเจนเมื่อการจัดสรรจากฮีปดั้งเดิมล้มเหลวและฮีปดั้งเดิมอาจใกล้หมด รวมอยู่ในข้อความคือขนาด (เป็นไบต์) ของการร้องขอที่ล้มเหลวและเหตุผลสำหรับการร้องขอหน่วยความจำ ในกรณีส่วนใหญ่ <reason> คือชื่อของโมดูลต้นทางที่รายงานความล้มเหลวในการจัดสรร
หาก OOM ประเภทนี้ถูกส่งออกไป คุณอาจต้องใช้ยูทิลิตี้การแก้ไขปัญหาบนระบบปฏิบัติการของคุณเพื่อวินิจฉัยปัญหาเพิ่มเติม ในบางกรณี ปัญหาอาจไม่เกี่ยวข้องกับแอปพลิเคชันด้วยซ้ำ ตัวอย่างเช่น คุณอาจเห็นข้อผิดพลาดนี้หาก:
ระบบปฏิบัติการได้รับการกำหนดค่าด้วยพื้นที่สว็อปไม่เพียงพอ
กระบวนการอื่นในระบบกำลังใช้ทรัพยากรหน่วยความจำที่มีอยู่ทั้งหมด
นอกจากนี้ยังอาจเป็นไปได้ว่าแอปพลิเคชันล้มเหลวเนื่องจากการรั่วไหลของข้อมูล (เช่น หากแอปพลิเคชันหรือรหัสไลบรารีบางส่วนจัดสรรหน่วยความจำอย่างต่อเนื่อง แต่ไม่สามารถเผยแพร่ไปยังระบบปฏิบัติการได้)
<เหตุผล> <การติดตามสแต็ก> (วิธีดั้งเดิม)
หากคุณเห็นข้อความแสดงข้อผิดพลาดนี้และเฟรมบนสุดของการติดตามสแต็กของคุณเป็นเมธอดดั้งเดิม แสดงว่าเมธอดเนทีฟนั้นประสบปัญหาการจัดสรรล้มเหลว ข้อแตกต่างระหว่างข้อความนี้กับข้อความก่อนหน้าคือ ตรวจพบความล้มเหลวในการจัดสรรหน่วยความจำ Java ใน JNI หรือเมธอดดั้งเดิม มากกว่าในโค้ด Java VM
หาก OOM ประเภทนี้ถูกส่งออกไป คุณอาจต้องใช้ยูทิลิตี้บนระบบปฏิบัติการเพื่อวินิจฉัยปัญหาเพิ่มเติม
แอปพลิเคชันขัดข้องโดยไม่มี OOM
ในบางครั้ง แอปพลิเคชันอาจหยุดทำงานในไม่ช้าหลังจากความล้มเหลวในการจัดสรรจากฮีปดั้งเดิม กรณีนี้เกิดขึ้นหากคุณใช้โค้ดเนทีฟที่ไม่ตรวจสอบข้อผิดพลาดที่ส่งคืนโดยฟังก์ชันการจัดสรรหน่วยความจำ
ตัวอย่างเช่น การเรียกระบบ malloc
จะคืน NULL
หากไม่มีหน่วยความจำที่พร้อมใช้งาน หากไม่ได้ตรวจสอบการส่งคืนจาก malloc
แอปพลิเคชันอาจหยุดทำงานเมื่อพยายามเข้าถึงตำแหน่งหน่วยความจำที่ไม่ถูกต้อง ปัญหาประเภทนี้อาจค้นหาได้ยาก ทั้งนี้ขึ้นอยู่กับสถานการณ์
ในบางกรณี ข้อมูลจากบันทึกข้อผิดพลาดร้ายแรงหรือการถ่ายโอนข้อมูลการขัดข้องจะเพียงพอ หากสาเหตุของการขัดข้องถูกกำหนดให้เป็นการขาดการจัดการข้อผิดพลาดในการจัดสรรหน่วยความจำบางรายการ คุณจะต้องค้นหาสาเหตุของความล้มเหลวในการจัดสรรดังกล่าว เช่นเดียวกับปัญหาฮีปดั้งเดิมอื่น ๆ ระบบอาจได้รับการกำหนดค่าด้วยพื้นที่สว็อปไม่เพียงพอ กระบวนการอื่นอาจใช้ทรัพยากรหน่วยความจำที่มีอยู่ทั้งหมด ฯลฯ
การวินิจฉัยการรั่วไหล
ในกรณีส่วนใหญ่ การวินิจฉัยการรั่วไหลของหน่วยความจำต้องใช้ความรู้โดยละเอียดเกี่ยวกับแอปพลิเคชันที่เป็นปัญหา คำเตือน: กระบวนการอาจยาวและทำซ้ำได้
กลยุทธ์ของเราในการค้นหาหน่วยความจำรั่วจะค่อนข้างตรงไปตรงมา:
ระบุอาการ
เปิดใช้งานการรวบรวมขยะอย่างละเอียด
เปิดใช้งานการทำโปรไฟล์
วิเคราะห์ร่องรอย
1. ระบุอาการ
ตามที่กล่าวไว้ ในหลายกรณี กระบวนการ Java จะส่งข้อยกเว้นรันไทม์ OOM ในที่สุด ซึ่งเป็นตัวบ่งชี้ที่ชัดเจนว่าทรัพยากรหน่วยความจำของคุณหมดลงแล้ว ในกรณีนี้ คุณต้องแยกความแตกต่างระหว่างความจำเสื่อมปกติกับการรั่วไหล วิเคราะห์ข้อความของ OOM และพยายามค้นหาผู้กระทำผิดตามการสนทนาที่ให้ไว้ข้างต้น
บ่อยครั้ง หากแอปพลิเคชัน Java ขอพื้นที่จัดเก็บมากกว่าที่รันไทม์ฮีปเสนอ อาจเกิดจากการออกแบบที่ไม่ดี ตัวอย่างเช่น หากแอปพลิเคชันสร้างสำเนารูปภาพหลายชุดหรือโหลดไฟล์ลงในอาร์เรย์ พื้นที่เก็บข้อมูลจะหมดเมื่อรูปภาพหรือไฟล์มีขนาดใหญ่มาก นี่เป็นการสิ้นเปลืองทรัพยากรตามปกติ แอปพลิเคชันทำงานตามที่ออกแบบไว้ (แม้ว่าการออกแบบนี้จะดูไม่มีกระดูกก็ตาม)
แต่ถ้าแอปพลิเคชันเพิ่มการใช้หน่วยความจำอย่างต่อเนื่องในขณะที่ประมวลผลข้อมูลประเภทเดียวกัน คุณอาจมีหน่วยความจำรั่ว
2. เปิดใช้งานการรวบรวมขยะอย่างละเอียด
วิธีที่เร็วที่สุดวิธีหนึ่งในการยืนยันว่าคุณมีหน่วยความจำรั่วคือการเปิดใช้งานการรวบรวมขยะแบบละเอียด ปัญหาข้อจำกัดของหน่วยความจำมักจะสามารถระบุได้โดยการตรวจสอบรูปแบบในเอาต์พุต verbosegc
โดยเฉพาะอย่างยิ่ง อาร์กิวเมนต์ -verbosegc
ช่วยให้คุณสร้างการติดตามทุกครั้งที่เริ่มกระบวนการรวบรวมขยะ (GC) กล่าวคือ ขณะที่หน่วยความจำกำลังถูกเก็บขยะ รายงานสรุปจะถูกพิมพ์ออกมาเป็นข้อผิดพลาดมาตรฐาน ทำให้คุณเข้าใจว่าหน่วยความจำของคุณได้รับการจัดการอย่างไร

ต่อไปนี้คือเอาต์พุตทั่วไปที่สร้างด้วยตัวเลือก –verbosegc
:
แต่ละบล็อค (หรือ stanza) ในไฟล์การติดตาม GC นี้มีการกำหนดหมายเลขตามลำดับที่เพิ่มขึ้น เพื่อให้เข้าใจถึงการติดตามนี้ คุณควรดูกลุ่มความล้มเหลวในการจัดสรรที่ต่อเนื่องกัน และมองหาหน่วยความจำที่ว่าง (ไบต์และเปอร์เซ็นต์) ที่ลดลงเมื่อเวลาผ่านไปในขณะที่หน่วยความจำทั้งหมด (ที่นี่ 19725304) เพิ่มขึ้น นี่เป็นสัญญาณทั่วไปของการสูญเสียความทรงจำ
3. เปิดใช้งานการทำโปรไฟล์
JVM ที่ต่างกันเสนอวิธีต่างๆ ในการสร้างไฟล์การติดตามเพื่อสะท้อนกิจกรรมของฮีป ซึ่งโดยทั่วไปแล้วจะมีข้อมูลโดยละเอียดเกี่ยวกับประเภทและขนาดของอ็อบเจ็กต์ สิ่งนี้เรียกว่า การทำโปรไฟล์ฮีป
4. วิเคราะห์ร่องรอย
โพสต์นี้เน้นที่การติดตามที่สร้างโดย Java VisualVM ร่องรอยสามารถมาในรูปแบบต่างๆ ได้ เนื่องจากสามารถสร้างขึ้นโดยเครื่องมือตรวจจับการรั่วไหลของหน่วยความจำ Java ที่แตกต่างกัน แต่แนวคิดเบื้องหลังจะเหมือนกันเสมอ: ค้นหาบล็อกของวัตถุในฮีปที่ไม่ควรอยู่ที่นั่น และพิจารณาว่าวัตถุเหล่านี้สะสมหรือไม่ แทนที่จะปล่อย สิ่งที่น่าสนใจเป็นพิเศษคืออ็อบเจ็กต์ชั่วคราวที่ทราบว่าได้รับการจัดสรรทุกครั้งที่มีทริกเกอร์เหตุการณ์บางอย่างในแอปพลิเคชัน Java การมีอยู่ของอินสแตนซ์อ็อบเจ็กต์จำนวนมากที่ควรมีอยู่ในปริมาณเล็กน้อยเท่านั้น โดยทั่วไปจะบ่งชี้ถึงข้อบกพร่องของแอปพลิเคชัน
สุดท้าย การแก้ไขหน่วยความจำรั่ว คุณจะต้องตรวจสอบโค้ดของคุณอย่างละเอียด การเรียนรู้เกี่ยวกับประเภทของวัตถุที่รั่วไหลจะมีประโยชน์มากและทำให้การดีบักเร็วขึ้นอย่างมาก
การรวบรวมขยะทำงานอย่างไรใน JVM
ก่อนที่เราจะเริ่มการวิเคราะห์แอปพลิเคชันที่มีปัญหาหน่วยความจำรั่ว มาดูวิธีการทำงานของการรวบรวมขยะใน JVM ก่อน
JVM ใช้รูปแบบของตัวรวบรวมขยะที่เรียกว่าตัว รวบรวมการติดตาม ซึ่งโดยพื้นฐานแล้วทำงานโดยการหยุดโลกรอบ ๆ โลกชั่วคราว ทำเครื่องหมายวัตถุรูททั้งหมด (วัตถุที่อ้างอิงโดยตรงโดยการรันเธรด) และตามการอ้างอิงของพวกเขา ทำเครื่องหมายแต่ละวัตถุที่เห็นตลอดทาง
Java ใช้สิ่งที่เรียกว่าตัวรวบรวมขยะใน ชั่ว อายุคนตามสมมติฐานของสมมติฐานรุ่น ซึ่งระบุว่า อ็อบเจ็กต์ส่วนใหญ่ที่สร้างขึ้นจะถูกละทิ้งอย่างรวดเร็ว และ อ็อบเจ็กต์ที่ไม่ได้รวบรวมอย่างรวดเร็วมักจะอยู่ชั่วขณะ หนึ่ง
ตามสมมติฐานนี้ Java แบ่งพาร์ติชันอ็อบเจ็กต์ออกเป็นหลายรุ่น นี่คือการตีความภาพ:
Young Generation - นี่คือจุดเริ่มต้นของวัตถุ มันมีสองรุ่นย่อย:
Eden Space - วัตถุเริ่มต้นที่นี่ วัตถุส่วนใหญ่ถูกสร้างขึ้นและถูกทำลายในเอเดนสเปซ ที่นี่ GC ทำ Minor GCs ซึ่งเป็นการรวบรวมขยะที่ปรับให้เหมาะสม เมื่อดำเนินการ Minor GC การอ้างอิงถึงวัตถุที่ยังต้องการจะถูกย้ายไปยังพื้นที่ผู้รอดชีวิต (S0 หรือ S1)
Survivor Space (S0 และ S1) - วัตถุที่เอาชีวิตรอดจากอีเดนมาอยู่ที่นี่ มี 2 อย่างนี้ และใช้งานเพียงครั้งเดียวเท่านั้น (เว้นแต่เราจะมีหน่วยความจำรั่วอย่างร้ายแรง) ตัวหนึ่งถูกกำหนดเป็น empty และอีกอันเป็น live สลับกับทุกรอบ GC
Tenured Generation - เรียกอีกอย่างว่าคนรุ่นเก่า (พื้นที่เก่าในรูปที่ 2) พื้นที่นี้เก็บวัตถุที่เก่ากว่าที่มีอายุการใช้งานยาวนานขึ้น (ย้ายจากพื้นที่ผู้รอดชีวิตหากพวกมันมีชีวิตอยู่นานพอ) เมื่อพื้นที่นี้เต็ม GC จะทำ Full GC ซึ่งมีค่าใช้จ่ายมากกว่าในแง่ของประสิทธิภาพ หากพื้นที่นี้เติบโตโดยไม่มีขอบเขต JVM จะโยน
OutOfMemoryError - Java heap space
การ สร้างถาวร - รุ่นที่สามที่เกี่ยวข้องอย่างใกล้ชิดกับรุ่นที่ดำรงตำแหน่ง รุ่นถาวรนั้นพิเศษเพราะเก็บข้อมูลที่เครื่องเสมือนต้องการเพื่ออธิบายอ็อบเจ็กต์ที่ไม่มีความเท่าเทียมกันในระดับภาษา Java ตัวอย่างเช่น ออบเจ็กต์ที่อธิบายคลาสและเมธอดจะถูกเก็บไว้ในรุ่นถาวร
Java ฉลาดพอที่จะใช้วิธีรวบรวมขยะที่แตกต่างกันในแต่ละรุ่น คนรุ่นใหม่ได้รับการจัดการโดยใช้ตัวรวบรวม การติดตามและคัดลอก ที่เรียกว่า Parallel New Collector นักสะสมคนนี้หยุดโลก แต่เนื่องจากคนรุ่นใหม่โดยทั่วไปมีขนาดเล็ก การหยุดชั่วคราวจึงสั้น
สำหรับข้อมูลเพิ่มเติมเกี่ยวกับรุ่น JVM และวิธีการทำงานโดยละเอียด โปรดไปที่การจัดการหน่วยความจำในเอกสาร Java HotSpot Virtual Machine
การตรวจจับการรั่วไหลของหน่วยความจำ
ในการค้นหาหน่วยความจำรั่วและกำจัดมัน คุณต้องมีเครื่องมือหน่วยความจำรั่วที่เหมาะสม ได้เวลาตรวจจับและลบรอยรั่วดังกล่าวโดยใช้ Java VisualVM
การทำโปรไฟล์ฮีปจากระยะไกลด้วย Java VisualVM
VisualVM เป็นเครื่องมือที่ให้อินเทอร์เฟซแบบภาพสำหรับการดูข้อมูลโดยละเอียดเกี่ยวกับแอปพลิเคชันที่ใช้เทคโนโลยี Java ในขณะที่กำลังทำงาน
ด้วย VisualVM คุณสามารถดูข้อมูลที่เกี่ยวข้องกับแอปพลิเคชันในเครื่องและที่ทำงานบนโฮสต์ระยะไกลได้ คุณยังสามารถเก็บข้อมูลเกี่ยวกับอินสแตนซ์ซอฟต์แวร์ JVM และบันทึกข้อมูลลงในระบบโลคัลของคุณได้
เพื่อให้ได้รับประโยชน์จากคุณลักษณะทั้งหมดของ Java VisualVM คุณควรเรียกใช้ Java Platform, Standard Edition (Java SE) เวอร์ชัน 6 หรือสูงกว่า
การเปิดใช้งานการเชื่อมต่อระยะไกลสำหรับ JVM
ในสภาพแวดล้อมการใช้งานจริง มักจะเป็นเรื่องยากที่จะเข้าถึงเครื่องจริงที่จะใช้โค้ดของเรา โชคดีที่เราสามารถสร้างโปรไฟล์แอปพลิเคชัน Java ของเราจากระยะไกลได้
อันดับแรก เราต้องให้สิทธิ์การเข้าถึง JVM บนเครื่องเป้าหมาย ในการดำเนินการดังกล่าว ให้สร้างไฟล์ชื่อ jstatd.all.policy โดยมีเนื้อหาดังต่อไปนี้:
grant codebase "file:${java.home}/../lib/tools.jar" { permission java.security.AllPermission; };
เมื่อสร้างไฟล์แล้ว เราจำเป็นต้องเปิดใช้งานการเชื่อมต่อระยะไกลกับ VM เป้าหมายโดยใช้เครื่องมือ jstatd - Virtual Machine jstat Daemon ดังนี้:
jstatd -p <PORT_NUMBER> -J-Djava.security.policy=<PATH_TO_POLICY_FILE>
ตัวอย่างเช่น:
jstatd -p 1234 -J-Djava.security.policy=D:\jstatd.all.policy
เมื่อ jstatd เริ่มทำงานใน VM เป้าหมาย เราสามารถเชื่อมต่อกับเครื่องเป้าหมายและกำหนดโปรไฟล์แอปพลิเคชันจากระยะไกลที่มีปัญหาหน่วยความจำรั่ว
การเชื่อมต่อกับโฮสต์ระยะไกล
ในเครื่องไคลเอนต์ เปิดพร้อมต์แล้วพิมพ์ jvisualvm
เพื่อเปิดเครื่องมือ VisualVM
ต่อไป เราต้องเพิ่มโฮสต์ระยะไกลใน VisualVM เนื่องจาก JVM เป้าหมายเปิดใช้งานเพื่ออนุญาตการเชื่อมต่อระยะไกลจากเครื่องอื่นที่มี J2SE 6 ขึ้นไป เราจึงเริ่มเครื่องมือ Java VisualVM และเชื่อมต่อกับโฮสต์ระยะไกล หากการเชื่อมต่อกับรีโมตโฮสต์สำเร็จ เราจะเห็นแอปพลิเคชัน Java ที่ทำงานอยู่ใน JVM เป้าหมายดังที่แสดงไว้ที่นี่:
ในการเรียกใช้ตัวสร้างโปรไฟล์หน่วยความจำในแอปพลิเคชัน เราเพียงแค่ดับเบิลคลิกที่ชื่อในแผงด้านข้าง
ตอนนี้เราพร้อมแล้วด้วยตัววิเคราะห์หน่วยความจำ เรามาตรวจสอบแอปพลิเคชันที่มีปัญหาหน่วยความจำรั่ว ซึ่งเราจะเรียกว่า MemLeak
MemLeak
แน่นอนว่ามีหลายวิธีในการสร้างหน่วยความจำรั่วใน Java เพื่อความง่าย เราจะกำหนดคลาสให้เป็นคีย์ใน HashMap
แต่เราจะไม่กำหนดเมธอด equals() และ hashcode()
HashMap คือการนำตารางแฮชไปใช้งานสำหรับอินเทอร์เฟซแผนที่ และด้วยเหตุนี้จึงกำหนดแนวคิดพื้นฐานของคีย์และค่า: แต่ละค่าเกี่ยวข้องกับคีย์ที่ไม่ซ้ำกัน ดังนั้นหากคีย์สำหรับคู่คีย์-ค่าที่กำหนดมีอยู่แล้วใน HashMap ค่าปัจจุบันจะถูกแทนที่
จำเป็นที่คลาสคีย์ของเราต้องมีการใช้งานเมธอด equals()
และ hashcode()
ที่ถูกต้อง หากไม่มีพวกเขา ก็ไม่รับประกันว่าจะมีการสร้างคีย์ที่ดี
โดยไม่ได้กำหนดเมธอด equals()
และ hashcode()
เราเพิ่มคีย์เดียวกันใน HashMap ซ้ำแล้วซ้ำเล่า และแทนที่จะแทนที่คีย์ตามที่ควรจะเป็น HashMap จะเติบโตอย่างต่อเนื่อง โดยไม่สามารถระบุคีย์ที่เหมือนกันเหล่านี้และโยน OutOfMemoryError
.
นี่คือคลาส MemLeak:
package com.post.memory.leak; import java.util.Map; public class MemLeak { public final String key; public MemLeak(String key) { this.key =key; } public static void main(String args[]) { try { Map map = System.getProperties(); for(;;) { map.put(new MemLeak("key"), "value"); } } catch(Exception e) { e.printStackTrace(); } } }
หมายเหตุ: หน่วยความจำรั่ว ไม่ได้ เกิดจากลูปอนันต์ในบรรทัดที่ 14: การวนซ้ำแบบอนันต์อาจทำให้ทรัพยากรหมดได้ แต่ไม่ใช่หน่วยความจำรั่ว หากเราใช้เมธอด equals()
และ hashcode()
อย่างเหมาะสม โค้ดก็จะทำงานได้ดีแม้จะวนเป็นอนันต์ เนื่องจากเราจะมีองค์ประกอบเดียวใน HashMap
(สำหรับผู้ที่สนใจ นี่คือวิธีการอื่น (โดยเจตนา) ที่ทำให้เกิดการรั่วไหล)
การใช้ Java VisualVM
ด้วย Java VisualVM เราสามารถตรวจสอบหน่วยความจำ Java Heap และระบุว่าพฤติกรรมนั้นบ่งบอกถึงหน่วยความจำรั่วหรือไม่
นี่คือการแสดงกราฟิกของตัววิเคราะห์ Java Heap ของ MemLeak หลังจากการเริ่มต้น (จำการสนทนาของเราเกี่ยวกับรุ่นต่างๆ):
หลังจากผ่านไปเพียง 30 วินาที Old Generation ก็ใกล้จะเต็มแล้ว แสดงให้เห็นว่าถึงแม้จะใช้ Full GC แต่ Old Generation ก็เติบโตขึ้นเรื่อยๆ เป็นสัญญาณที่ชัดเจนของหน่วยความจำรั่ว
วิธีการหนึ่งในการตรวจหาสาเหตุของการรั่วไหลจะแสดงในรูปต่อไปนี้ ( คลิกเพื่อซูม ) ที่สร้างขึ้นโดยใช้ Java VisualVM พร้อมฮีปดัมพ์ ที่นี่ เราจะเห็นว่า 50% ของวัตถุ Hashtable$Entry อยู่ในฮีป ในขณะที่บรรทัดที่สองชี้เราไปที่คลาส MemLeak ดังนั้น หน่วยความจำรั่วจึงเกิดจาก ตารางแฮช ที่ใช้ภายในคลาส MemLeak
สุดท้าย ให้สังเกต Java Heap หลังจาก OutOfMemoryError
ของเราซึ่ง คนรุ่นใหม่และคนรุ่นเก่าเต็มไป หมด
บทสรุป
หน่วยความจำรั่วเป็นหนึ่งในปัญหาแอปพลิเคชัน Java ที่แก้ไขได้ยากที่สุด เนื่องจากอาการจะหลากหลายและทำซ้ำได้ยาก เราได้สรุปแนวทางทีละขั้นตอนเพื่อค้นหาหน่วยความจำรั่วไหลและระบุแหล่งที่มา แต่เหนือสิ่งอื่นใด โปรดอ่านข้อความแสดงข้อผิดพลาดของคุณอย่างใกล้ชิดและใส่ใจกับสแต็กเทรซของคุณ—ไม่ใช่ว่าการรั่วไหลทั้งหมดจะง่ายอย่างที่ปรากฏ
ภาคผนวก
นอกจาก Java VisualVM แล้ว ยังมีเครื่องมืออื่นๆ อีกหลายอย่างที่สามารถทำการตรวจจับการรั่วไหลของหน่วยความจำได้ เครื่องตรวจจับรอยรั่วจำนวนมากทำงานในระดับไลบรารีโดยสกัดกั้นการเรียกใช้รูทีนการจัดการหน่วยความจำ ตัวอย่างเช่น HPROF
เป็นเครื่องมือบรรทัดคำสั่งอย่างง่ายที่มาพร้อมกับ Java 2 Platform Standard Edition (J2SE) สำหรับการทำโปรไฟล์ฮีปและ CPU เอาต์พุตของ HPROF
สามารถวิเคราะห์ได้โดยตรงหรือใช้เป็นอินพุตสำหรับเครื่องมืออื่นๆ เช่น JHAT
เมื่อเราทำงานกับแอปพลิเคชัน Java 2 Enterprise Edition (J2EE) มีโซลูชันตัววิเคราะห์ heap dump จำนวนมากที่เป็นมิตรกว่า เช่น IBM Heapdumps สำหรับแอปพลิเคชันเซิร์ฟเวอร์ Websphere