ฉันเพิ่งได้รับการสัมภาษณ์และฉันถูกขอให้สร้างหน่วยความจำรั่วด้วย Java
ไม่จำเป็นต้องพูดอะไรเลยฉันรู้สึกงี่เง่าที่ไม่มีเงื่อนงำว่าจะเริ่มสร้างได้อย่างไร
ตัวอย่างจะเป็นอย่างไร
ฉันเพิ่งได้รับการสัมภาษณ์และฉันถูกขอให้สร้างหน่วยความจำรั่วด้วย Java
ไม่จำเป็นต้องพูดอะไรเลยฉันรู้สึกงี่เง่าที่ไม่มีเงื่อนงำว่าจะเริ่มสร้างได้อย่างไร
ตัวอย่างจะเป็นอย่างไร
คำตอบ:
นี่เป็นวิธีที่ดีในการสร้างการรั่วไหลของหน่วยความจำที่แท้จริง (วัตถุที่ไม่สามารถเข้าถึงได้โดยการเรียกใช้รหัส แต่ยังคงเก็บไว้ในหน่วยความจำ) ในจาวาบริสุทธิ์:
ClassLoader
(ขยะที่กำหนดเอง)new byte[1000000]
) ThreadLocal
ร้านค้าที่แข็งแกร่งในการอ้างอิงในช่องคงที่แล้วเก็บอ้างอิงถึงตัวเองใน การจัดสรรหน่วยความจำเสริมเป็นทางเลือก (การรั่วไหลของอินสแตนซ์ของคลาสนั้นเพียงพอ) แต่จะทำให้การรั่วไหลนั้นทำงานเร็วขึ้นมากClassLoader
โหลดจากเนื่องจากวิธีThreadLocal
การใช้งานใน JDK ของ Oracle สิ่งนี้สร้างความจำรั่ว:
Thread
มีเขตข้อมูลส่วนตัวthreadLocals
ซึ่งจริง ๆ แล้วเก็บค่า thread-localThreadLocal
วัตถุดังนั้นหลังจากThreadLocal
วัตถุนั้นถูกรวบรวมขยะรายการของมันจะถูกลบออกจากแผนที่ThreadLocal
วัตถุที่เป็นกุญแจสำคัญวัตถุนั้นจะไม่ถูกรวบรวมขยะหรือลบออกจากแผนที่ตราบใดที่เธรดยังมีชีวิตอยู่ในตัวอย่างนี้ห่วงโซ่ของการอ้างอิงที่แข็งแกร่งมีลักษณะเช่นนี้:
Thread
วัตถุ→ threadLocals
แผนที่→อินสแตนซ์ของคลาสตัวอย่าง→ตัวอย่างคลาส→สแตติกThreadLocal
ฟิลด์→ ThreadLocal
วัตถุ
(การที่ClassLoader
ไม่ได้มีบทบาทในการสร้างการรั่วไหลจริงๆมันทำให้การรั่วไหลแย่ลงเพราะห่วงโซ่อ้างอิงเพิ่มเติมนี้: class class →→คลาสClassLoader
ทั้งหมดที่โหลดไว้มันแย่กว่าเดิมในการใช้ JVM โดยเฉพาะอย่างยิ่งก่อน Java 7 เนื่องจากคลาสและClassLoader
s ถูกจัดสรรโดยตรงไปยัง permgen และไม่เคยถูกรวบรวมขยะเลย)
รูปแบบของรูปแบบนี้คือสาเหตุที่แอปพลิเคชั่นคอนเทนเนอร์ (เช่น Tomcat) สามารถรั่วไหลของหน่วยความจำได้เช่นเดียวกับตะแกรงถ้าคุณมักจะปรับใช้แอปพลิเคชั่นที่เกิดขึ้นเพื่อใช้ThreadLocal
s ในทางกลับกัน สิ่งนี้สามารถเกิดขึ้นได้ด้วยเหตุผลหลายประการและมักจะยากที่จะดีบักและ / หรือแก้ไข
อัพเดท : เนื่องจากผู้คนจำนวนมากขอให้มันนี่บางโค้ดตัวอย่างที่แสดงให้เห็นว่าพฤติกรรมนี้ในการดำเนินการ
การอ้างอิงวัตถุคงที่ในฟิลด์ [ฟิลด์สุดท้ายของ esp]
class MemorableClass {
static final ArrayList list = new ArrayList(100);
}
การโทรString.intern()
ด้วย String ที่มีความยาว
String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();
(ไม่เปิดเผย) สตรีมแบบเปิด (ไฟล์เครือข่าย ฯลฯ ... )
try {
BufferedReader br = new BufferedReader(new FileReader(inputFile));
...
...
} catch (Exception e) {
e.printStacktrace();
}
การเชื่อมต่อที่ไม่เปิดเผย
try {
Connection conn = ConnectionFactory.getConnection();
...
...
} catch (Exception e) {
e.printStacktrace();
}
พื้นที่ที่ไม่สามารถเข้าถึงได้จากตัวรวบรวมขยะของ JVMเช่นหน่วยความจำที่จัดสรรผ่านวิธีดั้งเดิม
ในเว็บแอปพลิเคชันวัตถุบางอย่างจะถูกเก็บไว้ในขอบเขตแอปพลิเคชันจนกว่าแอปพลิเคชันจะหยุดหรือลบอย่างชัดเจน
getServletContext().setAttribute("SOME_MAP", map);
ตัวเลือก JVM ไม่ถูกต้องหรือไม่เหมาะสมเช่นnoclassgc
ตัวเลือกบน IBM JDK ที่ป้องกันการรวบรวมขยะคลาสที่ไม่ได้ใช้
ดูการตั้งค่า IBM JDK
close()
โดยปกติจะไม่เรียกใช้ใน finalizer เธรดตั้งแต่อาจเป็นการดำเนินการบล็อก) มันเป็นวิธีปฏิบัติที่ไม่ดีที่จะไม่ปิด แต่ก็ไม่ทำให้เกิดการรั่วไหล Unclosed java.sql.Connection เหมือนกัน
intern
เนื้อหา hashtable เท่านั้น ด้วยเหตุนี้จึงมีการรวบรวมขยะอย่างถูกต้องและไม่รั่วไหล (แต่ IANAJP) mindprod.com/jgloss/interned.html#GC
สิ่งที่ต้องทำคือใช้ HashSet ที่ไม่ถูกต้อง (หรือไม่มีอยู่จริง) hashCode()
หรือequals()
จากนั้นเพิ่ม "ซ้ำกัน" ต่อไป แทนที่จะเพิกเฉยการทำซ้ำตามที่ควรจะเป็นชุดจะเติบโตขึ้นเรื่อย ๆ และคุณจะไม่สามารถลบออกได้
หากคุณต้องการคีย์ / อิลิเมนต์ที่ไม่ดีเหล่านี้แขวนอยู่รอบ ๆ คุณสามารถใช้ฟิลด์คงที่เช่น
class BadKey {
// no hashCode or equals();
public final String key;
public BadKey(String key) { this.key = key; }
}
Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.
ด้านล่างจะมีกรณีที่ไม่ชัดเจนที่จาวารั่วนอกเหนือจากกรณีมาตรฐานของผู้ฟังที่ถูกลืมการอ้างอิงแบบคงที่กุญแจปลอม / ที่แก้ไขได้ในแฮชแมปหรือเพียงแค่เธรดที่ติดอยู่โดยไม่มีโอกาสสิ้นสุดวงจรชีวิต
File.deleteOnExit()
- รั่วสตริงเสมอ char[]
ดังนั้นภายหลังใช้ไม่ได้ ; @Daniel ถึงแม้จะไม่ต้องการลงคะแนนก็ตามฉันจะมุ่งเน้นที่หัวข้อเพื่อแสดงอันตรายของเธรดที่ไม่มีการจัดการส่วนใหญ่ไม่ต้องการแม้แต่จะแกว่ง
Runtime.addShutdownHook
และไม่ลบ ... และแม้แต่กับ removeShutdownHook เนื่องจากข้อผิดพลาดในคลาส ThreadGroup ที่เกี่ยวข้องกับเธรดที่ไม่ได้เริ่มต้นอาจไม่ได้รับการรวบรวมและรั่วไหลของ ThreadGroup ได้อย่างมีประสิทธิภาพ JGroup มีการรั่วไหลใน GossipRouter
การสร้าง แต่ไม่เริ่มต้น a Thread
จะอยู่ในหมวดหมู่เดียวกันกับด้านบน
การสร้างเธรดจะสืบทอดContextClassLoader
และAccessControlContext
รวมถึงThreadGroup
และInheritedThreadLocal
การอ้างอิงเหล่านั้นทั้งหมดเป็นการรั่วที่อาจเกิดขึ้นพร้อมกับคลาสทั้งหมดที่โหลดโดย classloader และการอ้างอิงแบบสแตติกทั้งหมดและ ja-ja เอฟเฟกต์สามารถมองเห็นได้โดยเฉพาะอย่างยิ่งกับกรอบ jucExecutor ทั้งหมดที่มีThreadFactory
อินเทอร์เฟซที่เรียบง่ายสุด ๆ นอกจากนี้ยังมีห้องสมุดจำนวนมากที่เริ่มต้นเธรดเมื่อมีการร้องขอ (มีวิธีที่นิยมในห้องสมุดมากเกินไป)
ThreadLocal
แคช; สิ่งเหล่านี้เป็นความชั่วร้ายในหลายกรณี ฉันแน่ใจว่าทุกคนเห็นแคชแบบง่าย ๆ โดยใช้ ThreadLocal เป็นข่าวดี: ถ้าเธรดยังคงดำเนินต่อไปมากกว่าที่คาดไว้ในบริบทของ ClassLoader ชีวิตมันเป็นการรั่วไหลเล็กน้อยที่บริสุทธิ์ อย่าใช้แคชของ ThreadLocal เว้นแต่จำเป็นจริงๆ
การเรียกใช้ThreadGroup.destroy()
เมื่อ ThreadGroup ไม่มีเธรดเอง แต่ยังคงเก็บ ThreadGroups ย่อยไว้ การรั่วไหลที่ไม่ดีที่จะป้องกัน ThreadGroup เพื่อลบออกจากพาเรนต์ แต่เด็ก ๆ ทั้งหมดจะไม่สามารถระบุได้
ใช้ WeakHashMap และค่า (ใน) อ้างอิงคีย์โดยตรง นี่เป็นสิ่งที่ยากที่จะหาโดยไม่ต้องมีกองขยะ ที่ใช้กับการขยายทั้งหมดWeak/SoftReference
ที่อาจเก็บการอ้างอิงที่ยากกลับไปยังวัตถุที่มีการป้องกัน
ใช้java.net.URL
กับโปรโตคอล HTTP (S) และโหลดทรัพยากรจาก (!) อันนี้เป็นพิเศษการKeepAliveCache
สร้างเธรดใหม่ในระบบ ThreadGroup รั่วไหลของ classloader บริบทของเธรดปัจจุบัน เธรดจะถูกสร้างขึ้นเมื่อมีการร้องขอครั้งแรกเมื่อไม่มีเธรดที่มีชีวิตอยู่ดังนั้นคุณอาจโชคดีหรือเพิ่งรั่ว การรั่วไหลได้รับการแก้ไขแล้วใน Java 7 และรหัสที่สร้างเธรดอย่างถูกต้องจะลบตัวโหลดคลาสบริบท มีอีกสองสามกรณี (เช่น ImageFetcher, แก้ไขแล้ว ) ของการสร้างเธรดที่คล้ายกัน
ใช้การInflaterInputStream
ส่งผ่านnew java.util.zip.Inflater()
ในตัวสร้าง ( PNGImageDecoder
ตัวอย่าง) และไม่เรียกend()
ใช้ตัวสร้างแรงลม ถ้าคุณส่งผ่านคอนสตรัคเตอร์เพียงแค่new
ไม่มีโอกาส ... และใช่การเรียกclose()
ใช้สตรีมไม่ได้ปิดตัวอินเทอร์รัปต์ถ้ามันผ่านด้วยตนเองเป็นพารามิเตอร์คอนสตรัคเตอร์ นี่ไม่ใช่การรั่วไหลที่แท้จริงเพราะมันจะถูกปล่อยโดย finalizer ... เมื่อมันเห็นว่าจำเป็น จนถึงขณะนั้นมันกินหน่วยความจำดั้งเดิมดังนั้นไม่ดีก็สามารถทำให้ Linux oom_killer ฆ่ากระบวนการด้วยการยกเว้นโทษ ปัญหาหลักคือการสรุปใน Java ไม่น่าเชื่อถือมากและ G1 ทำให้แย่ลงจนถึง 7.0.2 คุณธรรมของเรื่องราว: ปลดปล่อยทรัพยากรพื้นเมืองทันทีที่คุณทำได้ Finalizer นั้นยากจนเกินไป
java.util.zip.Deflater
กรณีเดียวกันกับ อันนี้แย่กว่ามากเนื่องจาก Deflater หิวในหน่วยความจำใน Java เช่นใช้ 15 บิต (สูงสุด) และ 8 ระดับหน่วยความจำ (9 คือสูงสุด) จัดสรรหน่วยความจำเนทีฟหลายร้อย KB โชคดีที่Deflater
ไม่ได้ใช้กันอย่างแพร่หลายและเพื่อความรู้ของฉัน JDK ไม่มีการใช้ผิดวิธี มักจะโทรend()
ถ้าคุณสร้างด้วยตนเองหรือDeflater
Inflater
ส่วนที่ดีที่สุดของสองครั้งล่าสุด: คุณไม่สามารถค้นหาได้ผ่านเครื่องมือทำโปรไฟล์ปกติ
(ฉันสามารถเพิ่มเวลาอีก wasters ฉันได้พบตามคำขอ)
ขอให้โชคดีและอยู่อย่างปลอดภัย การรั่วไหลเป็นสิ่งที่ชั่วร้าย!
Creating but not starting a Thread...
ใช่ฉันถูกกัดโดยคนนี้มาหลายศตวรรษแล้ว! (Java 1.3)
unstarted
จำนวน แต่ป้องกันกลุ่มเธรดจากการทำลาย (ความชั่วร้ายที่น้อยลง แต่ยังรั่ว)
ThreadGroup.destroy()
เมื่อ ThreadGroup ไม่มีเธรดเอง ... "เป็นจุดบกพร่องที่ไม่น่าเชื่อ ฉันกำลังไล่ล่าสิ่งนี้เป็นเวลาหลายชั่วโมงนำไปสู่ความหลงผิดเพราะการระบุเธรดใน GUI ควบคุมของฉันไม่แสดงอะไรเลย แต่กลุ่มเธรดและสันนิษฐานว่ากลุ่มลูกอย่างน้อยหนึ่งกลุ่มจะไม่หายไป
ตัวอย่างส่วนใหญ่ที่นี่ "ซับซ้อนเกินไป" พวกเขาเป็นกรณีขอบ ด้วยตัวอย่างเหล่านี้โปรแกรมเมอร์ทำผิดพลาด (เช่นไม่ต้องกำหนดค่าใหม่เท่ากับ / hashcode) หรือถูกกัดโดยมุมของ JVM / JAVA (โหลดคลาสที่มีสแตติก ... ) ฉันคิดว่านั่นไม่ใช่ประเภทของตัวอย่างที่ผู้สัมภาษณ์ต้องการหรือแม้แต่กรณีทั่วไป
แต่มีกรณีที่หน่วยความจำรั่วง่ายกว่า ตัวรวบรวมขยะจะปลดปล่อยสิ่งที่ไม่ได้อ้างอิงอีกต่อไป เราในฐานะนักพัฒนา Java ไม่สนใจเรื่องความจำ เราจัดสรรเมื่อจำเป็นและปล่อยให้เป็นอิสระโดยอัตโนมัติ ละเอียด.
แต่แอปพลิเคชั่นที่มีอายุการใช้งานยาวนานมักจะมีสถานะที่ใช้ร่วมกัน มันสามารถเป็นอะไรก็ได้สถิตยศาสตร์ซิงเกิล ... บ่อยครั้งที่แอพพลิเคชั่นที่ไม่สำคัญมีแนวโน้มที่จะสร้างกราฟวัตถุที่ซับซ้อน เพียงลืมที่จะตั้งค่าอ้างอิงเป็นโมฆะหรือบ่อยครั้งที่ลืมลบวัตถุหนึ่งจากคอลเลกชันก็เพียงพอที่จะทำให้หน่วยความจำรั่ว
แน่นอนว่าผู้ฟังทุกประเภท (เช่นผู้ฟัง UI) แคชหรือสถานะการแบ่งปันที่ยาวนานมักจะทำให้หน่วยความจำรั่วหากไม่ได้รับการจัดการอย่างเหมาะสม สิ่งที่ต้องทำความเข้าใจคือกรณีนี้ไม่ใช่มุมของ Java หรือมีปัญหากับตัวรวบรวมขยะ มันเป็นปัญหาการออกแบบ เราออกแบบให้เราเพิ่มผู้ฟังไปยังวัตถุที่มีอายุการใช้งานยาวนาน แต่เราไม่ได้ลบผู้ฟังออกเมื่อไม่ต้องการใช้อีกต่อไป เราแคชวัตถุ แต่เราไม่มีกลยุทธ์ในการลบวัตถุออกจากแคช
เราอาจมีกราฟที่ซับซ้อนซึ่งเก็บสถานะก่อนหน้านี้ที่ต้องการโดยการคำนวณ แต่สถานะก่อนหน้านั้นเชื่อมโยงกับสถานะก่อนหน้านี้เป็นต้น
เหมือนที่เราต้องปิดการเชื่อมต่อ SQL หรือไฟล์ เราจำเป็นต้องตั้งค่าการอ้างอิงที่เหมาะสมเป็นโมฆะและลบองค์ประกอบออกจากคอลเลกชัน เราจะมีกลยุทธ์การแคชที่เหมาะสม (ขนาดหน่วยความจำสูงสุดจำนวนองค์ประกอบหรือตัวนับ) วัตถุทั้งหมดที่อนุญาตให้ผู้ฟังได้รับการแจ้งเตือนต้องมีทั้งวิธีการ addListener และ removeListener และเมื่อไม่ใช้ตัวแจ้งเตือนเหล่านี้อีกต่อไปพวกเขาจะต้องล้างรายชื่อผู้ฟัง
การรั่วไหลของหน่วยความจำนั้นเป็นไปได้อย่างแท้จริงและสามารถคาดเดาได้อย่างสมบูรณ์แบบ ไม่จำเป็นต้องใช้คุณสมบัติพิเศษภาษาหรือมุมกรณี หน่วยความจำรั่วมีทั้งตัวบ่งชี้ว่ามีบางอย่างขาดหายไปหรือแม้แต่ปัญหาการออกแบบ
WeakReference
) ที่มีอยู่ จากที่หนึ่งไปยังอีก หากการอ้างอิงวัตถุมีบิตสำรองอาจเป็นประโยชน์หากมีตัวบ่งชี้ "ใส่ใจเกี่ยวกับเป้าหมาย" ...
PhantomReference
) หากพบว่าวัตถุนั้นไม่มีใครที่ห่วงใย WeakReference
ค่อนข้างใกล้เข้ามา แต่ต้องถูกแปลงเป็นข้อมูลอ้างอิงที่แข็งแกร่งก่อนที่จะสามารถใช้งานได้ หากรอบ GC เกิดขึ้นในขณะที่มีการอ้างอิงที่รัดกุมเป้าหมายจะถือว่ามีประโยชน์
คำตอบขึ้นอยู่กับสิ่งที่ผู้สัมภาษณ์คิดว่าพวกเขาถาม
เป็นไปได้หรือไม่ที่จะทำให้ Java รั่ว? แน่นอนมันเป็นและมีตัวอย่างมากมายในคำตอบอื่น ๆ
แต่มีเมตาคำถามหลายคำถามที่อาจถูกถามอยู่
ฉันกำลังอ่านเมตาคำถามของคุณว่า "มีคำตอบอะไรที่ฉันสามารถใช้ในสถานการณ์สัมภาษณ์นี้" ดังนั้นฉันจะมุ่งเน้นไปที่ทักษะการสัมภาษณ์แทน Java ฉันเชื่อว่าคุณมีแนวโน้มที่จะทำซ้ำสถานการณ์ที่ไม่รู้คำตอบของคำถามในการสัมภาษณ์มากกว่าที่คุณจะต้องรู้วิธีทำให้ Java รั่วไหล ดังนั้นหวังว่านี่จะช่วยได้
หนึ่งในทักษะที่สำคัญที่สุดที่คุณสามารถพัฒนาสำหรับการสัมภาษณ์คือการเรียนรู้ที่จะฟังคำถามและทำงานร่วมกับผู้สัมภาษณ์เพื่อดึงความตั้งใจของพวกเขา สิ่งนี้ไม่เพียงช่วยให้คุณตอบคำถามในแบบที่พวกเขาต้องการ แต่ยังแสดงให้เห็นว่าคุณมีทักษะการสื่อสารที่สำคัญ และเมื่อพูดถึงทางเลือกระหว่างนักพัฒนาที่มีความสามารถหลายคนเท่ากันฉันจะจ้างคนที่ฟังคิดและเข้าใจก่อนที่พวกเขาจะตอบทุกครั้ง
ต่อไปนี้เป็นตัวอย่างที่ไม่มีจุดหมายสวยถ้าคุณไม่เข้าใจJDBC หรืออย่างน้อยวิธี JDBC คาดว่าจะมีนักพัฒนาที่จะใกล้Connection
, Statement
และอินสแตนซ์ก่อนที่จะทิ้งพวกเขาหรือการสูญเสียการอ้างอิงไปยังพวกเขาแทนที่จะอาศัยในการดำเนินการResultSet
finalize
void doWork()
{
try
{
Connection conn = ConnectionFactory.getConnection();
PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
ResultSet rs = stmt.executeQuery();
while(rs.hasNext())
{
... process the result set
}
}
catch(SQLException sqlEx)
{
log(sqlEx);
}
}
ปัญหาข้างต้นคือConnection
วัตถุไม่ได้ถูกปิดและด้วยเหตุนี้การเชื่อมต่อทางกายภาพจะยังคงเปิดอยู่จนกว่าตัวรวบรวมขยะจะมาและเห็นว่ามันไม่สามารถเข้าถึงได้ GC จะเรียกใช้finalize
เมธอด แต่มีไดร์เวอร์ JDBC ที่ไม่ได้ใช้finalize
อย่างน้อยก็ไม่ได้ใช้วิธีเดียวกันกับที่Connection.close
ใช้ พฤติกรรมที่เกิดขึ้นคือแม้ว่าหน่วยความจำจะถูกเรียกคืนเนื่องจากมีการรวบรวมวัตถุที่ไม่สามารถเข้าถึงได้ทรัพยากร (รวมถึงหน่วยความจำ) ที่เชื่อมโยงกับConnection
วัตถุนั้นอาจไม่สามารถเรียกคืนได้
ในเหตุการณ์ดังกล่าวที่ได้Connection
's finalize
วิธีการทำทุกอย่างไม่สะอาดหนึ่งจริงๆแล้วอาจจะพบว่าการเชื่อมต่อทางกายภาพกับเซิร์ฟเวอร์ฐานข้อมูลจะมีอายุรอบการเก็บขยะหลายจนกว่าเซิร์ฟเวอร์ฐานข้อมูลในที่สุดตัวเลขออกมาว่าการเชื่อมต่อไม่ได้มีชีวิตอยู่ (ถ้ามัน ทำ) และควรปิด
แม้ว่าไดรเวอร์ JDBC จะถูกนำไปใช้finalize
มันก็เป็นไปได้สำหรับข้อยกเว้นที่จะถูกโยนทิ้งในระหว่างการสรุป พฤติกรรมที่เกิดขึ้นคือหน่วยความจำใด ๆ ที่เชื่อมโยงกับวัตถุ "อยู่เฉยๆ" ในขณะนี้จะไม่ถูกเรียกคืนเนื่องจากfinalize
มีการรับประกันว่าจะเรียกใช้เพียงครั้งเดียว
สถานการณ์ด้านบนของการพบข้อยกเว้นในระหว่างการสรุปวัตถุมีความสัมพันธ์กับสถานการณ์อื่นที่อาจนำไปสู่การรั่วไหลของหน่วยความจำ - การฟื้นคืนชีพของวัตถุ การฟื้นคืนชีพของวัตถุมักทำโดยเจตนาโดยการสร้างการอ้างอิงที่แข็งแกร่งไปยังวัตถุจากการสรุปแล้วจากวัตถุอื่น เมื่อการคืนชีพของวัตถุถูกนำไปใช้ในทางที่ผิดจะนำไปสู่การรั่วไหลของหน่วยความจำร่วมกับแหล่งหน่วยความจำรั่ว
มีตัวอย่างอีกมากมายที่คุณสามารถคิดในใจเหมือน
List
อินสแตนซ์ที่คุณเพิ่มไปยังรายการเท่านั้นและไม่ลบออกจากมัน (แม้ว่าคุณควรจะกำจัดองค์ประกอบที่คุณไม่ต้องการอีกต่อไป) หรือSocket
s หรือFile
s แต่ไม่ปิดเมื่อไม่ต้องการใช้อีกต่อไป (คล้ายกับตัวอย่างด้านบนที่เกี่ยวข้องกับConnection
คลาส)Connection.close
บล็อกสุดท้ายของการเรียก SQL ทั้งหมดของฉัน เพื่อความสนุกสนานเป็นพิเศษฉันเรียกใช้โพรซีเดอร์ที่เก็บของ Oracle มานานซึ่งต้องการการล็อกทางฝั่ง Java เพื่อป้องกันการโทรไปยังฐานข้อมูลมากเกินไป
อาจเป็นหนึ่งในตัวอย่างที่ง่ายที่สุดของการรั่วไหลของหน่วยความจำที่มีศักยภาพและวิธีการหลีกเลี่ยงมันคือการใช้ ArrayList.remove (int):
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index + 1, elementData, index,
numMoved);
elementData[--size] = null; // (!) Let gc do its work
return oldValue;
}
หากคุณใช้งานด้วยตัวคุณเองคุณจะคิดว่าจะล้างองค์ประกอบอาร์เรย์ที่ไม่ได้ใช้อีกต่อไปelementData[--size] = null
หรือไม่? การอ้างอิงนั้นอาจทำให้วัตถุขนาดใหญ่มีชีวิตอยู่ ...
เมื่อใดก็ตามที่คุณอ้างอิงไปยังวัตถุที่คุณไม่ต้องการให้หน่วยความจำรั่วไหลอีกต่อไป ดูที่การจัดการการรั่วไหลของหน่วยความจำในโปรแกรม Javaสำหรับตัวอย่างของการรั่วไหลของหน่วยความจำใน Java และสิ่งที่คุณสามารถทำได้
...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language.
ฉันไม่เห็นว่าคุณวาดข้อสรุปที่ มีวิธีที่น้อยกว่าในการสร้างหน่วยความจำรั่วใน Java โดยคำจำกัดความใด ๆ มันยังคงเป็นคำถามที่ถูกต้องแน่นอน
คุณสามารถทำให้หน่วยความจำรั่วด้วยคลาสsun.misc.Unsafe ในความเป็นจริงคลาสเซอร์วิสนี้ถูกใช้ในคลาสมาตรฐานที่แตกต่างกัน (ตัวอย่างเช่นในคลาสjava.nio ) คุณไม่สามารถสร้างตัวอย่างของการเรียนนี้โดยตรงแต่คุณอาจใช้สะท้อนที่จะทำ
โค้ดไม่ได้รวบรวมใน Eclipse IDE - รวบรวมโดยใช้คำสั่ง javac
(ในระหว่างการรวบรวมคุณจะได้รับคำเตือน)
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;
public class TestUnsafe {
public static void main(String[] args) throws Exception{
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field f = unsafeClass.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
System.out.print("4..3..2..1...");
try
{
for(;;)
unsafe.allocateMemory(1024*1024);
} catch(Error e) {
System.out.println("Boom :)");
e.printStackTrace();
}
}
}
ฉันสามารถคัดลอกคำตอบของฉันจากที่นี่: วิธีที่ง่ายที่สุดที่จะทำให้หน่วยความจำรั่วใน Java?
"การรั่วไหลของหน่วยความจำในวิทยาศาสตร์คอมพิวเตอร์ (หรือการรั่วไหลในบริบทนี้) เกิดขึ้นเมื่อโปรแกรมคอมพิวเตอร์ใช้หน่วยความจำ แต่ไม่สามารถปล่อยมันกลับสู่ระบบปฏิบัติการได้" (วิกิพีเดีย)
คำตอบง่ายๆคือ: คุณทำไม่ได้ Java ทำการจัดการหน่วยความจำอัตโนมัติและจะเพิ่มทรัพยากรที่ไม่จำเป็นสำหรับคุณ คุณไม่สามารถหยุดสิ่งนี้ได้ มันจะสามารถปล่อยทรัพยากรได้เสมอ ในโปรแกรมที่มีการจัดการหน่วยความจำด้วยตนเองจะแตกต่างกัน คุณไม่สามารถรับหน่วยความจำใน C โดยใช้ malloc () ในการเพิ่มหน่วยความจำคุณต้องมีตัวชี้ที่ malloc ส่งคืนและโทรฟรี () แต่ถ้าคุณไม่มีตัวชี้อีกต่อไป (เขียนทับหรือเกินอายุการใช้งานแล้ว) คุณน่าเสียดายที่ไม่สามารถเพิ่มหน่วยความจำนี้ได้และทำให้คุณมีหน่วยความจำรั่ว
คำตอบอื่น ๆ ทั้งหมดที่อยู่ในคำจำกัดความของฉันไม่รั่วหน่วยความจำจริงๆ พวกเขาตั้งเป้าหมายในการเติมความทรงจำด้วยสิ่งไร้จุดหมายอย่างรวดเร็ว แต่เมื่อใดก็ตามที่คุณยังคงสามารถตรวจสอบวัตถุที่คุณสร้างขึ้นและทำให้หน่วยความจำว่าง -> ไม่รั่วไหล คำตอบของ acconradนั้นค่อนข้างใกล้เคียงกับที่ฉันต้องยอมรับเพราะวิธีการแก้ปัญหาของเขานั้นมีประสิทธิภาพเพียงแค่ "พัง" ตัวเก็บขยะโดยบังคับให้มันวนซ้ำไม่รู้จบ)
คำตอบที่ยาวคือ: คุณสามารถทำให้หน่วยความจำรั่วโดยการเขียนไลบรารีสำหรับ Java โดยใช้ JNI ซึ่งสามารถจัดการหน่วยความจำด้วยตนเองและทำให้หน่วยความจำรั่ว ถ้าคุณเรียกไลบรารีนี้กระบวนการจาวาของคุณจะทำให้หน่วยความจำรั่วไหล หรือคุณสามารถมีข้อบกพร่องใน JVM เพื่อให้ JVM สูญเสียความจำ อาจมีข้อบกพร่องใน JVM อาจมีบางคนที่รู้จักกันเนื่องจากการรวบรวมขยะไม่ใช่เรื่องเล็กน้อย แต่ก็ยังมีข้อบกพร่องอยู่ โดยการออกแบบนี้เป็นไปไม่ได้ คุณอาจจะขอรหัสจาวาบางตัวที่ได้รับผลกระทบจากข้อผิดพลาดดังกล่าว ขออภัยฉันไม่รู้จักและอาจไม่เป็นข้อผิดพลาดอีกต่อไปในรุ่น Java ถัดไป
นี่คือหนึ่งง่าย / อุบาทว์ผ่านhttp://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29
public class StringLeaker
{
private final String muchSmallerString;
public StringLeaker()
{
// Imagine the whole Declaration of Independence here
String veryLongString = "We hold these truths to be self-evident...";
// The substring here maintains a reference to the internal char[]
// representation of the original string.
this.muchSmallerString = veryLongString.substring(0, 1);
}
}
เนื่องจากสตริงย่อยอ้างอิงถึงการแสดงภายในของสตริงเดิมซึ่งยาวกว่ามากสตริงดั้งเดิมจึงอยู่ในหน่วยความจำ ดังนั้นตราบใดที่คุณมี StringLeaker อยู่ในการเล่นคุณจะมีสตริงเดิมทั้งหมดอยู่ในหน่วยความจำเช่นกันแม้ว่าคุณอาจคิดว่าคุณกำลังถือสายอักขระตัวเดียวอยู่
วิธีหลีกเลี่ยงการจัดเก็บการอ้างอิงที่ไม่พึงประสงค์ไปยังสตริงต้นฉบับคือการทำสิ่งนี้:
...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...
เพื่อเพิ่มความไม่ดีคุณอาจ.intern()
ใช้สตริงย่อยด้วย:
...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...
การทำเช่นนี้จะเก็บทั้งสตริงยาวดั้งเดิมและซับสตริงที่ได้รับในหน่วยความจำแม้ว่าหลังจากอินสแตนซ์ StringLeaker ถูกยกเลิกไปแล้ว
muchSmallerString
ถูกปล่อยให้ว่าง (เนื่องจากStringLeaker
วัตถุถูกทำลาย) สายยาวจะถูกปล่อยเช่นกัน สิ่งที่ฉันเรียกหน่วยความจำรั่วคือหน่วยความจำที่ไม่เคยได้รับใน JVM นี้ this.muchSmallerString=new String(this.muchSmallerString)
แต่คุณได้แสดงให้เห็นตัวเองว่าจะเพิ่มหน่วยความจำ: ด้วยการรั่วไหลของหน่วยความจำจริงไม่มีอะไรที่คุณสามารถทำได้
intern
กรณีที่ไม่ใช่"หน่วยความจำแปลกใจ" มากกว่า "หน่วยความจำรั่ว" .intern()
ถึงแม้ว่าสตริงย่อยจะสร้างสถานการณ์ที่การอ้างอิงไปยังสตริงที่ยาวกว่านั้นถูกสงวนไว้และไม่สามารถทำให้เป็นอิสระได้
ตัวอย่างทั่วไปของสิ่งนี้ในโค้ด GUI คือเมื่อสร้างวิดเจ็ต / ส่วนประกอบและเพิ่มผู้ฟังไปยังวัตถุที่กำหนดขอบเขตแบบสแตติก / แอ็พพลิเคชันบางส่วนจากนั้นไม่ลบตัวฟังเมื่อวิดเจ็ตถูกทำลาย ไม่เพียง แต่คุณจะมีหน่วยความจำรั่ว แต่ยังมีการแสดงที่มีประสิทธิภาพเช่นเมื่อสิ่งที่คุณกำลังฟังไฟเหตุการณ์ผู้ฟังเก่าของคุณทั้งหมดจะถูกเรียกเช่นกัน
ใช้เว็บแอปพลิเคชันใด ๆ ที่ทำงานอยู่ในคอนเทนเนอร์ servlet (Tomcat, Jetty, Glassfish หรืออะไรก็ตาม ... ) ปรับใช้แอปใหม่ 10 หรือ 20 ครั้งในหนึ่งแถว (อาจเพียงพอที่จะแตะ WAR ในไดเรกทอรี autodeploy ของเซิร์ฟเวอร์
นอกเสียจากว่าใครได้ทำการทดสอบสิ่งนี้จริง ๆ โอกาสสูงที่คุณจะได้รับ OutOfMemoryError หลังจากการปรับใช้สองครั้งเนื่องจากแอปพลิเคชันไม่ได้ดูแลการล้างข้อมูลหลังจากตัวเอง คุณอาจพบข้อบกพร่องในเซิร์ฟเวอร์ของคุณด้วยการทดสอบนี้
ปัญหาคืออายุการใช้งานของคอนเทนเนอร์นานกว่าอายุการใช้งานของแอปพลิเคชันของคุณ คุณต้องตรวจสอบให้แน่ใจว่าการอ้างอิงทั้งหมดที่คอนเทนเนอร์อาจมีกับวัตถุหรือคลาสของแอปพลิเคชันของคุณอาจถูกรวบรวมขยะ
หากมีการอ้างอิงเพียงหนึ่งเดียวที่รอดชีวิตจากการเลิกใช้งานแอพพลิเคชั่นเว็บของคุณ classloader ที่เกี่ยวข้องและด้วยเหตุนี้คลาสทั้งหมดของแอปพลิเคชันเว็บของคุณจะไม่สามารถรวบรวมขยะได้
เธรดที่เริ่มต้นโดยแอปพลิเคชันของคุณ, ตัวแปร ThreadLocal, ตัวบันทึกการทำงานเป็นข้อสงสัยบางประการที่ทำให้เกิดการรั่วไหลของ classloader
อาจจะใช้รหัสพื้นเมืองภายนอกผ่าน JNI?
ด้วยจาวาบริสุทธิ์นั้นแทบจะเป็นไปไม่ได้เลย
แต่นั่นเป็นเรื่องเกี่ยวกับการรั่วไหลของหน่วยความจำแบบ "มาตรฐาน" เมื่อคุณไม่สามารถเข้าถึงหน่วยความจำได้อีกต่อไป แต่แอปพลิเคชันยังคงเป็นเจ้าของ คุณสามารถเก็บการอ้างอิงไปยังวัตถุที่ไม่ได้ใช้แทนหรือเปิดสตรีมโดยไม่ต้องปิดมันในภายหลัง
ฉันมี "หน่วยความจำรั่ว" ที่ดีเกี่ยวกับ PermGen และ XML ในการแยกวิเคราะห์หนึ่งครั้ง ตัวแยกวิเคราะห์ XML ที่เราใช้ (ฉันจำไม่ได้ว่าอันไหน) ทำ String.intern () ในชื่อแท็กเพื่อทำการเปรียบเทียบได้เร็วขึ้น หนึ่งในลูกค้าของเรามีความคิดที่ดีในการจัดเก็บค่าข้อมูลไม่ได้อยู่ในแอตทริบิวต์ XML หรือข้อความ แต่เป็น tagnames ดังนั้นเราจึงมีเอกสารเช่น:
<data>
<1>bla</1>
<2>foo</>
...
</data>
ในความเป็นจริงพวกเขาไม่ได้ใช้ตัวเลข แต่เป็นข้อความที่ยาวกว่า (ประมาณ 20 ตัวอักษร) ซึ่งไม่ซ้ำใครและมีอัตราการเข้าใช้วันละ 10-15 ล้าน นั่นทำให้ขยะ 200 MB ต่อวันซึ่งไม่จำเป็นอีกต่อไปและไม่เคย GCed (เนื่องจากอยู่ใน PermGen) เราได้ตั้งค่า permgen ไว้ที่ 512 MB ดังนั้นจึงใช้เวลาประมาณสองวันในการยกเว้นหน่วยความจำไม่เพียงพอ (OOME) เพื่อให้มาถึง ...
หน่วยความจำรั่วคืออะไร:
ตัวอย่างทั่วไป:
แคชของวัตถุเป็นจุดเริ่มต้นที่ดีในการเลอะสิ่งต่าง ๆ
private static final Map<String, Info> myCache = new HashMap<>();
public void getInfo(String key)
{
// uses cache
Info info = myCache.get(key);
if (info != null) return info;
// if it's not in cache, then fetch it from the database
info = Database.fetch(key);
if (info == null) return null;
// and store it in the cache
myCache.put(key, info);
return info;
}
แคชของคุณเติบโตและโตขึ้น และในไม่ช้าฐานข้อมูลทั้งหมดจะถูกดูดเข้าสู่หน่วยความจำ การออกแบบที่ดีกว่าใช้ LRUMap (เก็บเฉพาะวัตถุที่เพิ่งใช้ในแคช)
แน่นอนคุณสามารถทำให้สิ่งต่าง ๆ มีความซับซ้อนมากขึ้น:
มักจะเกิดอะไรขึ้น:
หากวัตถุข้อมูลนี้มีการอ้างอิงถึงวัตถุอื่น ๆ ซึ่งมีการอ้างอิงไปยังวัตถุอื่นอีกครั้ง ในแบบที่คุณอาจคิดว่านี่เป็นหน่วยความจำรั่วบางส่วน (เกิดจากการออกแบบที่ไม่ดี)
ฉันคิดว่ามันน่าสนใจที่ไม่มีใครใช้ตัวอย่างภายในของชั้นเรียน หากคุณมีชั้นเรียนภายใน มันเก็บรักษาการอ้างอิงถึงคลาสที่มีอยู่โดยเนื้อแท้ แน่นอนว่าไม่ใช่เทคนิคการรั่วไหลของหน่วยความจำเพราะ Java WILL ทำความสะอาดมันในที่สุด แต่สิ่งนี้อาจทำให้คลาสหยุดทำงานนานกว่าที่คาดไว้
public class Example1 {
public Example2 getNewExample2() {
return this.new Example2();
}
public class Example2 {
public Example2() {}
}
}
ตอนนี้ถ้าคุณเรียกใช้ตัวอย่าง 1 และรับตัวอย่างที่ 2 ทิ้งตัวอย่างที่ 1 คุณจะยังคงมีลิงค์ไปยังวัตถุตัวอย่าง 1
public class Referencer {
public static Example2 GetAnExample2() {
Example1 ex = new Example1();
return ex.getNewExample2();
}
public static void main(String[] args) {
Example2 ex = Referencer.GetAnExample2();
// As long as ex is reachable; Example1 will always remain in memory.
}
}
ฉันเคยได้ยินข่าวลือด้วยว่าถ้าคุณมีตัวแปรที่มีอยู่นานเกินระยะเวลาที่กำหนด Java สันนิษฐานว่ามันจะมีอยู่เสมอและจะไม่พยายามล้างหากไม่สามารถเข้าถึงได้ในรหัสอีกต่อไป แต่นั่นไม่ได้ยืนยันอย่างสมบูรณ์
ฉันเพิ่งพบสถานการณ์การรั่วไหลของหน่วยความจำที่เกิดขึ้นในทางโดย log4j
Log4j มีกลไกนี้เรียกว่าNested Diagnostic Context (NDC) ซึ่งเป็นเครื่องมือในการแยกแยะเอาท์พุทบันทึก interleaved จากแหล่งต่าง ๆ ความละเอียดที่ NDC ทำงานเป็นเธรดดังนั้นจึงแยกความแตกต่างของเอาต์พุตบันทึกจากเธรดที่ต่างกันแยกกัน
เพื่อจัดเก็บแท็กเฉพาะของเธรดคลาส NDC ของ log4j ใช้ Hashtable ซึ่งคีย์โดยวัตถุเธรดเอง (ซึ่งตรงกันข้ามกับที่กล่าวถึง id ของเธรด) และจนถึงแท็ก NDC ยังคงอยู่ในหน่วยความจำวัตถุทั้งหมดที่อยู่ในเธรด วัตถุยังอยู่ในหน่วยความจำ ในเว็บแอปพลิเคชันของเราเราใช้ NDC เพื่อแท็กล็อกเอาต์เอาต์พร้อมกับรหัสคำขอเพื่อแยกบันทึกจากคำขอเดียวแยกกัน คอนเทนเนอร์ที่เชื่อมโยงแท็ก NDC กับเธรดจะลบออกในขณะที่ส่งคืนการตอบกลับจากคำขอ ปัญหาเกิดขึ้นเมื่อระหว่างการประมวลผลการร้องขอเธรดลูกได้รับการวางไข่บางอย่างเช่นรหัสต่อไปนี้:
pubclic class RequestProcessor {
private static final Logger logger = Logger.getLogger(RequestProcessor.class);
public void doSomething() {
....
final List<String> hugeList = new ArrayList<String>(10000);
new Thread() {
public void run() {
logger.info("Child thread spawned")
for(String s:hugeList) {
....
}
}
}.start();
}
}
ดังนั้นบริบท NDC จึงเชื่อมโยงกับเธรดอินไลน์ที่เกิดขึ้น วัตถุเธรดที่เป็นกุญแจสำคัญสำหรับบริบท NDC นี้คือเธรดแบบอินไลน์ซึ่งมีวัตถุ hugeList แขวนอยู่ ดังนั้นแม้ว่าเธรดจะทำสิ่งที่ทำเสร็จแล้วการอ้างอิงไปยัง hugeList นั้นยังคงมีชีวิตอยู่โดยบริบท NDC Hastable จึงทำให้หน่วยความจำรั่ว
ผู้สัมภาษณ์อาจมองหาการอ้างอิงแบบวงกลมเช่นรหัสด้านล่าง (ซึ่งบังเอิญมีเพียงหน่วยความจำรั่วใน JVM เก่ามากที่ใช้การนับการอ้างอิง แต่เป็นคำถามที่ค่อนข้างคลุมเครือดังนั้นจึงเป็นโอกาสสำคัญที่คุณจะได้แสดงความเข้าใจเกี่ยวกับการจัดการหน่วยความจำ JVM
class A {
B bRef;
}
class B {
A aRef;
}
public class Main {
public static void main(String args[]) {
A myA = new A();
B myB = new B();
myA.bRef = myB;
myB.aRef = myA;
myA=null;
myB=null;
/* at this point, there is no access to the myA and myB objects, */
/* even though both objects still have active references. */
} /* main */
}
จากนั้นคุณสามารถอธิบายได้ว่าด้วยการนับการอ้างอิงรหัสด้านบนจะทำให้หน่วยความจำรั่ว แต่ JVM ที่ทันสมัยส่วนใหญ่ไม่ได้ใช้การนับการอ้างอิงอีกต่อไปส่วนใหญ่ใช้ตัวเก็บกวาดขยะซึ่งในความเป็นจริงจะรวบรวมหน่วยความจำนี้
ถัดไปคุณอาจอธิบายการสร้างวัตถุที่มีทรัพยากรดั้งเดิมอยู่เช่นนี้
public class Main {
public static void main(String args[]) {
Socket s = new Socket(InetAddress.getByName("google.com"),80);
s=null;
/* at this point, because you didn't close the socket properly, */
/* you have a leak of a native descriptor, which uses memory. */
}
}
จากนั้นคุณสามารถอธิบายได้ว่านี่เป็นเทคนิคการรั่วไหลของหน่วยความจำ แต่จริงๆแล้วการรั่วไหลนั้นเกิดจากรหัสเนทีฟใน JVM ที่จัดสรรทรัพยากรดั้งเดิมไว้ซึ่งไม่ได้รับการสนับสนุนจากโค้ด Java ของคุณ
ในตอนท้ายของวันด้วย JVM ที่ทันสมัยคุณต้องเขียนโค้ด Java บางส่วนที่จัดสรรทรัพยากรดั้งเดิมนอกขอบเขตปกติของการรับรู้ของ JVM
ทุกคนลืมเส้นทางของรหัสเนทีฟเสมอ นี่คือสูตรง่ายๆสำหรับการรั่วไหล:
malloc
ในวิธีพื้นเมืองโทร free
อย่าเรียกโปรดจำไว้ว่าการจัดสรรหน่วยความจำในรหัสเนทีฟมาจากฮีป JVM
สร้างแผนที่แบบคงที่และเพิ่มการอ้างอิงอย่างหนักต่อไป สิ่งเหล่านั้นจะไม่เป็น GC'd
public class Leaker {
private static final Map<String, Object> CACHE = new HashMap<String, Object>();
// Keep adding until failure.
public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}
คุณสามารถสร้างการรั่วไหลของหน่วยความจำที่กำลังเคลื่อนที่ได้โดยการสร้างอินสแตนซ์ใหม่ของคลาสในวิธีการสรุปของคลาสนั้น คะแนนโบนัสหาก finalizer สร้างหลายอินสแตนซ์ ต่อไปนี้เป็นโปรแกรมอย่างง่ายที่ปล่อยให้กองทั้งหมดในบางครั้งระหว่างสองสามวินาทีและไม่กี่นาทีขึ้นอยู่กับขนาดฮีปของคุณ:
class Leakee {
public void check() {
if (depth > 2) {
Leaker.done();
}
}
private int depth;
public Leakee(int d) {
depth = d;
}
protected void finalize() {
new Leakee(depth + 1).check();
new Leakee(depth + 1).check();
}
}
public class Leaker {
private static boolean makeMore = true;
public static void done() {
makeMore = false;
}
public static void main(String[] args) throws InterruptedException {
// make a bunch of them until the garbage collector gets active
while (makeMore) {
new Leakee(0).check();
}
// sit back and watch the finalizers chew through memory
while (true) {
Thread.sleep(1000);
System.out.println("memory=" +
Runtime.getRuntime().freeMemory() + " / " +
Runtime.getRuntime().totalMemory());
}
}
}
ฉันยังไม่คิดว่าจะมีใครพูดถึงสิ่งนี้: คุณสามารถรื้อฟื้นวัตถุโดยการแทนที่ขั้นตอนสุดท้าย () วิธีการที่จะจบ () เก็บการอ้างอิงของที่นี้ ตัวรวบรวมขยะจะถูกเรียกเพียงครั้งเดียวบนวัตถุดังนั้นหลังจากนั้นวัตถุจะไม่ถูกทำลาย
finalize()
จะไม่ถูกเรียกใช้ แต่วัตถุจะถูกรวบรวมเมื่อไม่มีการอ้างอิงเพิ่มเติม ตัวรวบรวมขยะไม่ได้ถูกเรียกว่า 'อย่างใดอย่างหนึ่ง
finalize()
วิธีการนี้สามารถเรียกได้เพียงครั้งเดียวโดย JVM แต่นี่ไม่ได้หมายความว่าจะไม่สามารถรวบรวมขยะซ้ำได้หากวัตถุนั้นได้รับการฟื้นคืนชีพแล้วจึงถูกปฏิเสธอีกครั้ง หากมีรหัสการปิดทรัพยากรในfinalize()
วิธีการรหัสนี้จะไม่ได้รับการเรียกใช้อีกครั้งนี้อาจทำให้หน่วยความจำรั่ว
ฉันเจอการรั่วไหลของทรัพยากรที่ลึกซึ้งยิ่งขึ้นเมื่อเร็ว ๆ นี้ เราเปิดทรัพยากรผ่าน getResourceAsStream ของคลาสโหลดเดอร์และเกิดขึ้นว่าการจัดการอินพุตสตรีมไม่ได้ถูกปิด
อืมคุณอาจพูดว่าช่างเป็นคนงี่เง่า
สิ่งที่ทำให้สิ่งนี้น่าสนใจก็คือ: ด้วยวิธีนี้คุณสามารถรั่วไหลหน่วยความจำฮีปของกระบวนการพื้นฐานมากกว่าจากฮีปของ JVM
สิ่งที่คุณต้องมีคือไฟล์ jar ที่มีไฟล์อยู่ข้างในซึ่งจะถูกอ้างอิงจากโค้ด Java ไฟล์ jar ที่ใหญ่กว่าหน่วยความจำที่เร็วกว่าจะได้รับการจัดสรร
คุณสามารถสร้างไหอย่างง่ายดายด้วยคลาสต่อไปนี้:
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class BigJarCreator {
public static void main(String[] args) throws IOException {
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
zos.putNextEntry(new ZipEntry("resource.txt"));
zos.write("not too much in here".getBytes());
zos.closeEntry();
zos.putNextEntry(new ZipEntry("largeFile.out"));
for (int i=0 ; i<10000000 ; i++) {
zos.write((int) (Math.round(Math.random()*100)+20));
}
zos.closeEntry();
zos.close();
}
}
เพียงวางลงในไฟล์ชื่อ BigJarCreator.java รวบรวมและเรียกใช้จากบรรทัดคำสั่ง:
javac BigJarCreator.java
java -cp . BigJarCreator
และอื่น ๆ : คุณพบไฟล์เก็บถาวร jar ในไดเรกทอรีการทำงานปัจจุบันของคุณโดยมีไฟล์อยู่สองไฟล์
มาสร้างคลาสที่สองกันเถอะ:
public class MemLeak {
public static void main(String[] args) throws InterruptedException {
int ITERATIONS=100000;
for (int i=0 ; i<ITERATIONS ; i++) {
MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
}
System.out.println("finished creation of streams, now waiting to be killed");
Thread.sleep(Long.MAX_VALUE);
}
}
โดยทั่วไปคลาสนี้ไม่ทำอะไรเลย แต่สร้างออบเจ็กต์ InputStream ที่ไม่อ้างอิง วัตถุเหล่านั้นจะถูกเก็บรวบรวมขยะทันทีและไม่ส่งผลต่อขนาดของกอง มันเป็นสิ่งสำคัญสำหรับตัวอย่างของเราในการโหลดทรัพยากรที่มีอยู่จากไฟล์ jar และขนาดมีความสำคัญที่นี่!
หากคุณสงสัยลองรวบรวมและเริ่มชั้นเรียนด้านบน แต่อย่าลืมเลือกขนาดฮีปที่เหมาะสม (2 MB):
javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak
คุณจะไม่พบข้อผิดพลาดของ OOM ที่นี่เนื่องจากไม่มีการอ้างอิงใด ๆ จะถูกเก็บไว้แอปพลิเคชันจะทำงานต่อไปไม่ว่าคุณจะเลือก ITERATIONS มากแค่ไหนในตัวอย่างด้านบน ปริมาณการใช้หน่วยความจำของกระบวนการของคุณ (มองเห็นได้ในด้านบน (RES / RSS) หรือกระบวนการสำรวจ) เพิ่มขึ้นเว้นแต่แอปพลิเคชันจะได้รับคำสั่งรอ ในการตั้งค่าด้านบนจะจัดสรรหน่วยความจำประมาณ 150 MB
หากคุณต้องการให้แอปพลิเคชันเล่นอย่างปลอดภัยให้ปิดสตรีมอินพุตทันทีที่สร้าง:
MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();
และกระบวนการของคุณจะต้องไม่เกิน 35 MB โดยไม่ขึ้นอยู่กับจำนวนการวนซ้ำ
ค่อนข้างง่ายและน่าประหลาดใจ
ตามที่ผู้คนจำนวนมากได้แนะนำการรั่วไหลของทรัพยากรค่อนข้างง่ายที่จะทำให้เกิด - เช่นตัวอย่าง JDBC การรั่วไหลของหน่วยความจำจริงนั้นค่อนข้างยากโดยเฉพาะถ้าคุณไม่ต้องพึ่งพา JVM ที่แตกหักเพื่อทำเพื่อคุณ ...
แนวคิดของการสร้างวัตถุที่มีรอยเท้าขนาดใหญ่มากและจากนั้นไม่สามารถเข้าถึงวัตถุเหล่านั้นก็ไม่ได้เป็นการรั่วไหลของหน่วยความจำจริง หากไม่มีสิ่งใดสามารถเข้าถึงได้มันจะถูกเก็บรวบรวมขยะและหากบางสิ่งสามารถเข้าถึงได้ก็จะไม่ใช่การรั่วไหล ...
วิธีหนึ่งที่เคยทำงาน - และฉันก็ไม่รู้ว่ามันจะยัง - คือการมีห่วงโซ่วงกลมสามลึก ในขณะที่วัตถุ A มีการอ้างอิงถึงวัตถุ B วัตถุ B มีการอ้างอิงถึงวัตถุ C และวัตถุ C มีการอ้างอิงไปยังวัตถุ A GC เป็นคนฉลาดพอที่จะรู้ว่าสองโซ่ลึก - ในขณะที่ A <--> B - สามารถรวบรวมได้อย่างปลอดภัยหาก A และ B ไม่สามารถเข้าถึงได้โดยสิ่งอื่น แต่ไม่สามารถจัดการห่วงโซ่สามทาง ...
อีกวิธีในการสร้างการรั่วไหลของหน่วยความจำขนาดใหญ่ที่อาจเกิดขึ้นคือการเก็บการอ้างอิงMap.Entry<K,V>
ของTreeMap
ของ
มันยากที่จะประเมินว่าทำไมสิ่งนี้ถึงใช้ได้กับTreeMap
s เท่านั้น แต่โดยการดูการใช้งานอาจเป็นเพราะ: TreeMap.Entry
ร้านค้าอ้างอิงถึงพี่น้องของมันดังนั้นถ้า a TreeMap
พร้อมที่จะถูกเก็บรวบรวม มันMap.Entry
จากนั้นแผนที่ทั้งหมดจะถูกเก็บไว้ในหน่วยความจำ
สถานการณ์ในชีวิตจริง:
ลองนึกภาพว่ามีเคียวรี db ที่ส่งคืนTreeMap
โครงสร้างข้อมูลขนาดใหญ่ คนมักจะใช้TreeMap
s เป็นลำดับการแทรกองค์ประกอบจะถูกเก็บไว้
public static Map<String, Integer> pseudoQueryDatabase();
ถ้าแบบสอบถามถูกเรียกหลายครั้งและสำหรับแต่ละแบบสอบถาม (ดังนั้นสำหรับแต่ละMap
คืน) คุณบันทึกEntry
ใดที่หนึ่งหน่วยความจำจะเพิ่มขึ้นเรื่อย ๆ
พิจารณาคลาส wrapper ต่อไปนี้:
class EntryHolder {
Map.Entry<String, Integer> entry;
EntryHolder(Map.Entry<String, Integer> entry) {
this.entry = entry;
}
}
การประยุกต์ใช้:
public class LeakTest {
private final List<EntryHolder> holdersCache = new ArrayList<>();
private static final int MAP_SIZE = 100_000;
public void run() {
// create 500 entries each holding a reference to an Entry of a TreeMap
IntStream.range(0, 500).forEach(value -> {
// create map
final Map<String, Integer> map = pseudoQueryDatabase();
final int index = new Random().nextInt(MAP_SIZE);
// get random entry from map
for (Map.Entry<String, Integer> entry : map.entrySet()) {
if (entry.getValue().equals(index)) {
holdersCache.add(new EntryHolder(entry));
break;
}
}
// to observe behavior in visualvm
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
public static Map<String, Integer> pseudoQueryDatabase() {
final Map<String, Integer> map = new TreeMap<>();
IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
return map;
}
public static void main(String[] args) throws Exception {
new LeakTest().run();
}
}
หลังจากการpseudoQueryDatabase()
โทรแต่ละครั้งmap
อินสแตนซ์ควรพร้อมสำหรับการรวบรวม แต่จะไม่เกิดขึ้นเนื่องจากEntry
มีการจัดเก็บอย่างน้อยหนึ่งรายการที่อื่น
ทั้งนี้ขึ้นอยู่กับการตั้งค่าโปรแกรมประยุกต์อาจเกิดความผิดพลาดในช่วงแรกเนื่องจากjvm
OutOfMemoryError
คุณสามารถเห็นได้จากvisualvm
กราฟนี้ว่าหน่วยความจำเติบโตอย่างไร
สิ่งเดียวกันไม่ได้เกิดขึ้นกับโครงสร้างข้อมูลที่ถูกแฮช ( HashMap
)
HashMap
นี่คือกราฟเมื่อใช้
การแก้ไขปัญหา? เพียงแค่บันทึกโดยตรงที่สำคัญ / ค่า (ตามที่คุณอาจจะทำอยู่แล้ว) Map.Entry
มากกว่าการประหยัด
เธรดจะไม่ถูกรวบรวมจนกว่าจะสิ้นสุด พวกเขาทำหน้าที่เป็นรากของการเก็บขยะ พวกเขาเป็นหนึ่งในไม่กี่วัตถุที่จะไม่ถูกเรียกคืนเพียงแค่ลืมเกี่ยวกับพวกเขาหรือลบล้างการอ้างอิงถึงพวกเขา
พิจารณา: รูปแบบพื้นฐานเพื่อยุติเธรดผู้ทำงานคือการตั้งค่าตัวแปรเงื่อนไขบางอย่างที่เธรดเห็น เธรดสามารถตรวจสอบตัวแปรเป็นระยะและใช้เป็นสัญญาณเพื่อยกเลิก หากไม่ได้ประกาศตัวแปรvolatile
การเปลี่ยนแปลงของตัวแปรอาจไม่สามารถมองเห็นได้โดยเธรดดังนั้นจึงไม่รู้ว่าจะยุติ หรือลองคิดดูว่าเธรดบางตัวต้องการอัปเดตวัตถุที่แชร์ แต่หยุดชะงักในขณะที่พยายามล็อค
หากคุณมีเธรดเพียงหยิบไม่กี่ข้อบกพร่องเหล่านี้อาจจะชัดเจนเนื่องจากโปรแกรมของคุณจะหยุดทำงานอย่างถูกต้อง หากคุณมีเธรดพูลที่สร้างเธรดเพิ่มเติมตามต้องการเธรดที่ล้าสมัย / ค้างอาจไม่ถูกสังเกตเห็นและจะสะสมอย่างไม่มีกำหนดทำให้หน่วยความจำรั่ว เธรดมีแนวโน้มที่จะใช้ข้อมูลอื่นในแอปพลิเคชันของคุณดังนั้นจะป้องกันสิ่งที่อ้างอิงโดยตรงจากการรวบรวม
เป็นตัวอย่างของเล่น:
static void leakMe(final Object object) {
new Thread() {
public void run() {
Object o = object;
for (;;) {
try {
sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {}
}
}
}.start();
}
เรียกSystem.gc()
ทุกอย่างที่คุณชอบ แต่วัตถุที่ผ่านไปleakMe
จะไม่ตาย
(* * * * * แก้ไข)
ฉันคิดว่าตัวอย่างที่ถูกต้องอาจใช้ตัวแปร ThreadLocal ในสภาพแวดล้อมที่รวมเธรด
ตัวอย่างเช่นการใช้ตัวแปร ThreadLocal ใน Servlets เพื่อสื่อสารกับเว็บคอมโพเนนต์อื่น ๆ โดยมีเธรดที่สร้างขึ้นโดยคอนเทนเนอร์และดูแลรักษา idle ในพูล ตัวแปร ThreadLocal หากไม่ได้รับการทำความสะอาดอย่างถูกต้องจะอยู่ที่นั่นจนกว่าองค์ประกอบเว็บเดียวกันจะเขียนทับค่าของพวกเขา
แน่นอนเมื่อพบปัญหาสามารถแก้ไขได้อย่างง่ายดาย
ผู้สัมภาษณ์อาจมองหาวิธีการอ้างอิงแบบวงกลม:
public static void main(String[] args) {
while (true) {
Element first = new Element();
first.next = new Element();
first.next.next = first;
}
}
นี่เป็นปัญหาคลาสสิกที่มีตัวนับขยะอ้างอิง จากนั้นคุณจะอธิบายอย่างสุภาพว่า JVM ใช้อัลกอริทึมที่ซับซ้อนมากขึ้นซึ่งไม่มีข้อ จำกัด นี้
- ทาร์ล
first
นี้ไม่มีประโยชน์และควรเก็บรวบรวมขยะ ในการอ้างอิงการสะสมขยะนับวัตถุจะไม่ถูกปลดปล่อยเพราะมีการอ้างอิงที่ใช้งานอยู่ (โดยตัวมันเอง) ลูปไม่มีที่สิ้นสุดอยู่ที่นี่เพื่อบอกเลิกการรั่วไหล: เมื่อคุณเรียกใช้โปรแกรมหน่วยความจำจะเพิ่มขึ้นเรื่อย ๆ