เหตุใดการอ่านไฟล์ลงในหน่วยความจำจึงใช้หน่วยความจำ 4 เท่าใน Java

ฉันมีรหัสต่อไปนี้ซึ่งอ่านในไฟล์ต่อไปนี้ ผนวก \r\n ต่อท้ายแต่ละบรรทัดและวางผลลัพธ์ไว้ในบัฟเฟอร์สตริง:

public InputStream getInputStream() throws Exception {
    StringBuffer holder = new StringBuffer();
    try{
        FileInputStream reader = new FileInputStream(inputPath);


        BufferedReader br = new BufferedReader(new InputStreamReader(reader));
        String strLine;
        //Read File Line By Line
        boolean start = true;
        while ((strLine = br.readLine()) != null)   {
            if( !start )    
                holder.append("\r\n");

            holder.append(strLine);
            start = false;
        }
        //Close the input stream
        reader.close();
    }catch (Throwable e){//this is where the heap error is caught up to 2Gb
      System.err.println("Error: " + e.getMessage());
    }


    return new StringBufferInputStream(holder.toString());
}

ฉันลองอ่านในไฟล์ขนาด 400Mb และฉันเปลี่ยนพื้นที่ฮีปสูงสุดเป็น 2Gb แต่ยังคงให้ข้อยกเว้นฮีปหน่วยความจำไม่เพียงพอ มีความคิดอะไรบ้าง?


person erotsppa    schedule 06.07.2009    source แหล่งที่มา
comment
หากคุณเพียงพยายามแปลงไฟล์จากรูปแบบ unix เป็นรูปแบบ windows ฉันขอแนะนำให้คุณใช้คำสั่ง unix2dos ที่มีอยู่ในหลายแห่ง (มาตรฐานบน linuxes ส่วนใหญ่ รวมอยู่ใน cygwin ฯลฯ )   -  person rmeador    schedule 07.07.2009
comment
การแปลงแบบสตรีมจะยังคงเป็นไปได้โดยใช้ java เพียงอย่ารวม strLine เข้ากับตัวยึด แต่พิมพ์ลงใน FileOutputStream ทันที คุณช่วยแสดงให้เราเห็นว่า MemExc ชี้ไปที่ใด?   -  person akarnokd    schedule 09.07.2009


คำตอบ (9)


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

person Chris W. Rea    schedule 06.07.2009
comment
ฉันประหลาดใจที่ฉันลงคะแนนให้กับคำตอบนี้ จริงๆ แล้ว บางครั้งนักพัฒนาของเราเสียเวลาไปกับการพยายามคิดว่าเหตุใดวิธีการบางอย่างจึงไม่ได้ผลตามที่เราหวังไว้ ในเมื่อเราควรจะถอยกลับและลองใช้แนวทางอื่น ฉันคิดว่าทุกครั้งที่ต้องจัดการกับไฟล์ขนาดใหญ่มากและโหลดข้อมูลทั้งหมดลงในหน่วยความจำ คำถามแรกควรเป็นเพราะเหตุใด - person Chris W. Rea; 07.07.2009
comment
เมื่อนักพัฒนาซอฟต์แวร์ถามหาวิธีแก้ปัญหา เห็นได้ชัดว่ามีเหตุผล อย่าคิดว่าทุกคำถามที่ถามมาจากนักเรียนมัธยมปลาย - person erotsppa; 07.07.2009
comment
@erotsppa: แล้ว... เหตุผลคืออะไร? - person Andy Mikula; 07.07.2009
comment
@erotsppa: เห็นด้วย นั่นเป็นเหตุผลที่ฉันถามว่าทำไมไม่ แทนที่จะบอกว่าควรทำอย่างหลีกเลี่ยงไม่ได้ ตัวฉันเองกำลังถามคำถามว่าเหตุใดจึงไม่พิจารณาแนวทางอื่น อย่าถือว่าทุกคำตอบวางตัว :-) - person Chris W. Rea; 07.07.2009
comment
คุณไม่จำเป็นต้องเป็นนักเรียนมัธยมปลายถึงจะจมอยู่กับรายละเอียดและพลาดภาพที่ใหญ่กว่า/แนวทางแก้ไขอื่นๆ.. - person Andrew Coleson; 07.07.2009
comment
@Andreas_D: ไม่เห็นด้วย สามารถแก้ปัญหาได้โดยไม่ต้องตอบคำถามโดยตรง บ่อยครั้งคำถามคือปัญหา! - person Chris W. Rea; 07.07.2009
comment
@Andreas_D: ไม่เห็นด้วยอย่างยิ่ง ฉันคิดว่าคำตอบของ cwrea นั้นถูกต้อง และการโหวตลงของคุณควรถูกยกเลิก - person duffymo; 07.07.2009
comment
คุณสามารถเดิมพันได้ว่าจะไม่มีการโหวตคำตอบของ cwrea หาก Jon Skeet โพสต์สิ่งเดียวกัน - person duffymo; 07.07.2009
comment
@duffymo: ไม่ โปรดอย่าละทิ้ง ... ฉันยินดียอมรับคำวิจารณ์ นั่นเป็นส่วนหนึ่งของสิ่งที่ทำให้ชุมชนใช้งานได้ :-) - person Chris W. Rea; 07.07.2009
comment
ฉันคิดว่ามันเป็นจุดที่ถูกต้อง แต่ก็ไม่ได้ให้ความช่วยเหลืออะไรมากนัก ดังนั้นการโหวต 6 ครั้งจึงดูค่อนข้างมากเกินไป - person Adamski; 07.07.2009
comment
@Andreas_D: ขึ้นอยู่กับว่าคุณดูคำถามว่าเหตุใดฉันจึงเห็นข้อยกเว้นนี้โดยเฉพาะซึ่งต่างจากที่ฉันจะหลีกเลี่ยงข้อยกเว้นนี้ได้อย่างไร หากคำถามคืออย่างหลัง คำตอบที่แนะนำให้ออกแบบโปรแกรมใหม่เพื่อหลีกเลี่ยงการใช้หน่วยความจำขนาดใหญ่ก็มีประโยชน์ การให้คำอธิบายเกี่ยวกับ Java ภายในจะไม่ช่วย OP ในความจริงที่ว่าไม่ว่าพวกเขาจะ ปรับแต่งขนาดเล็ก ก็ตาม วิธีการ พื้นฐาน ในการโหลดไฟล์ลงในหน่วยความจำก็มีด้านที่ไม่ดี เอฟเฟกต์: โปรแกรมจะไม่ปรับขนาดและจะชนกำแพงในที่สุด แม้ว่าจะมีการปรับแต่งเล็กน้อยก็ตาม - person Chris W. Rea; 07.07.2009
comment
นี่ไม่ใช่คำตอบ แต่เป็นความคิดเห็นที่มีประโยชน์มาก ควรไปในส่วนความคิดเห็นในส่วนคำตอบและไม่ควรได้รับการโหวต ( เนื่องจากไม่ได้ตอบคำถาม ) bit .ly/MohSi - person OscarRyz; 07.07.2009
comment
@cwrea: ฉันขอยืนยันว่าเป็นการยากที่จะตัดสินว่าวิธีการนี้ผิดโดยพื้นฐานหรือไม่ (และโปรแกรมจะชนกำแพง) โดยไม่ทราบเพิ่มเติมเกี่ยวกับแอปพลิเคชัน อาจเป็นได้ว่าแอปอ่าน / จัดเก็บไฟล์เดียวในหน่วยความจำเท่านั้น เครื่องโฮสต์อาจมีหน่วยความจำ 256Gb ขนาดไฟล์จะไม่เกิน X เป็นต้น - person Adamski; 07.07.2009
comment
@ Adamski: ตกลงซึ่งเป็นอีกครั้งว่าทำไมฉันถามว่าทำไมไม่ [... ]? ฉันไม่เพียงแค่ใช้คำตอบในรูปแบบของคำถามเพราะฉันดูเรื่อง Jeopardy มากเกินไป! :-) - person Chris W. Rea; 07.07.2009
comment
ดูค่าที่ส่งคืนของวิธีการ - วิธีการนี้ผิดโดยพื้นฐานโดยมีความมั่นใจเกือบ 100% และนี่เป็นคำตอบเดียวที่สมเหตุสมผล - person Michael Borgwardt; 07.07.2009
comment
คำตอบกล่าวถึงปัญหา บางทีอาจไม่ใช่คำถามเฉพาะเจาะจง แต่ใครจะสนใจว่าจะช่วยแก้ปัญหาที่เกิดขึ้นได้หรือไม่ เท่าที่นี่ไม่ใช่ประเด็นไร้สาระ SO เห็นได้ชัดว่าผู้ใช้ SO ไม่เห็นด้วยเนื่องจากนี่คือคำตอบที่ได้รับการโหวตสูงสุด - person Ed S.; 07.07.2009
comment
ในขณะที่ฉันคิดว่าคำตอบที่นำเสนอแนวทางอื่นในการแก้ปัญหา... ฉันคิดว่ามันมักจะถูกใช้เพื่อตอบคำถามจริง เป็นการดีกว่ามากที่จะเข้าใจว่าเหตุใดแนวทางหนึ่งจึงดีกว่าอีกวิธีหนึ่ง แทนที่จะใช้วิธีอื่นเพราะมันได้ผล ฉันคิดว่า OP อาจต้องพิจารณาการออกแบบที่แตกต่างออกไป แต่โดยพื้นฐานแล้วกำลังพยายามทำความเข้าใจสิ่งต่าง ๆ เกี่ยวกับหน่วยความจำในจาวา บทเรียนจากโค้ดที่โพสต์จะเป็นประโยชน์ในอนาคต ฉันไม่คิดว่าคำตอบเช่นนี้ผิดที่ไปโดยสิ้นเชิง แต่ฉันหวังว่ามันจะไม่กลายเป็นคำตอบที่ได้รับการยอมรับอย่างแน่นอน @Ed Swangren: ไม่ใช่อีกต่อไป :-) - person Tom; 07.07.2009
comment
อาจจะไม่ใช่คำตอบสุดท้ายที่ได้รับการยอมรับหรือเป็นคำตอบอันดับต้นๆ แต่จะเป็นคำตอบที่มีความคิดเห็นมากที่สุด ฮ่าๆ! - person Chris W. Rea; 07.07.2009
comment
ให้ฉันพูดแบบนี้ ถ้าฉันถามคำถาม และผู้ใช้ SO บางคนบอกว่า เฮ้ คุณทำผิดตั้งแต่แรกเลย ลองทำดูสิ! และฉันก็ทำ และมันก็ใช้งานได้ดี ฉันมีความสุข - person Ed S.; 07.07.2009

อาจเกี่ยวข้องกับการปรับขนาด StringBuffer เมื่อถึงความจุ - สิ่งนี้เกี่ยวข้องกับการสร้าง char[] ใหม่เป็นสองเท่าของขนาดก่อนหน้าแล้วคัดลอกเนื้อหาไปยังอาร์เรย์ใหม่ เมื่อรวมกับประเด็นที่ทำไปแล้วเกี่ยวกับอักขระใน Java ที่ถูกจัดเก็บเป็น 2 ไบต์ สิ่งนี้จะเพิ่มการใช้งานหน่วยความจำของคุณอย่างแน่นอน

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

อีกประเด็น: โดยทั่วไปคุณควรชอบ StringBuilder มากกว่า StringBuffer เนื่องจากการดำเนินการกับมันเร็วกว่า

คุณสามารถลองใช้ "CharBuffer" ของคุณเองได้ โดยใช้ตัวอย่าง LinkedList ของ char[] เพื่อหลีกเลี่ยงการจัดสรร / คัดลอกอาร์เรย์ที่มีราคาแพง คุณสามารถทำให้คลาสนี้ใช้งาน CharSequence และอาจหลีกเลี่ยงการแปลงเป็น String โดยสิ้นเชิง คำแนะนำอีกประการหนึ่งสำหรับการนำเสนอที่กะทัดรัดยิ่งขึ้น: หากคุณกำลังอ่านข้อความภาษาอังกฤษที่มีคำซ้ำจำนวนมาก คุณสามารถอ่านและจัดเก็บแต่ละคำได้ โดยใช้ฟังก์ชัน String.intern() เพื่อลดพื้นที่เก็บข้อมูลลงอย่างมาก

person Adamski    schedule 06.07.2009
comment
เมื่อมันสร้าง char[] ใหม่ที่เป็นสองเท่าของขนาดก่อนหน้า หน่วยความจำทั้งหมดจะถูกจัดสรรในคราวเดียวหรือไม่? สมมติว่า char[] ก่อนหน้าคือ 1GB มันจะพยายามจัดสรรหน่วยความจำสำหรับ 2Gb ทันทีหรือไม่ หรือเต็มเมื่อไร? - person erotsppa; 07.07.2009
comment
มันจะจัดสรรอาร์เรย์ใหม่เมื่ออาร์เรย์เก่าเต็มเท่านั้น - person Adamski; 07.07.2009
comment
อาเรย์เก่าคือ 1GB อาเรย์เก่าเต็มสร้างอาเรย์ใหม่ 2GB คัดลอกอาเรย์ 1GB ไปยังอาเรย์ 2GB (แต่ปัจจุบันคุณมีหน่วยความจำ 3GB อยู่ในมือ) 1GB สูญเสียการอ้างอิงที่รอการรวบรวมขยะ อาเรย์ 2GB กลายเป็นที่เก็บข้อมูลใหม่และเหลืออยู่ พื้นที่ (เป็น 1GB ตั้งแต่ 1GB แรกถูกคัดลอกจากอาเรย์เก่า) เริ่มมีการใช้งาน - person Sekhat; 07.07.2009
comment
แน่นอน - มันเป็นนักฆ่าตัวจริง - person Adamski; 07.07.2009
comment
ดังนั้นคำตอบจะเป็นเช่น use default Capacity = file.size() ? ถ้าเป็นไปได้? - person OscarRyz; 07.07.2009
comment
ดูเหมือนว่าจะเป็น file.size() * 2 อย่างน้อยบวกกับจำนวนบรรทัดใหม่ (สำหรับการแทรก \r พิเศษ) - person Yishai; 07.07.2009
comment
ใช่ หากคุณทราบขนาดล่วงหน้า (ซึ่งจะต้องเพิ่มอักขระที่เพิ่มเข้าไป) การจัดสรรขนาดเต็มล่วงหน้าเป็นความคิดที่ดี - person Michael Borgwardt; 07.07.2009
comment
@Adamski, @Yishai: ทำไม file.size() * 2? ความจุของ StringBuffer นับเป็นอักขระ ไม่ใช่ไบต์ และอาจมีอักขระในไฟล์ได้ไม่มากไปกว่าจำนวนไบต์ (สมมติว่าไม่มีการใช้การเข้ารหัสที่แปลกใหม่) ความจุเริ่มต้นที่ file.size() + expectedLineCount * 2 น่าจะประหยัดกว่า - person gustafc; 07.07.2009
comment
@Gustafc - ขอโทษ; คุณพูดถูก ฉันจะลบความคิดเห็นของฉันเพื่อไม่ให้เกิดความสับสน - person Adamski; 07.07.2009
comment
@Adamski: โดยทั่วไปคุณไม่ควรชอบ StringBuilder มากกว่า StringBuffer เพราะมันเร็วกว่า โดยเฉพาะอย่างยิ่ง StringBuffer ทำงานช้ากว่าเนื่องจากเป็นเธรดที่ปลอดภัย StringBuilder ไม่ปลอดภัยสำหรับเธรด หากคุณไม่ได้ต้องจัดการกับหลายเธรด คุณควรใช้ StringBuilder เพราะมันเร็วกว่า - person Tom; 07.07.2009
comment
@ทอม: ขอบคุณ - ฉันตั้งใจจะเขียนให้เร็วขึ้นเนื่องจากไม่มีการซิงโครไนซ์ .. - person Adamski; 07.07.2009

ในการเริ่มต้นด้วยสตริง Java คือ UTF-16 (เช่น 2 ไบต์ต่ออักขระ) ดังนั้นสมมติว่าไฟล์อินพุตของคุณคือ ASCII หรือรูปแบบหนึ่งไบต์ต่ออักขระที่คล้ายกัน ดังนั้น holder จะเป็น ~2x ขนาดของข้อมูลอินพุต บวกด้วย พิเศษ \r\n ต่อบรรทัดและค่าใช้จ่ายเพิ่มเติมใดๆ มีอยู่ประมาณ 800MB ทันที โดยถือว่ามีค่าใช้จ่ายในการจัดเก็บต่ำมากใน StringBuffer

ฉันยังเชื่อได้ว่าเนื้อหาของไฟล์ของคุณถูกบัฟเฟอร์สองครั้ง - ครั้งแรกที่ระดับ I/O และอีกครั้งใน BufferedReader

อย่างไรก็ตาม เพื่อให้ทราบแน่ชัด อาจเป็นการดีที่สุดที่จะดูว่ามีอะไรอยู่บนฮีปจริงๆ - ใช้เครื่องมือเช่น HPROF เพื่อดูว่าความทรงจำของคุณหายไปไหน

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

person DaveR    schedule 06.07.2009
comment
ฉันพิจารณาแล้ว แต่ก็ยังไม่ได้อธิบายว่าทำไมมันถึงเกิน 2Gb (และเป็นไปได้มากกว่านั้น ยังไม่ทดสอบผ่าน 2Gb) - person erotsppa; 07.07.2009
comment
แอปของคุณมีฮีปน้อยกว่า 2Gb มาก เช่น. บน Windows พื้นที่ที่อยู่ของกระบวนการเดียวคือเพียง 2Gb โดยค่าเริ่มต้น ภายใน 2Gb นั้นคุณจะต้องปรับการแมปให้เหมาะสมสำหรับ .dll ทั้งหมด java vm อาจจองพื้นที่บางส่วนสำหรับตัวเอง ฯลฯ ภายในส่วนที่เหลือ คุณจะมีการกระจายตัวของหน่วยความจำ - ป้องกันการจัดสรรใหม่ของวัตถุ BIG - เช่นอาร์เรย์ของคุณจาก ถูกจัดสรรใหม่ (ซึ่งจำเป็นต้องคัดลอกสิ่งทั้งหมดแล้วปล่อยต้นฉบับฟรี) เนื่องจากมีที่ไม่เพียงพอสำหรับสิ่งใหญ่เช่นนี้ - เป็นเพียงช่องว่างเล็ก ๆ ที่ของเล็ก ๆ สามารถใส่ได้ - person nos; 08.07.2009

คุณมีปัญหาหลายประการที่นี่:

  • Unicode: อักขระใช้พื้นที่ในหน่วยความจำเป็นสองเท่าของดิสก์ (สมมติว่ามีการเข้ารหัส 1 ไบต์)
  • การปรับขนาด StringBuffer: สามารถเพิ่มหน่วยความจำที่ถูกครอบครองเป็นสองเท่า (ถาวร) และสามเท่า (ชั่วคราว) แม้ว่านี่จะเป็นกรณีที่เลวร้ายที่สุด
  • StringBuffer.toString() เพิ่มหน่วยความจำที่ถูกครอบครองเป็นสองเท่าชั่วคราวเนื่องจากทำสำเนา

เมื่อรวมกันทั้งหมดนี้หมายความว่าคุณอาจต้องใช้ขนาดไฟล์ของคุณใน RAM สูงสุดถึง 8 เท่าเป็นการชั่วคราว เช่น 3.2G สำหรับไฟล์ 400M แม้ว่าเครื่องของคุณจะมี RAM มากขนาดนั้น แต่ก็ต้องใช้งานระบบปฏิบัติการ 64 บิตและ JVM เพื่อให้ได้ฮีปมากขนาดนั้นสำหรับ JVM

โดยรวมแล้ว มันเป็นความคิดที่น่าสยดสยองที่จะเก็บ String ขนาดใหญ่ไว้ในหน่วยความจำ - และมันไม่จำเป็นเลย เช่นกัน - เนื่องจากวิธีการของคุณส่งคืน InputStream สิ่งที่คุณต้องการจริงๆคือ FilterInputStream ที่เพิ่มการขึ้นบรรทัดใหม่ทันที

person Michael Borgwardt    schedule 06.07.2009
comment
เราควรจะดำเนินการอย่างไรเกี่ยวกับการใช้คลาสย่อย FilterInputStream ที่เพิ่มตัวแบ่งบรรทัดได้ทันที - person erotsppa; 07.07.2009
comment
เพียงขยาย FilterInputStream และเขียนทับเมธอด read() เพื่อตรวจจับการขึ้นบรรทัดใหม่และส่งคืน \r\n ก่อนที่จะดำเนินการต่อกับสตรีมที่เหลือที่เหลือ มันจะซับซ้อนเล็กน้อยถ้าคุณต้องการรองรับการมาร์ก/รีเซ็ต แต่คุณอาจไม่ต้องการสิ่งนั้น - person Michael Borgwardt; 07.07.2009
comment
คำถามอื่น: จริงๆ แล้วคุณต้องการบรรลุผลอะไร? ทำให้ตัวแบ่งบรรทัดเป็นปกติหรือไม่ นั่นดูเหมือนจะเป็นทั้งหมดที่วิธีนี้กำลังทำอยู่จริง - person Michael Borgwardt; 07.07.2009
comment
StringBuffer.toString() ไม่ได้ทำการคัดลอกเสมอไป เป็นการคัดลอกเมื่อเขียน ซึ่งหมายความว่าการคัดลอกจะล่าช้าจนกว่าคุณจะแก้ไข StringBuffer ครั้งถัดไป - person finnw; 07.07.2009
comment
แหล่งที่มา JDK 1.6.0u12 ของฉันไม่เห็นด้วยกับคุณ - person Michael Borgwardt; 07.07.2009
comment
Michael Borgwardt: วิธีอ่านใดที่จะเขียนทับ? มีมากมาย. คุณสามารถให้รหัสตัวอย่างได้หรือไม่? - person erotsppa; 07.07.2009
comment
คุณจะต้องเขียนทับทั้งหมด แต่คุณสามารถให้อันที่ใช้อาเรย์เรียกอันที่ไม่มีพารามิเตอร์และให้อันสุดท้ายมีตรรกะทั้งหมดของคุณ - person Michael Borgwardt; 07.07.2009
comment
read() ส่งคืน int เดียว ดังนั้นฉันจะสามารถส่งคืน \r\n ได้อย่างไร - person erotsppa; 07.07.2009
comment
โดยการจดจำ (ในฟิลด์อ็อบเจ็กต์) ว่าคุณเพิ่งพบการขึ้นบรรทัดใหม่แล้วส่งคืนอักขระเหล่านี้ในการเรียกติดต่อกันหรือไม่ - person Michael Borgwardt; 08.07.2009
comment
ตกลงสุดท้ายนี้ ฉันควรใช้ตรรกะของตรรกะแบบอาเรย์ที่อยู่ด้านบนของอันที่ไม่มีพารามิเตอร์ได้อย่างไร - person erotsppa; 08.07.2009
comment
ไม่เป็นไร ฉันคัดลอกมาจากซอร์สโค้ด Java ไม่แน่ใจว่านี่เป็นวิธีที่ดีที่สุดหรือไม่ - person erotsppa; 08.07.2009

มันคือ StringBuffer ตัวสร้างที่ว่างเปล่าสร้าง StringBuffer ที่มีความยาวเริ่มต้น 16 ไบต์ ตอนนี้ถ้าคุณต่อท้ายบางสิ่งและความจุไม่เพียงพอ มันจะทำ Arraycopy ของ String Array ภายในไปยังบัฟเฟอร์ใหม่

ดังนั้นในความเป็นจริง เมื่อแต่ละบรรทัดต่อท้าย StringBuffer จะต้องสร้างสำเนาของ Array ภายในที่สมบูรณ์ ซึ่งเกือบสองเท่าของหน่วยความจำที่ต้องการเมื่อต่อท้ายบรรทัดสุดท้าย เมื่อใช้ร่วมกับการแสดง UTF-16 ส่งผลให้มีความต้องการหน่วยความจำที่สังเกตได้

แก้ไข

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

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

person Andreas Dolk    schedule 06.07.2009
comment
-1 ไม่จริง; StringBuffer จะเพิ่มขนาดเป็นสองเท่าเมื่อขนาดปัจจุบันไม่เพียงพอ ไม่ใช่เพิ่มขึ้นทีละน้อย - person Michael Borgwardt; 07.07.2009
comment
@Andreas ฉันมี JDK 1.5 กับฉันเท่านั้น แต่เอกสาร java สาธารณะบอกว่าความจุเพิ่มขึ้นเป็นสองเท่าเป็นอย่างน้อยดังนั้นฉันจึงไม่คิดว่าพวกเขากำลังเปลี่ยนแปลงสิ่งนั้น ตรวจสอบวิธีการ SureCapacity อาจเป็นได้ว่าคุณกำลังอ่านผิด - person Yishai; 07.07.2009
comment
ไม่ ความแตกต่างอยู่ระหว่าง ความยาว ของลำดับนามธรรมของอักขระ ซึ่งแน่นอนว่าเพิ่มขึ้นอย่างแน่นอนด้วยจำนวนอักขระที่ต่อท้าย และ ขนาด ของอาร์เรย์พื้นฐาน ซึ่งอาจใหญ่กว่ามากและขยายเป็นขั้นตอนใหญ่เพื่อลดปริมาณการคัดลอก - person Michael Borgwardt; 07.07.2009

ในการแทรกครั้งสุดท้ายใน StringBuffer คุณต้องจัดสรรหน่วยความจำเป็นสามเท่า เนื่องจาก StringBuffer จะขยายด้วย (ขนาด + 1) * 2 เสมอ (ซึ่งเพิ่มเป็นสองเท่าอยู่แล้วเนื่องจาก Unicode) ดังนั้นไฟล์ขนาด 400GB อาจต้องมีการจัดสรร 800GB * 3 == 2.4GB ที่ส่วนท้ายของส่วนแทรก มันอาจจะน้อยกว่านั้นก็ได้ ขึ้นอยู่กับว่าเมื่อถึงเกณฑ์นั้นจริงๆ

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

[ตามคำแนะนำของ Michael ฉันได้ตรวจสอบสิ่งนี้เพิ่มเติม และ concat ไม่ได้ช่วยอะไรที่นี่ เนื่องจากมันจะคัดลอกบัฟเฟอร์ถ่าน ดังนั้นถึงแม้จะไม่ต้องการเป็นสามเท่า แต่ก็ต้องใช้หน่วยความจำสองเท่าในตอนท้าย]

คุณสามารถใช้ Buffer ต่อไปได้ (หรือดีกว่า Builder ในกรณีนี้) หากคุณทราบขนาดสูงสุดของไฟล์และเตรียมใช้งานขนาดของ Buffer ในการสร้าง และคุณแน่ใจว่าวิธีนี้จะถูกเรียกจากเธรดเดียวเท่านั้นในแต่ละครั้ง .

แต่จริงๆ แล้ววิธีการโหลดไฟล์ขนาดใหญ่ดังกล่าวลงในหน่วยความจำในครั้งเดียวควรทำเป็นทางเลือกสุดท้ายเท่านั้น

person Yishai    schedule 06.07.2009
comment
ว้าว คำถามนี้ทำให้เกิดการลงคะแนนโหวตให้กับคำตอบมากมาย แต่ถ้าคุณ downvote อย่างน้อยก็ระบุเหตุผลด้วย - person Yishai; 07.07.2009
comment
การใช้การต่อสตริงจะใช้เวลานานมาก อาจเป็นปีเลยทีเดียว ไม่ ฉันไม่ได้พูดเกินจริง - person Michael Borgwardt; 07.07.2009

ฉันขอแนะนำให้คุณใช้แคชไฟล์ OS แทนการคัดลอกข้อมูลลงในหน่วยความจำ Java ผ่านอักขระและกลับไปเป็นไบต์อีกครั้ง หากคุณอ่านไฟล์ซ้ำตามต้องการ (อาจแปลงไฟล์ตามที่คุณดำเนินการ) ไฟล์จะเร็วขึ้น และมีโอกาสง่ายกว่ามาก

คุณต้องการมากกว่า 2 GB เนื่องจากตัวอักษร 1 ไบต์ใช้ถ่าน (2 ไบต์) ในหน่วยความจำและเมื่อ StringBuffer ของคุณปรับขนาดคุณต้องเพิ่มเป็นสองเท่า (เพื่อคัดลอกอาเรย์เก่าไปยังอาเรย์ใหม่ที่ใหญ่กว่า) โดยทั่วไปอาเรย์ใหม่จะใหญ่กว่า 50% ดังนั้นคุณจึงต้องการ สูงสุด 6 เท่าของขนาดไฟล์ต้นฉบับ หากประสิทธิภาพยังไม่แย่พอ คุณกำลังใช้ StringBuffer แทน StringBuilder ซึ่งจะซิงโครไนซ์ทุกการโทรเมื่อเห็นได้ชัดว่าไม่จำเป็น (สิ่งนี้จะทำให้คุณช้าลงเท่านั้น แต่ใช้หน่วยความจำเท่ากัน)

person Peter Lawrey    schedule 07.07.2009

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

person David    schedule 07.07.2009

ฉันขอแนะนำให้ตรวจสอบ Commons IO FileUtils คลาสสำหรับสิ่งนี้ โดยเฉพาะ: org.apache.commons.io.FileUtils#readFileToString คุณยังสามารถระบุการเข้ารหัสได้หากคุณรู้ว่าคุณใช้เฉพาะ ASCII เท่านั้น

person joeslice    schedule 07.07.2009