วิธีการสร้างหน่วยความจำรั่วใน Java?


3223

ฉันเพิ่งได้รับการสัมภาษณ์และฉันถูกขอให้สร้างหน่วยความจำรั่วด้วย Java
ไม่จำเป็นต้องพูดอะไรเลยฉันรู้สึกงี่เง่าที่ไม่มีเงื่อนงำว่าจะเริ่มสร้างได้อย่างไร

ตัวอย่างจะเป็นอย่างไร


275
ฉันจะบอกพวกเขาว่า Java ใช้ตัวรวบรวมขยะและขอให้พวกเขามีความเฉพาะเจาะจงมากขึ้นเกี่ยวกับคำจำกัดความของ "หน่วยความจำรั่ว" อธิบายว่า - ยกเว้นข้อผิดพลาดของ JVM - Java ไม่สามารถรั่วหน่วยความจำในลักษณะเดียวกัน / C ++ สามารถ คุณจะต้องมีการอ้างอิงไปยังวัตถุที่อยู่ที่ไหนสักแห่ง
Darien

371
ฉันคิดว่ามันตลกที่คำตอบส่วนใหญ่ผู้คนกำลังมองหาเคสและเคล็ดลับเหล่านั้นและดูเหมือนจะหายไปอย่างสิ้นเชิง (IMO) พวกเขาสามารถแสดงโค้ดที่ให้การอ้างอิงที่ไร้ประโยชน์กับวัตถุที่จะไม่ใช้อีกครั้งและในเวลาเดียวกันก็ไม่ปล่อยการอ้างอิงเหล่านั้น อาจกล่าวได้ว่ากรณีเหล่านี้ไม่ใช่การรั่วไหลของหน่วยความจำ "ของจริง" เพราะยังมีการอ้างอิงไปยังวัตถุเหล่านั้นอยู่รอบ ๆ แต่ถ้าโปรแกรมไม่เคยใช้การอ้างอิงเหล่านั้นอีกครั้งและไม่เคยปล่อยมันอีกต่อไป หน่วยความจำรั่วจริง "
ehabkost

62
สุจริตฉันไม่สามารถเชื่อคำถามที่คล้ายกันที่ฉันถามเกี่ยวกับ "ไป" ได้ลงคะแนนไปที่ -1 ที่นี่: stackoverflow.com/questions/4400311/… โดยทั่วไปหน่วยความจำรั่วที่ฉันพูดถึงคือคนที่ได้รับ upvotes มากกว่า 200 ครั้งไปที่ OP แต่ฉันถูกโจมตีและดูถูกเพื่อถามว่า "Go" มีปัญหาเดียวกันหรือไม่ ยังไงก็เถอะฉันไม่แน่ใจว่าวิกิทุกสิ่งทำงานได้ดีขนาดนั้น
SyntaxT3rr0r

74
@ SyntaxT3rr0r - คำตอบของ darien ไม่ใช่ fanboyism เขายอมรับอย่างชัดเจนว่า JVM บางแห่งสามารถมีข้อบกพร่องที่หมายถึงความจำรั่ว สิ่งนี้แตกต่างจากสเปคภาษาที่ยอมให้มีการรั่วไหลของหน่วยความจำ
Peter Recore

31
@ehabkost: ไม่พวกเขาไม่เทียบเท่า (1)คุณมีความสามารถในการเรียกคืนหน่วยความจำในขณะที่ "รั่วจริง" โปรแกรม C / C ++ ของคุณลืมช่วงที่จัดสรรไว้ไม่มีวิธีที่ปลอดภัยในการกู้คืน (2)คุณสามารถตรวจจับปัญหาได้อย่างง่ายดายด้วยการทำโปรไฟล์เนื่องจากคุณสามารถดูว่าวัตถุใดที่ "bloat" เกี่ยวข้อง (3) "การรั่วไหลที่แท้จริง" เป็นข้อผิดพลาดที่ชัดเจนในขณะที่โปรแกรมที่เก็บวัตถุจำนวนมากรอบ ๆ จนกว่าจะถูกยกเลิกอาจเป็นส่วนหนึ่งของการตั้งใจทำงาน
Darien

คำตอบ:


2309

นี่เป็นวิธีที่ดีในการสร้างการรั่วไหลของหน่วยความจำที่แท้จริง (วัตถุที่ไม่สามารถเข้าถึงได้โดยการเรียกใช้รหัส แต่ยังคงเก็บไว้ในหน่วยความจำ) ในจาวาบริสุทธิ์:

  1. แอปพลิเคชันสร้างเธรดที่ใช้งานมานาน (หรือใช้เธรดพูลเพื่อให้รั่วไหลเร็วยิ่งขึ้น)
  2. ด้ายโหลดชั้นเรียนผ่าน ClassLoader(ขยะที่กำหนดเอง)
  3. จัดสรรระดับก้อนขนาดใหญ่ของหน่วยความจำ (เช่นnew byte[1000000]) ThreadLocalร้านค้าที่แข็งแกร่งในการอ้างอิงในช่องคงที่แล้วเก็บอ้างอิงถึงตัวเองใน การจัดสรรหน่วยความจำเสริมเป็นทางเลือก (การรั่วไหลของอินสแตนซ์ของคลาสนั้นเพียงพอ) แต่จะทำให้การรั่วไหลนั้นทำงานเร็วขึ้นมาก
  4. แอปพลิเคชันล้างการอ้างอิงทั้งหมดไปยังคลาสที่กำหนดเองหรือClassLoaderโหลดจาก
  5. ทำซ้ำ

เนื่องจากวิธีThreadLocalการใช้งานใน JDK ของ Oracle สิ่งนี้สร้างความจำรั่ว:

  • แต่ละอันThreadมีเขตข้อมูลส่วนตัวthreadLocalsซึ่งจริง ๆ แล้วเก็บค่า thread-local
  • แต่ละคีย์ในแผนที่นี้เป็นการอ้างอิงที่อ่อนแอไปยังThreadLocalวัตถุดังนั้นหลังจากThreadLocalวัตถุนั้นถูกรวบรวมขยะรายการของมันจะถูกลบออกจากแผนที่
  • แต่ค่าแต่ละค่าเป็นการอ้างอิงที่แข็งแกร่งดังนั้นเมื่อค่า (โดยตรงหรือโดยอ้อม) ชี้ไปยังThreadLocalวัตถุที่เป็นกุญแจสำคัญวัตถุนั้นจะไม่ถูกรวบรวมขยะหรือลบออกจากแผนที่ตราบใดที่เธรดยังมีชีวิตอยู่

ในตัวอย่างนี้ห่วงโซ่ของการอ้างอิงที่แข็งแกร่งมีลักษณะเช่นนี้:

Threadวัตถุ→ threadLocalsแผนที่→อินสแตนซ์ของคลาสตัวอย่าง→ตัวอย่างคลาส→สแตติกThreadLocalฟิลด์→ ThreadLocalวัตถุ

(การที่ClassLoaderไม่ได้มีบทบาทในการสร้างการรั่วไหลจริงๆมันทำให้การรั่วไหลแย่ลงเพราะห่วงโซ่อ้างอิงเพิ่มเติมนี้: class class →→คลาสClassLoaderทั้งหมดที่โหลดไว้มันแย่กว่าเดิมในการใช้ JVM โดยเฉพาะอย่างยิ่งก่อน Java 7 เนื่องจากคลาสและClassLoaders ถูกจัดสรรโดยตรงไปยัง permgen และไม่เคยถูกรวบรวมขยะเลย)

รูปแบบของรูปแบบนี้คือสาเหตุที่แอปพลิเคชั่นคอนเทนเนอร์ (เช่น Tomcat) สามารถรั่วไหลของหน่วยความจำได้เช่นเดียวกับตะแกรงถ้าคุณมักจะปรับใช้แอปพลิเคชั่นที่เกิดขึ้นเพื่อใช้ThreadLocals ในทางกลับกัน สิ่งนี้สามารถเกิดขึ้นได้ด้วยเหตุผลหลายประการและมักจะยากที่จะดีบักและ / หรือแก้ไข

อัพเดท : เนื่องจากผู้คนจำนวนมากขอให้มันนี่บางโค้ดตัวอย่างที่แสดงให้เห็นว่าพฤติกรรมนี้ในการดำเนินการ


186
+1 การรั่วไหลของ ClassLoader เป็นการรั่วไหลของหน่วยความจำที่เจ็บปวดที่สุดในโลก JEE มักเกิดจาก libs ของบุคคลที่สามที่แปลงข้อมูล (BeanUtils, ตัวแปลงสัญญาณ XML / JSON) สิ่งนี้สามารถเกิดขึ้นได้เมื่อ lib ถูกโหลดนอกรูทของตัวโหลดคลาสของแอปพลิเคชันของคุณ แต่เก็บการอ้างอิงไปยังคลาสของคุณ (เช่นโดยการแคช) เมื่อคุณยกเลิกการปรับใช้ / ปรับใช้แอปของคุณใหม่ JVM จะไม่สามารถรวบรวมขยะของแอป classloader (และดังนั้นจึงโหลดคลาสทั้งหมดด้วย) ดังนั้นด้วยซ้ำจะปรับใช้เซิร์ฟเวอร์แอปในที่สุดบอร์ก หากโชคดีคุณจะได้รับเบาะแสกับ ClassCastException zxyAbc ไม่สามารถส่งไปยัง zxyAbc ได้
earcam

7
Tomcat ใช้เทคนิคและตัวแปรคงที่ทั้งหมดในคลาสที่โหลดทั้งหมด, Tomcat มีจำนวนมากและการเขียนโค้ดที่ไม่ดี (ต้องใช้เวลาและส่งการแก้ไข) รวมทั้ง ConcurrentLinkedQueue เป็นแคชสำหรับวัตถุ (เล็ก) ภายใน เล็กจนแม้แต่ ConcurrentLinkedQueue.Node ก็ใช้หน่วยความจำได้มากกว่า
bestsss

57
+1: การรั่วไหลของ Classloader เป็นฝันร้าย ฉันใช้เวลาหลายสัปดาห์พยายามที่จะคิดออก สิ่งที่น่าเศร้าก็คือสิ่งที่ @earcam บอกไว้ส่วนใหญ่เกิดจาก libs ของบุคคลที่สามและผู้สร้างโปรไฟล์ส่วนใหญ่ไม่สามารถตรวจหารอยรั่วเหล่านี้ได้ มีคำอธิบายที่ดีและชัดเจนในบล็อกนี้เกี่ยวกับการรั่วไหลของ Classloader blogs.oracle.com/fkieviet/entry/…
Adrian M

4
@ Nicolas: คุณแน่ใจหรือไม่ JRockit ทำวัตถุคลาส GC โดยค่าเริ่มต้นและ HotSpot ไม่ได้ แต่ AFAIK JRockit ยังไม่สามารถ GC คลาสหรือ ClassLoader ที่อ้างอิงโดย ThreadLocal ได้
Daniel Pryden

6
Tomcat จะพยายามที่จะตรวจสอบการรั่วไหลของเหล่านี้ให้คุณและเตือนเกี่ยวกับพวกเขา: wiki.apache.org/tomcat/MemoryLeakProtection เวอร์ชันล่าสุดบางครั้งจะแก้ไขการรั่วไหลสำหรับคุณ
Matthijs Bierman

1212

การอ้างอิงวัตถุคงที่ในฟิลด์ [ฟิลด์สุดท้ายของ 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


178
ฉันไม่เห็นด้วยว่าคุณลักษณะและบริบทของเซสชันนั้น "รั่ว" มันเป็นเพียงตัวแปรที่มีอายุยืนยาว และสนามสุดท้ายคงที่มากหรือน้อยเพียงคงที่ บางทีค่าคงที่ขนาดใหญ่ควรหลีกเลี่ยง แต่ฉันไม่คิดว่ามันยุติธรรมที่จะเรียกมันว่าหน่วยความจำรั่ว
Ian McLaird

80
(ไม่เปิดเผย) สตรีมแบบเปิด (ไฟล์, เครือข่าย ฯลฯ ... ) , ไม่รั่วไหลเพื่อความเป็นจริง, ระหว่างการสรุป (ซึ่งจะเป็นหลังจากรอบ GC ถัดไป) ปิด () กำลังจะจัดตารางเวลา ( close()โดยปกติจะไม่เรียกใช้ใน finalizer เธรดตั้งแต่อาจเป็นการดำเนินการบล็อก) มันเป็นวิธีปฏิบัติที่ไม่ดีที่จะไม่ปิด แต่ก็ไม่ทำให้เกิดการรั่วไหล Unclosed java.sql.Connection เหมือนกัน
bestsss

33
ใน JVM ส่วนใหญ่ที่มีสติดูเหมือนว่าคลาส String จะมีการอ้างอิงที่ไม่ดีเกี่ยวกับinternเนื้อหา hashtable เท่านั้น ด้วยเหตุนี้จึงมีการรวบรวมขยะอย่างถูกต้องและไม่รั่วไหล (แต่ IANAJP) mindprod.com/jgloss/interned.html#GC
Matt B.

42
วิธีคงฟิลด์อ้างอิงวัตถุวัตถุ [esp สุดท้ายเขตข้อมูล] เป็นหน่วยความจำรั่ว
Kanagavelu Sugumar

5
@cHao True อันตรายที่ฉันพบไม่ใช่ปัญหาจากความทรงจำที่สตรีมรั่วไหล ปัญหาคือหน่วยความจำรั่วไหลออกมาไม่เพียงพอ คุณสามารถจับที่จับได้จำนวนมาก แต่ยังมีหน่วยความจำมากมาย จากนั้น Garbage Collector อาจตัดสินใจไม่รบกวนการรวบรวมเต็มเพราะมันยังมีหน่วยความจำมากมาย ซึ่งหมายความว่าตัวเรียกขั้นสุดท้ายไม่ได้ถูกเรียกใช้ดังนั้นคุณจึงไม่มีที่จับ ปัญหาคือว่า finalizers จะทำงานโดยปกติก่อนที่คุณจะหมดหน่วยความจำจากการรั่วไหลของลำธาร แต่มันอาจไม่ได้รับการเรียกก่อนที่คุณจะหมดสิ่งอื่นนอกเหนือจากหน่วยความจำ
Patrick M

460

สิ่งที่ต้องทำคือใช้ 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.

68
ที่จริงแล้วคุณสามารถลบองค์ประกอบออกจาก HashSet แม้ว่าคลาสองค์ประกอบจะได้รับ hashCode และเท่ากับผิด เพิ่งได้รับตัววนซ้ำสำหรับชุดและใช้วิธีการลบเนื่องจากตัววนซ้ำนั้นทำงานกับรายการต้นแบบเองไม่ใช่องค์ประกอบ (โปรดทราบว่า hashCode ที่ไม่ได้นำไปใช้งานนั้นไม่เพียงพอที่จะทำให้เกิดการรั่วไหลค่าเริ่มต้นจะใช้เอกลักษณ์ของวัตถุอย่างง่ายและเพื่อให้คุณสามารถรับองค์ประกอบและลบออกได้ตามปกติ)
Donal Fellows

12
@ Donal สิ่งที่ฉันพยายามจะพูดฉันเดาว่าฉันไม่เห็นด้วยกับนิยามของหน่วยความจำรั่ว ฉันจะพิจารณา (เพื่อดำเนินการเปรียบเทียบต่อ) เทคนิคการลบตัววนซ้ำของคุณให้เป็นแบบหยดกระทะภายใต้รอยรั่ว การรั่วไหลยังคงมีอยู่โดยไม่คำนึงถึงกระทะหยด
corsiKa

94
ฉันยอมรับว่านี่ไม่ใช่หน่วยความจำ "รั่ว" เพราะคุณสามารถลบการอ้างอิงไปยังแฮชเซ็ตและรอให้ GC เริ่มเตะและโอมเพี้ยง! หน่วยความจำกลับไป
user541686

12
@ SyntaxT3rr0r ฉันตีความคำถามของคุณว่าถามว่ามีอะไรในภาษาที่นำไปสู่การรั่วไหลของหน่วยความจำโดยธรรมชาติ คำตอบคือไม่ คำถามนี้ถามว่าเป็นไปได้หรือไม่ที่จะสร้างสถานการณ์เพื่อสร้างบางสิ่งบางอย่างเช่นความจำรั่ว ตัวอย่างเหล่านี้ไม่มีการรั่วไหลของหน่วยความจำในลักษณะที่โปรแกรมเมอร์ C / C ++ จะเข้าใจ
Peter Lawrey

11
@ Peter Lawrey: ยังทำในสิ่งที่คุณคิดเกี่ยวกับเรื่องนี้: "มีไม่อะไรในภาษา C ที่เป็นธรรมชาติรั่วไหลของหน่วยความจำรั่วถ้าคุณไม่ลืมที่จะด้วยตนเองฟรีหน่วยความจำที่คุณจัดสรร" มันจะเป็นอย่างไรสำหรับความไม่ซื่อสัตย์ทางปัญญา อย่างไรก็ตามฉันเหนื่อย: คุณสามารถมีคำสุดท้าย
ไวยากรณ์ T3rr0r

271

ด้านล่างจะมีกรณีที่ไม่ชัดเจนที่จาวารั่วนอกเหนือจากกรณีมาตรฐานของผู้ฟังที่ถูกลืมการอ้างอิงแบบคงที่กุญแจปลอม / ที่แก้ไขได้ในแฮชแมปหรือเพียงแค่เธรดที่ติดอยู่โดยไม่มีโอกาสสิ้นสุดวงจรชีวิต

  • File.deleteOnExit() - รั่วสตริงเสมอ ถ้าสายอักขระเป็นสายอักขระย่อยการรั่วไหลยิ่งแย่ลง (อักขระพื้นฐาน [] ยังรั่วไหลออกมาด้วย)- ใน Java 7 substring ยังสำเนา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 ฉันได้พบตามคำขอ)

ขอให้โชคดีและอยู่อย่างปลอดภัย การรั่วไหลเป็นสิ่งที่ชั่วร้าย!


23
Creating but not starting a Thread...ใช่ฉันถูกกัดโดยคนนี้มาหลายศตวรรษแล้ว! (Java 1.3)
leonbloy

@leonbloy ก่อนที่มันจะแย่ยิ่งกว่าเดิมเนื่องจากเธรดถูกเพิ่มลงในกลุ่มเธรดโดยตรงการเริ่มต้นไม่ได้หมายถึงการรั่วไหลที่ยากมาก ไม่ใช่แค่เพิ่มunstartedจำนวน แต่ป้องกันกลุ่มเธรดจากการทำลาย (ความชั่วร้ายที่น้อยลง แต่ยังรั่ว)
bestsss

ขอบคุณ! "การเรียกThreadGroup.destroy()เมื่อ ThreadGroup ไม่มีเธรดเอง ... "เป็นจุดบกพร่องที่ไม่น่าเชื่อ ฉันกำลังไล่ล่าสิ่งนี้เป็นเวลาหลายชั่วโมงนำไปสู่ความหลงผิดเพราะการระบุเธรดใน GUI ควบคุมของฉันไม่แสดงอะไรเลย แต่กลุ่มเธรดและสันนิษฐานว่ากลุ่มลูกอย่างน้อยหนึ่งกลุ่มจะไม่หายไป
Lawrence Dol

1
@ bests: ฉันอยากรู้ว่าทำไมคุณต้องการที่จะลบเบ็ดปิดเพราะมันทำงานที่ดีปิด JVM?
Lawrence Dol

203

ตัวอย่างส่วนใหญ่ที่นี่ "ซับซ้อนเกินไป" พวกเขาเป็นกรณีขอบ ด้วยตัวอย่างเหล่านี้โปรแกรมเมอร์ทำผิดพลาด (เช่นไม่ต้องกำหนดค่าใหม่เท่ากับ / hashcode) หรือถูกกัดโดยมุมของ JVM / JAVA (โหลดคลาสที่มีสแตติก ... ) ฉันคิดว่านั่นไม่ใช่ประเภทของตัวอย่างที่ผู้สัมภาษณ์ต้องการหรือแม้แต่กรณีทั่วไป

แต่มีกรณีที่หน่วยความจำรั่วง่ายกว่า ตัวรวบรวมขยะจะปลดปล่อยสิ่งที่ไม่ได้อ้างอิงอีกต่อไป เราในฐานะนักพัฒนา Java ไม่สนใจเรื่องความจำ เราจัดสรรเมื่อจำเป็นและปล่อยให้เป็นอิสระโดยอัตโนมัติ ละเอียด.

แต่แอปพลิเคชั่นที่มีอายุการใช้งานยาวนานมักจะมีสถานะที่ใช้ร่วมกัน มันสามารถเป็นอะไรก็ได้สถิตยศาสตร์ซิงเกิล ... บ่อยครั้งที่แอพพลิเคชั่นที่ไม่สำคัญมีแนวโน้มที่จะสร้างกราฟวัตถุที่ซับซ้อน เพียงลืมที่จะตั้งค่าอ้างอิงเป็นโมฆะหรือบ่อยครั้งที่ลืมลบวัตถุหนึ่งจากคอลเลกชันก็เพียงพอที่จะทำให้หน่วยความจำรั่ว

แน่นอนว่าผู้ฟังทุกประเภท (เช่นผู้ฟัง UI) แคชหรือสถานะการแบ่งปันที่ยาวนานมักจะทำให้หน่วยความจำรั่วหากไม่ได้รับการจัดการอย่างเหมาะสม สิ่งที่ต้องทำความเข้าใจคือกรณีนี้ไม่ใช่มุมของ Java หรือมีปัญหากับตัวรวบรวมขยะ มันเป็นปัญหาการออกแบบ เราออกแบบให้เราเพิ่มผู้ฟังไปยังวัตถุที่มีอายุการใช้งานยาวนาน แต่เราไม่ได้ลบผู้ฟังออกเมื่อไม่ต้องการใช้อีกต่อไป เราแคชวัตถุ แต่เราไม่มีกลยุทธ์ในการลบวัตถุออกจากแคช

เราอาจมีกราฟที่ซับซ้อนซึ่งเก็บสถานะก่อนหน้านี้ที่ต้องการโดยการคำนวณ แต่สถานะก่อนหน้านั้นเชื่อมโยงกับสถานะก่อนหน้านี้เป็นต้น

เหมือนที่เราต้องปิดการเชื่อมต่อ SQL หรือไฟล์ เราจำเป็นต้องตั้งค่าการอ้างอิงที่เหมาะสมเป็นโมฆะและลบองค์ประกอบออกจากคอลเลกชัน เราจะมีกลยุทธ์การแคชที่เหมาะสม (ขนาดหน่วยความจำสูงสุดจำนวนองค์ประกอบหรือตัวนับ) วัตถุทั้งหมดที่อนุญาตให้ผู้ฟังได้รับการแจ้งเตือนต้องมีทั้งวิธีการ addListener และ removeListener และเมื่อไม่ใช้ตัวแจ้งเตือนเหล่านี้อีกต่อไปพวกเขาจะต้องล้างรายชื่อผู้ฟัง

การรั่วไหลของหน่วยความจำนั้นเป็นไปได้อย่างแท้จริงและสามารถคาดเดาได้อย่างสมบูรณ์แบบ ไม่จำเป็นต้องใช้คุณสมบัติพิเศษภาษาหรือมุมกรณี หน่วยความจำรั่วมีทั้งตัวบ่งชี้ว่ามีบางอย่างขาดหายไปหรือแม้แต่ปัญหาการออกแบบ


24
ฉันคิดว่ามันตลกที่คำตอบอื่น ๆ ที่ผู้คนกำลังมองหาเคสและเคล็ดลับเหล่านั้นและดูเหมือนว่าจะขาดจุด พวกเขาสามารถแสดงโค้ดที่ทำให้การอ้างอิงไร้ประโยชน์ไปยังวัตถุที่จะไม่ใช้อีกครั้งและไม่ลบการอ้างอิงเหล่านั้น อาจกล่าวได้ว่ากรณีเหล่านี้ไม่ใช่การรั่วไหลของหน่วยความจำ "ของจริง" เพราะยังมีการอ้างอิงไปยังวัตถุเหล่านั้นอยู่รอบ ๆ แต่ถ้าโปรแกรมไม่เคยใช้การอ้างอิงเหล่านั้นอีกครั้งและไม่เคยปล่อยมันอีกต่อไป หน่วยความจำรั่วจริง "
ehabkost

@ Nicolas Bousquet: "การรั่วไหลของหน่วยความจำเป็นไปได้จริง ๆ แล้วเป็นเรื่องยาก" ขอบคุณมาก เพิ่มขึ้น +15 ดี ฉันตะโกนที่นี่เพื่อระบุความจริงนั้นเป็นสถานที่ของคำถามเกี่ยวกับภาษา Go: stackoverflow.com/questions/4400311 คำถามนี้ยังมี downvotes ติดลบ :(
SyntaxT3rr0r

GC ใน Java และ. NET มีความหมายที่ชัดเจนในกราฟสมมติฐานของวัตถุที่เก็บการอ้างอิงไปยังวัตถุอื่น ๆ เช่นเดียวกับกราฟของวัตถุที่ "สนใจ" วัตถุอื่น ๆ ในความเป็นจริงมันเป็นไปได้ว่าขอบสามารถอยู่ในกราฟการอ้างอิงที่ไม่ได้เป็นตัวแทนของ "ความห่วงใย" ความสัมพันธ์และเป็นไปได้สำหรับวัตถุที่จะดูแลเกี่ยวกับการดำรงอยู่ของวัตถุอื่นแม้ว่าจะไม่มีการอ้างอิงเส้นทางตรงหรือทางอ้อม (แม้จะใช้WeakReference) ที่มีอยู่ จากที่หนึ่งไปยังอีก หากการอ้างอิงวัตถุมีบิตสำรองอาจเป็นประโยชน์หากมีตัวบ่งชี้ "ใส่ใจเกี่ยวกับเป้าหมาย" ...
supercat

... และให้ระบบจัดเตรียมการแจ้งเตือน (ผ่านวิธีการที่คล้ายกันPhantomReference) หากพบว่าวัตถุนั้นไม่มีใครที่ห่วงใย WeakReferenceค่อนข้างใกล้เข้ามา แต่ต้องถูกแปลงเป็นข้อมูลอ้างอิงที่แข็งแกร่งก่อนที่จะสามารถใช้งานได้ หากรอบ GC เกิดขึ้นในขณะที่มีการอ้างอิงที่รัดกุมเป้าหมายจะถือว่ามีประโยชน์
supercat

นี่คือความคิดของฉันคำตอบที่ถูกต้อง เราเขียนการจำลองเมื่อหลายปีก่อน อย่างไรก็ตามเราได้ทำการเชื่อมโยงสถานะก่อนหน้ากับสถานะปัจจุบันโดยไม่ตั้งใจ เนื่องจากกำหนดเวลาเราไม่เคยแก้ไขหน่วยความจำรั่ว แต่ทำให้เป็น«คุณสมบัติ»โดยบันทึกไว้
2560

163

คำตอบขึ้นอยู่กับสิ่งที่ผู้สัมภาษณ์คิดว่าพวกเขาถาม

เป็นไปได้หรือไม่ที่จะทำให้ Java รั่ว? แน่นอนมันเป็นและมีตัวอย่างมากมายในคำตอบอื่น ๆ

แต่มีเมตาคำถามหลายคำถามที่อาจถูกถามอยู่

  • การนำ Java ไปใช้ "สมบูรณ์แบบ" ในทางทฤษฎีมีความเสี่ยงต่อการรั่วไหลหรือไม่
  • ผู้สมัครเข้าใจความแตกต่างระหว่างทฤษฎีและความจริงหรือไม่?
  • ผู้สมัครเข้าใจว่าการเก็บขยะอย่างไร
  • หรือว่าการรวบรวมขยะควรจะทำงานอย่างไรในกรณีที่เหมาะสมที่สุด?
  • พวกเขารู้หรือไม่ว่าพวกเขาสามารถโทรหาภาษาอื่นผ่านอินเทอร์เฟซดั้งเดิม
  • พวกเขารู้หรือไม่ว่าการรั่วไหลของหน่วยความจำในภาษาอื่น ๆ ?
  • ผู้สมัครไม่ทราบว่าการจัดการหน่วยความจำคืออะไรและสิ่งที่เกิดขึ้นเบื้องหลังใน Java?

ฉันกำลังอ่านเมตาคำถามของคุณว่า "มีคำตอบอะไรที่ฉันสามารถใช้ในสถานการณ์สัมภาษณ์นี้" ดังนั้นฉันจะมุ่งเน้นไปที่ทักษะการสัมภาษณ์แทน Java ฉันเชื่อว่าคุณมีแนวโน้มที่จะทำซ้ำสถานการณ์ที่ไม่รู้คำตอบของคำถามในการสัมภาษณ์มากกว่าที่คุณจะต้องรู้วิธีทำให้ Java รั่วไหล ดังนั้นหวังว่านี่จะช่วยได้

หนึ่งในทักษะที่สำคัญที่สุดที่คุณสามารถพัฒนาสำหรับการสัมภาษณ์คือการเรียนรู้ที่จะฟังคำถามและทำงานร่วมกับผู้สัมภาษณ์เพื่อดึงความตั้งใจของพวกเขา สิ่งนี้ไม่เพียงช่วยให้คุณตอบคำถามในแบบที่พวกเขาต้องการ แต่ยังแสดงให้เห็นว่าคุณมีทักษะการสื่อสารที่สำคัญ และเมื่อพูดถึงทางเลือกระหว่างนักพัฒนาที่มีความสามารถหลายคนเท่ากันฉันจะจ้างคนที่ฟังคิดและเข้าใจก่อนที่พวกเขาจะตอบทุกครั้ง


22
เมื่อใดก็ตามที่ฉันถามคำถามนั้นฉันกำลังมองหาคำตอบที่ง่าย ๆ - เพิ่มจำนวนคิวไม่มีการปิดฐานข้อมูล ฯลฯ และไม่ได้มีรายละเอียดของ classloader / thread แปลก ๆ พวกเขาเข้าใจว่าสิ่งที่ gc สามารถทำได้และไม่สามารถทำได้สำหรับคุณ ขึ้นอยู่กับงานที่คุณสัมภาษณ์ฉันเดาว่า
DaveC

โปรดดูคำถามของฉันขอบคุณstackoverflow.com/questions/31108772/…
แดเนียลนิวทาวน์

130

ต่อไปนี้เป็นตัวอย่างที่ไม่มีจุดหมายสวยถ้าคุณไม่เข้าใจJDBC หรืออย่างน้อยวิธี JDBC คาดว่าจะมีนักพัฒนาที่จะใกล้Connection, Statementและอินสแตนซ์ก่อนที่จะทิ้งพวกเขาหรือการสูญเสียการอ้างอิงไปยังพวกเขาแทนที่จะอาศัยในการดำเนินการResultSetfinalize

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อินสแตนซ์ที่คุณเพิ่มไปยังรายการเท่านั้นและไม่ลบออกจากมัน (แม้ว่าคุณควรจะกำจัดองค์ประกอบที่คุณไม่ต้องการอีกต่อไป) หรือ
  • เปิดSockets หรือFiles แต่ไม่ปิดเมื่อไม่ต้องการใช้อีกต่อไป (คล้ายกับตัวอย่างด้านบนที่เกี่ยวข้องกับConnectionคลาส)
  • อย่ายกเลิกการโหลด Singletons เมื่อนำแอปพลิเคชัน Java EE มาใช้ เห็นได้ชัดว่า Classloader ที่โหลดคลาส singleton จะเก็บการอ้างอิงไปยังคลาสและดังนั้นอินสแตนซ์ singleton จะไม่ถูกรวบรวม เมื่อมีการปรับใช้อินสแตนซ์ใหม่ของแอปพลิเคชันมักจะสร้างตัวโหลดคลาสใหม่และตัวโหลดคลาสเดิมจะยังคงมีอยู่เนื่องจาก Singleton

98
คุณจะถึงขีด จำกัด การเชื่อมต่อสูงสุดที่เปิดอยู่ก่อนที่จะถึงขีด จำกัด หน่วยความจำตามปกติ อย่าถามฉันว่าทำไมฉันถึงรู้ ...
Hardwareguy

ไดรเวอร์ Oracle JDBC มีชื่อเสียงในการทำเช่นนี้
chotchki

@Hardwareguy ฉันกดขีด จำกัด การเชื่อมต่อของฐานข้อมูล SQL เป็นจำนวนมากจนกระทั่งฉันใส่Connection.closeบล็อกสุดท้ายของการเรียก SQL ทั้งหมดของฉัน เพื่อความสนุกสนานเป็นพิเศษฉันเรียกใช้โพรซีเดอร์ที่เก็บของ Oracle มานานซึ่งต้องการการล็อกทางฝั่ง Java เพื่อป้องกันการโทรไปยังฐานข้อมูลมากเกินไป
Michael Shopsin

@Hardwareguy น่าสนใจ แต่ก็ไม่จำเป็นจริงๆที่ขีด จำกัด การเชื่อมต่อจริงจะถูกโจมตีในทุกสภาพแวดล้อม ตัวอย่างเช่นสำหรับแอปพลิเคชันที่ปรับใช้บนเว็บแอปพลิเคชันเซิร์ฟเวอร์ 11g ฉันได้เห็นการรั่วไหลของการเชื่อมต่อในระดับใหญ่ แต่เนื่องจากตัวเลือกในการเก็บเกี่ยวการเชื่อมต่อฐานข้อมูลรั่วไหลออกยังคงมีอยู่ในขณะที่หน่วยความจำรั่วถูกนำเสนอ ฉันไม่แน่ใจเกี่ยวกับสภาพแวดล้อมทั้งหมด
Aseem Bansal

จากประสบการณ์ของฉันคุณได้รับหน่วยความจำรั่วแม้ว่าคุณจะปิดการเชื่อมต่อ คุณต้องปิด ResultSet และ PreparedStatement ก่อน มีเซิร์ฟเวอร์ที่ล้มเหลวซ้ำแล้วซ้ำเล่าหลังจากชั่วโมงหรือแม้กระทั่งวันที่ทำงานได้ดีเนื่องจาก OutOfMemoryErrors จนกว่าฉันจะเริ่มทำเช่นนั้น
Bjørn Stenfeldt

119

อาจเป็นหนึ่งในตัวอย่างที่ง่ายที่สุดของการรั่วไหลของหน่วยความจำที่มีศักยภาพและวิธีการหลีกเลี่ยงมันคือการใช้ 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หรือไม่? การอ้างอิงนั้นอาจทำให้วัตถุขนาดใหญ่มีชีวิตอยู่ ...


5
และหน่วยความจำรั่วอยู่ตรงไหน?
rds

28
@maniek: ฉันไม่ได้หมายความว่ารหัสนี้แสดงถึงการรั่วไหลของหน่วยความจำ ฉันยกมาเพื่อแสดงให้เห็นว่าบางครั้งต้องใช้รหัสที่ไม่ชัดเจนเพื่อหลีกเลี่ยงการเก็บวัตถุโดยไม่ตั้งใจ
meriton

RangeCheck คืออะไร (ดัชนี); ?
Koray Tugay

6
Joshua Bloch ยกตัวอย่างนี้ใน Effective Java ซึ่งแสดงให้เห็นถึงการใช้งาน Stacks อย่างง่าย คำตอบที่ดีมาก
เช่า

แต่นั่นจะไม่ใช่การรั่วไหลของหน่วยความจำที่แท้จริงแม้ว่าจะลืม องค์ประกอบยังคงสามารถเข้าถึงได้อย่างปลอดภัยด้วย Reflection มันจะไม่ชัดเจนและเข้าถึงได้โดยตรงผ่านทางรายการอินเตอร์เฟส แต่วัตถุและการอ้างอิงยังคงอยู่ที่นั่นและสามารถเข้าถึงได้อย่างปลอดภัย
DGoiko

68

เมื่อใดก็ตามที่คุณอ้างอิงไปยังวัตถุที่คุณไม่ต้องการให้หน่วยความจำรั่วไหลอีกต่อไป ดูที่การจัดการการรั่วไหลของหน่วยความจำในโปรแกรม Javaสำหรับตัวอย่างของการรั่วไหลของหน่วยความจำใน Java และสิ่งที่คุณสามารถทำได้


14
ฉันไม่เชื่อว่านี่คือ "การรั่วไหล" มันเป็นบั๊กและโดยการออกแบบของโปรแกรมและภาษา การรั่วไหลจะเป็นวัตถุที่ห้อยอยู่โดยไม่มีการอ้างอิงใด ๆ
user541686

29
@ Mehrdad: นั่นเป็นเพียงคำจำกัดความแคบ ๆ ที่ไม่สามารถใช้ได้กับทุกภาษา ฉันขอยืนยันว่าหน่วยความจำรั่วใด ๆเป็นข้อผิดพลาดที่เกิดจากการออกแบบโปรแกรมไม่ดี
Bill the Lizard

9
@ Mehrdad: ...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language. ฉันไม่เห็นว่าคุณวาดข้อสรุปที่ มีวิธีที่น้อยกว่าในการสร้างหน่วยความจำรั่วใน Java โดยคำจำกัดความใด ๆ มันยังคงเป็นคำถามที่ถูกต้องแน่นอน
Bill the Lizard

7
@ 31eee384: หากโปรแกรมของคุณเก็บวัตถุไว้ในหน่วยความจำที่ไม่สามารถใช้งานได้ในทางเทคนิคแล้วมันเป็นหน่วยความจำรั่ว ความจริงที่ว่าคุณมีปัญหาใหญ่ไม่ได้เปลี่ยนไป
Bill the Lizard

8
@ 31eee384: ถ้าคุณรู้ว่ามันไม่เป็นเช่นนั้นมันก็เป็นไปไม่ได้ โปรแกรมดังที่เขียนไว้จะไม่สามารถเข้าถึงข้อมูลได้
Bill the Lizard

51

คุณสามารถทำให้หน่วยความจำรั่วด้วยคลาส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();
        }
    }

}

1
หน่วยความจำที่จัดสรรไม่สามารถมองเห็นได้สำหรับนักสะสมขยะ
Stemm

4
หน่วยความจำที่จัดสรรไม่ได้เป็นของ Java
bestsss

อาทิตย์ / oracle jvm นี้เป็นเรื่องเฉพาะหรือไม่? เช่นนี้จะทำงานกับ IBM?
เบอร์ลินบราวน์

2
หน่วยความจำไม่แน่นอน "เป็นของ Java" อย่างน้อยก็ในแง่ที่ว่า i) มันไม่สามารถใช้ได้กับคนอื่น ii) เมื่อแอปพลิเคชัน Java ออกจากมันจะถูกส่งกลับไปยังระบบ มันอยู่นอก JVM
Michael Anderson

3
สิ่งนี้จะสร้างใน eclipse (อย่างน้อยในเวอร์ชันล่าสุด) แต่คุณจะต้องเปลี่ยนการตั้งค่าคอมไพเลอร์: ในหน้าต่าง> การกำหนดค่าตามความชอบ> Java> คอมไพเลอร์> ข้อผิดพลาด / คำเตือน> ตั้ง API ที่คัดค้านและ จำกัด "
Michael Anderson

43

ฉันสามารถคัดลอกคำตอบของฉันจากที่นี่: วิธีที่ง่ายที่สุดที่จะทำให้หน่วยความจำรั่วใน Java?

"การรั่วไหลของหน่วยความจำในวิทยาศาสตร์คอมพิวเตอร์ (หรือการรั่วไหลในบริบทนี้) เกิดขึ้นเมื่อโปรแกรมคอมพิวเตอร์ใช้หน่วยความจำ แต่ไม่สามารถปล่อยมันกลับสู่ระบบปฏิบัติการได้" (วิกิพีเดีย)

คำตอบง่ายๆคือ: คุณทำไม่ได้ Java ทำการจัดการหน่วยความจำอัตโนมัติและจะเพิ่มทรัพยากรที่ไม่จำเป็นสำหรับคุณ คุณไม่สามารถหยุดสิ่งนี้ได้ มันจะสามารถปล่อยทรัพยากรได้เสมอ ในโปรแกรมที่มีการจัดการหน่วยความจำด้วยตนเองจะแตกต่างกัน คุณไม่สามารถรับหน่วยความจำใน C โดยใช้ malloc () ในการเพิ่มหน่วยความจำคุณต้องมีตัวชี้ที่ malloc ส่งคืนและโทรฟรี () แต่ถ้าคุณไม่มีตัวชี้อีกต่อไป (เขียนทับหรือเกินอายุการใช้งานแล้ว) คุณน่าเสียดายที่ไม่สามารถเพิ่มหน่วยความจำนี้ได้และทำให้คุณมีหน่วยความจำรั่ว

คำตอบอื่น ๆ ทั้งหมดที่อยู่ในคำจำกัดความของฉันไม่รั่วหน่วยความจำจริงๆ พวกเขาตั้งเป้าหมายในการเติมความทรงจำด้วยสิ่งไร้จุดหมายอย่างรวดเร็ว แต่เมื่อใดก็ตามที่คุณยังคงสามารถตรวจสอบวัตถุที่คุณสร้างขึ้นและทำให้หน่วยความจำว่าง -> ไม่รั่วไหล คำตอบของ acconradนั้นค่อนข้างใกล้เคียงกับที่ฉันต้องยอมรับเพราะวิธีการแก้ปัญหาของเขานั้นมีประสิทธิภาพเพียงแค่ "พัง" ตัวเก็บขยะโดยบังคับให้มันวนซ้ำไม่รู้จบ)

คำตอบที่ยาวคือ: คุณสามารถทำให้หน่วยความจำรั่วโดยการเขียนไลบรารีสำหรับ Java โดยใช้ JNI ซึ่งสามารถจัดการหน่วยความจำด้วยตนเองและทำให้หน่วยความจำรั่ว ถ้าคุณเรียกไลบรารีนี้กระบวนการจาวาของคุณจะทำให้หน่วยความจำรั่วไหล หรือคุณสามารถมีข้อบกพร่องใน JVM เพื่อให้ JVM สูญเสียความจำ อาจมีข้อบกพร่องใน JVM อาจมีบางคนที่รู้จักกันเนื่องจากการรวบรวมขยะไม่ใช่เรื่องเล็กน้อย แต่ก็ยังมีข้อบกพร่องอยู่ โดยการออกแบบนี้เป็นไปไม่ได้ คุณอาจจะขอรหัสจาวาบางตัวที่ได้รับผลกระทบจากข้อผิดพลาดดังกล่าว ขออภัยฉันไม่รู้จักและอาจไม่เป็นข้อผิดพลาดอีกต่อไปในรุ่น Java ถัดไป


12
นั่นเป็นคำจำกัดความที่รั่วไหลของหน่วยความจำ (และไม่มีประโยชน์มาก) อย่าง จำกัด คำจำกัดความเดียวที่เหมาะสมสำหรับวัตถุประสงค์ในทางปฏิบัติคือ "การรั่วไหลของหน่วยความจำคือเงื่อนไขใด ๆ ที่โปรแกรมยังคงเก็บหน่วยความจำที่จัดสรรไว้ต่อไปหลังจากข้อมูลที่เก็บไว้ไม่จำเป็นอีกต่อไป"
Mason Wheeler

1
คำตอบของ acconrad ดังกล่าวถูกลบไปแล้ว?
Tomáš Zato - Reinstate Monica

1
@ TomášZato: ไม่เลย ฉันเปิดการอ้างอิงด้านบนเพื่อลิงก์ในขณะนี้ดังนั้นคุณสามารถค้นหาได้อย่างง่ายดาย
yankee

การฟื้นคืนชีพของวัตถุคืออะไร? destructor เรียกว่ากี่ครั้ง คำถามเหล่านี้หักล้างคำตอบนี้ได้อย่างไร
ออทิสติก

1
แน่นอนคุณสามารถสร้างหน่วยความจำรั่วภายใน Java แม้จะมี GC และยังคงปฏิบัติตามคำนิยามข้างต้น เพียงแค่มีโครงสร้างข้อมูลแบบผนวกเท่านั้นป้องกันการเข้าถึงจากภายนอกเพื่อไม่ให้มีการลบรหัสอื่น ๆ ออก - โปรแกรมไม่สามารถปล่อยหน่วยความจำได้เนื่องจากไม่มีรหัส
toolforger

39

นี่คือหนึ่งง่าย / อุบาทว์ผ่าน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 ถูกยกเลิกไปแล้ว


4
ฉันจะไม่เรียกว่าหน่วยความจำรั่วต่อ se เมื่อmuchSmallerStringถูกปล่อยให้ว่าง (เนื่องจากStringLeakerวัตถุถูกทำลาย) สายยาวจะถูกปล่อยเช่นกัน สิ่งที่ฉันเรียกหน่วยความจำรั่วคือหน่วยความจำที่ไม่เคยได้รับใน JVM นี้ this.muchSmallerString=new String(this.muchSmallerString)แต่คุณได้แสดงให้เห็นตัวเองว่าจะเพิ่มหน่วยความจำ: ด้วยการรั่วไหลของหน่วยความจำจริงไม่มีอะไรที่คุณสามารถทำได้
rds

2
@rds นั่นเป็นประเด็นที่ยุติธรรม internกรณีที่ไม่ใช่"หน่วยความจำแปลกใจ" มากกว่า "หน่วยความจำรั่ว" .intern()ถึงแม้ว่าสตริงย่อยจะสร้างสถานการณ์ที่การอ้างอิงไปยังสตริงที่ยาวกว่านั้นถูกสงวนไว้และไม่สามารถทำให้เป็นอิสระได้
Jon Chambers

15
สตริงย่อยเมธอด () สร้างสตริงใหม่ใน java7 (ซึ่งเป็นลักษณะการทำงานใหม่)
anstarovoyt

คุณไม่จำเป็นต้องทำซับสตริง () ด้วยตัวเอง: ใช้ regex matcher เพื่อจับคู่ส่วนเล็ก ๆ ของอินพุตขนาดใหญ่และพกสตริง "แยก" เป็นเวลานาน ข้อมูลขนาดใหญ่ยังมีชีวิตอยู่จนถึง Java 6
Bananeweizen

37

ตัวอย่างทั่วไปของสิ่งนี้ในโค้ด GUI คือเมื่อสร้างวิดเจ็ต / ส่วนประกอบและเพิ่มผู้ฟังไปยังวัตถุที่กำหนดขอบเขตแบบสแตติก / แอ็พพลิเคชันบางส่วนจากนั้นไม่ลบตัวฟังเมื่อวิดเจ็ตถูกทำลาย ไม่เพียง แต่คุณจะมีหน่วยความจำรั่ว แต่ยังมีการแสดงที่มีประสิทธิภาพเช่นเมื่อสิ่งที่คุณกำลังฟังไฟเหตุการณ์ผู้ฟังเก่าของคุณทั้งหมดจะถูกเรียกเช่นกัน


1
แพลตฟอร์ม Android ให้กันตัวอย่างของการรั่วไหลของหน่วยความจำที่สร้างขึ้นโดยแคชบิตแมปในฟิลด์คงที่ของดู
rds

36

ใช้เว็บแอปพลิเคชันใด ๆ ที่ทำงานอยู่ในคอนเทนเนอร์ servlet (Tomcat, Jetty, Glassfish หรืออะไรก็ตาม ... ) ปรับใช้แอปใหม่ 10 หรือ 20 ครั้งในหนึ่งแถว (อาจเพียงพอที่จะแตะ WAR ในไดเรกทอรี autodeploy ของเซิร์ฟเวอร์

นอกเสียจากว่าใครได้ทำการทดสอบสิ่งนี้จริง ๆ โอกาสสูงที่คุณจะได้รับ OutOfMemoryError หลังจากการปรับใช้สองครั้งเนื่องจากแอปพลิเคชันไม่ได้ดูแลการล้างข้อมูลหลังจากตัวเอง คุณอาจพบข้อบกพร่องในเซิร์ฟเวอร์ของคุณด้วยการทดสอบนี้

ปัญหาคืออายุการใช้งานของคอนเทนเนอร์นานกว่าอายุการใช้งานของแอปพลิเคชันของคุณ คุณต้องตรวจสอบให้แน่ใจว่าการอ้างอิงทั้งหมดที่คอนเทนเนอร์อาจมีกับวัตถุหรือคลาสของแอปพลิเคชันของคุณอาจถูกรวบรวมขยะ

หากมีการอ้างอิงเพียงหนึ่งเดียวที่รอดชีวิตจากการเลิกใช้งานแอพพลิเคชั่นเว็บของคุณ classloader ที่เกี่ยวข้องและด้วยเหตุนี้คลาสทั้งหมดของแอปพลิเคชันเว็บของคุณจะไม่สามารถรวบรวมขยะได้

เธรดที่เริ่มต้นโดยแอปพลิเคชันของคุณ, ตัวแปร ThreadLocal, ตัวบันทึกการทำงานเป็นข้อสงสัยบางประการที่ทำให้เกิดการรั่วไหลของ classloader


1
สิ่งนี้ไม่ใช่เพราะหน่วยความจำรั่ว แต่เนื่องจากตัวโหลดคลาสไม่ได้ยกเลิกการโหลดชุดคลาสก่อนหน้านี้ ดังนั้นจึงไม่แนะนำให้ปรับใช้แอปพลิเคชันเซิร์ฟเวอร์อีกครั้งโดยไม่ต้องรีสตาร์ทเซิร์ฟเวอร์ (ไม่ใช่เครื่องจริง แต่เป็นแอพเซิร์ฟเวอร์) ฉันได้เห็นปัญหาเดียวกันกับ WebSphere
Sven

35

อาจจะใช้รหัสพื้นเมืองภายนอกผ่าน JNI?

ด้วยจาวาบริสุทธิ์นั้นแทบจะเป็นไปไม่ได้เลย

แต่นั่นเป็นเรื่องเกี่ยวกับการรั่วไหลของหน่วยความจำแบบ "มาตรฐาน" เมื่อคุณไม่สามารถเข้าถึงหน่วยความจำได้อีกต่อไป แต่แอปพลิเคชันยังคงเป็นเจ้าของ คุณสามารถเก็บการอ้างอิงไปยังวัตถุที่ไม่ได้ใช้แทนหรือเปิดสตรีมโดยไม่ต้องปิดมันในภายหลัง


22
ขึ้นอยู่กับคำจำกัดความของ "memory รั่ว" หาก "หน่วยความจำที่เก็บไว้ แต่ไม่จำเป็นอีกต่อไป" แล้วมันเป็นเรื่องง่ายที่จะทำใน Java หากเป็น "หน่วยความจำที่จัดสรร แต่ไม่สามารถเข้าถึงได้ด้วยรหัสเลย" แสดงว่ามันยากขึ้นเล็กน้อย
โจอาคิมซาวเออร์

@ โจอาคิมซาวเออร์ - ฉันหมายถึงประเภทที่สอง คนแรกนั้นค่อนข้างง่ายที่จะทำ :)
Rogach

6
"ด้วยจาวาบริสุทธิ์มันแทบจะเป็นไปไม่ได้" ประสบการณ์ของฉันเป็นอีกเรื่องหนึ่งโดยเฉพาะอย่างยิ่งเมื่อพูดถึงการนำแคชไปใช้โดยคนที่ไม่ได้ตระหนักถึงข้อผิดพลาดที่นี่
เฟเบียนบาร์นีย์

4
@Rogach: โดยทั่วไปมี 400 upvotes สำหรับคำตอบต่าง ๆ ของผู้ที่มี 10,000 ตัวแทนแสดงว่าในทั้งสองกรณีJoachim Sauerแสดงความคิดเห็นว่าเป็นไปได้มาก ดังนั้น "แทบจะเป็นไปไม่ได้" ของคุณก็ไม่มีเหตุผล
ไวยากรณ์ T3rr0r

32

ฉันมี "หน่วยความจำรั่ว" ที่ดีเกี่ยวกับ 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) เพื่อให้มาถึง ...


4
เพียงแค่ใส่รหัสตัวอย่างของคุณ: ฉันคิดว่าไม่อนุญาตให้ใช้หมายเลข (หรือสตริงที่ขึ้นต้นด้วยตัวเลข) เป็นชื่อองค์ประกอบใน XML
Paŭlo Ebermann

โปรดทราบว่านี่ไม่เป็นความจริงอีกต่อไปสำหรับ JDK 7+ โดยที่ String Interning เกิดขึ้นบนฮีป ดูบทความนี้สำหรับการเขียนรายละเอียด: java-performance.info/string-intern-in-java-6-7-8
jmiserez

ดังนั้นฉันคิดว่าการใช้ StringBuffer แทน String จะแก้ปัญหานี้ได้หรือไม่ มันจะไม่?
anubhs

24

หน่วยความจำรั่วคืออะไร:

  • มันเกิดจากข้อผิดพลาดหรือการออกแบบที่ไม่ดี
  • มันเป็นความทรงจำที่เสียไป
  • มันเลวร้ายลงเมื่อเวลาผ่านไป
  • ที่เก็บขยะไม่สามารถทำความสะอาดได้

ตัวอย่างทั่วไป:

แคชของวัตถุเป็นจุดเริ่มต้นที่ดีในการเลอะสิ่งต่าง ๆ

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 (เก็บเฉพาะวัตถุที่เพิ่งใช้ในแคช)

แน่นอนคุณสามารถทำให้สิ่งต่าง ๆ มีความซับซ้อนมากขึ้น:

  • ใช้การก่อสร้างThreadLocal
  • เพิ่มมากขึ้นต้นไม้อ้างอิงที่ซับซ้อน
  • หรือการรั่วไหลเกิดจากห้องสมุดของบุคคลที่ 3

มักจะเกิดอะไรขึ้น:

หากวัตถุข้อมูลนี้มีการอ้างอิงถึงวัตถุอื่น ๆ ซึ่งมีการอ้างอิงไปยังวัตถุอื่นอีกครั้ง ในแบบที่คุณอาจคิดว่านี่เป็นหน่วยความจำรั่วบางส่วน (เกิดจากการออกแบบที่ไม่ดี)


22

ฉันคิดว่ามันน่าสนใจที่ไม่มีใครใช้ตัวอย่างภายในของชั้นเรียน หากคุณมีชั้นเรียนภายใน มันเก็บรักษาการอ้างอิงถึงคลาสที่มีอยู่โดยเนื้อแท้ แน่นอนว่าไม่ใช่เทคนิคการรั่วไหลของหน่วยความจำเพราะ 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 สันนิษฐานว่ามันจะมีอยู่เสมอและจะไม่พยายามล้างหากไม่สามารถเข้าถึงได้ในรหัสอีกต่อไป แต่นั่นไม่ได้ยืนยันอย่างสมบูรณ์


2
ชั้นในมักไม่ค่อยมีปัญหา มันเป็นกรณีที่ตรงไปตรงมาและง่ายต่อการตรวจจับ ข่าวลือก็เป็นเพียงข่าวลือเช่นกัน
bestsss

2
"ข่าวลือ" ดูเหมือนว่ามีคนอ่านครึ่งเกี่ยวกับการทำงานของ generational GC วัตถุที่ไม่สามารถเข้าถึงได้ แต่สามารถเข้าถึงได้ในขณะนี้เพราะ JVM เลื่อนตำแหน่งออกจากรุ่นน้องดังนั้นจึงสามารถหยุดการตรวจสอบพวกเขาทุกครั้งได้ พวกเขาจะหลบเลี่ยง "ทำความสะอาดสายอักขระชั่วคราวของฉัน 5,000 คะแนน" โดยการออกแบบ แต่พวกเขาไม่เป็นอมตะ พวกเขายังคงมีสิทธิ์ได้รับคอลเลกชันและถ้า VM ถูกรัดไว้สำหรับ RAM ในที่สุดมันก็จะทำการกวาดแบบเต็ม GC และยึดหน่วยความจำนั้น
cHao

22

ฉันเพิ่งพบสถานการณ์การรั่วไหลของหน่วยความจำที่เกิดขึ้นในทางโดย 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 จึงทำให้หน่วยความจำรั่ว


ที่ดูด คุณควรตรวจสอบไลบรารี่
TraderJoeChicago

+1 คุณรู้อย่างถ่องแท้ว่ามีปัญหาที่คล้ายกันกับ MDC ใน slf4j / logback (ผลิตภัณฑ์ที่สืบทอดโดยผู้เขียนคนเดียวกัน) หรือไม่? ฉันกำลังจะดำน้ำลึกที่แหล่งที่มา แต่ต้องการตรวจสอบก่อน ขอบคุณสำหรับการโพสต์นี้
sparc_spread

20

ผู้สัมภาษณ์อาจมองหาการอ้างอิงแบบวงกลมเช่นรหัสด้านล่าง (ซึ่งบังเอิญมีเพียงหน่วยความจำรั่วใน 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


19

ทุกคนลืมเส้นทางของรหัสเนทีฟเสมอ นี่คือสูตรง่ายๆสำหรับการรั่วไหล:

  1. ประกาศวิธีการเนทิฟ
  2. mallocในวิธีพื้นเมืองโทร freeอย่าเรียก
  3. เรียกใช้วิธีการเนทิฟ

โปรดจำไว้ว่าการจัดสรรหน่วยความจำในรหัสเนทีฟมาจากฮีป JVM


1
ขึ้นอยู่กับเรื่องจริง.
Reg

18

สร้างแผนที่แบบคงที่และเพิ่มการอ้างอิงอย่างหนักต่อไป สิ่งเหล่านั้นจะไม่เป็น 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); }
}

87
การรั่วไหลนั้นเป็นอย่างไร? มันทำสิ่งที่คุณขอให้ทำ หากนั่นคือการรั่วไหลการสร้างและจัดเก็บวัตถุในที่ใดก็ตามเป็นการรั่วไหล
Falmarri

3
ฉันเห็นด้วยกับ @Falmarri ฉันไม่เห็นรอยรั่วที่นั่นคุณแค่สร้างวัตถุ แน่นอนคุณสามารถ 'เรียกคืน' หน่วยความจำที่คุณเพิ่งจัดสรรด้วยวิธีอื่นที่เรียกว่า 'removeFromCache' การรั่วไหลคือเมื่อคุณไม่สามารถเรียกคืนหน่วยความจำได้
Kyle

3
ประเด็นของฉันคือคนที่ยังคงสร้างวัตถุบางทีอาจวางไว้ในแคชอาจจบลงด้วยข้อผิดพลาด OOM หากพวกเขาไม่ระวัง
duffymo

8
@duffymo: แต่นั่นไม่ใช่คำถามที่ถาม มันไม่มีอะไรเกี่ยวข้องกับเพียงแค่ใช้หน่วยความจำทั้งหมดของคุณ
Falmarri

3
ไม่ถูกต้องอย่างแน่นอน คุณเพียงรวบรวมวัตถุในคอลเลกชันแผนที่ การอ้างอิงของพวกเขาจะถูกเก็บไว้เพราะแผนที่ถือพวกเขา
gyorgyabraham

16

คุณสามารถสร้างการรั่วไหลของหน่วยความจำที่กำลังเคลื่อนที่ได้โดยการสร้างอินสแตนซ์ใหม่ของคลาสในวิธีการสรุปของคลาสนั้น คะแนนโบนัสหาก 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());
        }
    }
}

15

ฉันยังไม่คิดว่าจะมีใครพูดถึงสิ่งนี้: คุณสามารถรื้อฟื้นวัตถุโดยการแทนที่ขั้นตอนสุดท้าย () วิธีการที่จะจบ () เก็บการอ้างอิงของที่นี้ ตัวรวบรวมขยะจะถูกเรียกเพียงครั้งเดียวบนวัตถุดังนั้นหลังจากนั้นวัตถุจะไม่ถูกทำลาย


10
นี่เป็นเรื่องจริง finalize()จะไม่ถูกเรียกใช้ แต่วัตถุจะถูกรวบรวมเมื่อไม่มีการอ้างอิงเพิ่มเติม ตัวรวบรวมขยะไม่ได้ถูกเรียกว่า 'อย่างใดอย่างหนึ่ง
bestsss

1
คำตอบนี้ทำให้เข้าใจผิดfinalize()วิธีการนี้สามารถเรียกได้เพียงครั้งเดียวโดย JVM แต่นี่ไม่ได้หมายความว่าจะไม่สามารถรวบรวมขยะซ้ำได้หากวัตถุนั้นได้รับการฟื้นคืนชีพแล้วจึงถูกปฏิเสธอีกครั้ง หากมีรหัสการปิดทรัพยากรในfinalize()วิธีการรหัสนี้จะไม่ได้รับการเรียกใช้อีกครั้งนี้อาจทำให้หน่วยความจำรั่ว
Tom Cammann

15

ฉันเจอการรั่วไหลของทรัพยากรที่ลึกซึ้งยิ่งขึ้นเมื่อเร็ว ๆ นี้ เราเปิดทรัพยากรผ่าน 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 โดยไม่ขึ้นอยู่กับจำนวนการวนซ้ำ

ค่อนข้างง่ายและน่าประหลาดใจ


14

ตามที่ผู้คนจำนวนมากได้แนะนำการรั่วไหลของทรัพยากรค่อนข้างง่ายที่จะทำให้เกิด - เช่นตัวอย่าง JDBC การรั่วไหลของหน่วยความจำจริงนั้นค่อนข้างยากโดยเฉพาะถ้าคุณไม่ต้องพึ่งพา JVM ที่แตกหักเพื่อทำเพื่อคุณ ...

แนวคิดของการสร้างวัตถุที่มีรอยเท้าขนาดใหญ่มากและจากนั้นไม่สามารถเข้าถึงวัตถุเหล่านั้นก็ไม่ได้เป็นการรั่วไหลของหน่วยความจำจริง หากไม่มีสิ่งใดสามารถเข้าถึงได้มันจะถูกเก็บรวบรวมขยะและหากบางสิ่งสามารถเข้าถึงได้ก็จะไม่ใช่การรั่วไหล ...

วิธีหนึ่งที่เคยทำงาน - และฉันก็ไม่รู้ว่ามันจะยัง - คือการมีห่วงโซ่วงกลมสามลึก ในขณะที่วัตถุ A มีการอ้างอิงถึงวัตถุ B วัตถุ B มีการอ้างอิงถึงวัตถุ C และวัตถุ C มีการอ้างอิงไปยังวัตถุ A GC เป็นคนฉลาดพอที่จะรู้ว่าสองโซ่ลึก - ในขณะที่ A <--> B - สามารถรวบรวมได้อย่างปลอดภัยหาก A และ B ไม่สามารถเข้าถึงได้โดยสิ่งอื่น แต่ไม่สามารถจัดการห่วงโซ่สามทาง ...


7
ยังไม่เคยมีกรณีมาก่อนในขณะนี้ GCs สมัยใหม่รู้วิธีจัดการกับการอ้างอิงแบบวงกลม
assylias

13

อีกวิธีในการสร้างการรั่วไหลของหน่วยความจำขนาดใหญ่ที่อาจเกิดขึ้นคือการเก็บการอ้างอิงMap.Entry<K,V>ของTreeMapของ

มันยากที่จะประเมินว่าทำไมสิ่งนี้ถึงใช้ได้กับTreeMaps เท่านั้น แต่โดยการดูการใช้งานอาจเป็นเพราะ: TreeMap.Entryร้านค้าอ้างอิงถึงพี่น้องของมันดังนั้นถ้า a TreeMapพร้อมที่จะถูกเก็บรวบรวม มันMap.Entryจากนั้นแผนที่ทั้งหมดจะถูกเก็บไว้ในหน่วยความจำ


สถานการณ์ในชีวิตจริง:

ลองนึกภาพว่ามีเคียวรี db ที่ส่งคืนTreeMapโครงสร้างข้อมูลขนาดใหญ่ คนมักจะใช้TreeMaps เป็นลำดับการแทรกองค์ประกอบจะถูกเก็บไว้

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มีการจัดเก็บอย่างน้อยหนึ่งรายการที่อื่น

ทั้งนี้ขึ้นอยู่กับการตั้งค่าโปรแกรมประยุกต์อาจเกิดความผิดพลาดในช่วงแรกเนื่องจากjvmOutOfMemoryError

คุณสามารถเห็นได้จากvisualvmกราฟนี้ว่าหน่วยความจำเติบโตอย่างไร

การถ่ายโอนข้อมูลหน่วยความจำ - TreeMap

สิ่งเดียวกันไม่ได้เกิดขึ้นกับโครงสร้างข้อมูลที่ถูกแฮช ( HashMap)

HashMapนี่คือกราฟเมื่อใช้

การถ่ายโอนข้อมูลหน่วยความจำ - HashMap

การแก้ไขปัญหา? เพียงแค่บันทึกโดยตรงที่สำคัญ / ค่า (ตามที่คุณอาจจะทำอยู่แล้ว) Map.Entryมากกว่าการประหยัด


ฉันได้เขียนเป็นมาตรฐานอย่างกว้างขวางมากขึ้นที่นี่


11

เธรดจะไม่ถูกรวบรวมจนกว่าจะสิ้นสุด พวกเขาทำหน้าที่เป็นรากของการเก็บขยะ พวกเขาเป็นหนึ่งในไม่กี่วัตถุที่จะไม่ถูกเรียกคืนเพียงแค่ลืมเกี่ยวกับพวกเขาหรือลบล้างการอ้างอิงถึงพวกเขา

พิจารณา: รูปแบบพื้นฐานเพื่อยุติเธรดผู้ทำงานคือการตั้งค่าตัวแปรเงื่อนไขบางอย่างที่เธรดเห็น เธรดสามารถตรวจสอบตัวแปรเป็นระยะและใช้เป็นสัญญาณเพื่อยกเลิก หากไม่ได้ประกาศตัวแปร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จะไม่ตาย

(* * * * * แก้ไข)


1
@Spidey ไม่มีอะไร "ติด" วิธีการโทรกลับทันทีและวัตถุที่ผ่านจะไม่ถูกเรียกคืน นั่นคือการรั่วไหลที่แม่นยำ
Boann

1
คุณจะมีเธรด "กำลังทำงาน" (หรือกำลังนอนอยู่ก็ตาม) ตลอดช่วงอายุของโปรแกรมของคุณ นั่นไม่นับว่าเป็นการรั่วสำหรับฉัน สระว่ายน้ำจะไม่นับเป็นการรั่วไหลแม้ว่าคุณจะไม่ได้ใช้ก็ตาม
Spidey

1
@Spidey "คุณจะมี [สิ่ง] ตลอดอายุการใช้งานของโปรแกรมของคุณซึ่งไม่นับเป็นการรั่วไหลของฉัน" คุณได้ยินเสียงตัวเองเหรอ?
Boann

3
@Spidey หากคุณจะนับหน่วยความจำที่กระบวนการรู้ว่าไม่รั่วไหลออกมาคำตอบทั้งหมดที่นี่ผิดเพราะกระบวนการจะติดตามว่าหน้าใดในพื้นที่ที่อยู่เสมือนของมันถูกแมป เมื่อกระบวนการยุติลงระบบปฏิบัติการจะล้างการรั่วไหลทั้งหมดโดยการวางหน้ากลับลงบนสแต็กหน้าว่าง เพื่อนำไปสู่จุดสูงสุดถัดไปเราสามารถเอาชนะการรั่วไหลของการโต้เถียงโดยชี้ให้เห็นว่าไม่มีบิตทางกายภาพในชิปแรมหรือในพื้นที่สว็อปบนดิสก์ถูกวางหรือทำลายทางกายภาพดังนั้นคุณจึงสามารถปิดคอมพิวเตอร์ได้ และอีกครั้งเพื่อล้างการรั่วไหลใด ๆ
Boann

1
คำจำกัดความเชิงปฏิบัติของการรั่วไหลคือหน่วยความจำที่หายไปจากการติดตามที่เราไม่รู้และไม่สามารถดำเนินการตามขั้นตอนที่จำเป็นในการเรียกคืนได้ เราจะต้องทำลายและสร้างพื้นที่หน่วยความจำทั้งหมด หัวข้อโกงเช่นนี้อาจเกิดขึ้นตามธรรมชาติผ่านการหยุดชะงักหรือหลบการใช้งานเธรดพูล วัตถุที่อ้างอิงโดยเธรดดังกล่าวแม้ในขณะนี้จะถูกป้องกันไม่ให้ถูกรวบรวมดังนั้นเราจึงมีหน่วยความจำซึ่งจะไม่ถูกเรียกคืนหรือนำกลับมาใช้ใหม่ตามธรรมชาติในช่วงอายุการใช้งานของโปรแกรม ฉันจะเรียกว่าเป็นปัญหา โดยเฉพาะมันเป็นหน่วยความจำรั่ว
Boann

10

ฉันคิดว่าตัวอย่างที่ถูกต้องอาจใช้ตัวแปร ThreadLocal ในสภาพแวดล้อมที่รวมเธรด

ตัวอย่างเช่นการใช้ตัวแปร ThreadLocal ใน Servlets เพื่อสื่อสารกับเว็บคอมโพเนนต์อื่น ๆ โดยมีเธรดที่สร้างขึ้นโดยคอนเทนเนอร์และดูแลรักษา idle ในพูล ตัวแปร ThreadLocal หากไม่ได้รับการทำความสะอาดอย่างถูกต้องจะอยู่ที่นั่นจนกว่าองค์ประกอบเว็บเดียวกันจะเขียนทับค่าของพวกเขา

แน่นอนเมื่อพบปัญหาสามารถแก้ไขได้อย่างง่ายดาย


10

ผู้สัมภาษณ์อาจมองหาวิธีการอ้างอิงแบบวงกลม:

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

นี่เป็นปัญหาคลาสสิกที่มีตัวนับขยะอ้างอิง จากนั้นคุณจะอธิบายอย่างสุภาพว่า JVM ใช้อัลกอริทึมที่ซับซ้อนมากขึ้นซึ่งไม่มีข้อ จำกัด นี้

- ทาร์ล


12
ปัญหานี้เป็นปัญหาคลาสสิกที่มีการอ้างอิงการนับสะสมขยะ แม้ 15 ปีที่แล้ว Java ไม่ได้ใช้การนับการอ้างอิง อ้าง การนับยังช้ากว่า GC
bestsss

4
ไม่ใช่ความจำรั่ว เพียงแค่วงวนไม่สิ้นสุด
Esben Skov Pedersen

2
@Esben ในการทำซ้ำแต่ละครั้งรายการก่อนหน้าfirstนี้ไม่มีประโยชน์และควรเก็บรวบรวมขยะ ในการอ้างอิงการสะสมขยะนับวัตถุจะไม่ถูกปลดปล่อยเพราะมีการอ้างอิงที่ใช้งานอยู่ (โดยตัวมันเอง) ลูปไม่มีที่สิ้นสุดอยู่ที่นี่เพื่อบอกเลิกการรั่วไหล: เมื่อคุณเรียกใช้โปรแกรมหน่วยความจำจะเพิ่มขึ้นเรื่อย ๆ
rds

@rds @ เวสลีย์ Tarle คิดว่าวงก็ไม่ได้ไม่มีที่สิ้นสุด จะยังมีหน่วยความจำรั่วหรือไม่
nz_21
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.