Mengapa membaca file ke dalam memori membutuhkan 4x memori di Java?

Saya memiliki kode berikut yang terbaca di file berikut, tambahkan \r\n ke akhir setiap baris dan letakkan hasilnya di buffer string:

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());
}

Saya mencoba membaca dalam file 400Mb, dan saya mengubah ruang tumpukan maksimal menjadi 2Gb namun masih memberikan pengecualian tumpukan memori habis. Ada ide?


person erotsppa    schedule 06.07.2009    source sumber
comment
jika Anda hanya mencoba mengonversi file dari format unix ke windows, saya sarankan Anda menggunakan perintah unix2dos yang tersedia di beberapa tempat (standar pada kebanyakan linux, termasuk dalam cygwin, dll)   -  person rmeador    schedule 07.07.2009
comment
Konversi streaming masih dapat dilakukan menggunakan java, hanya saja jangan menggabungkan strLine ke dalam dudukannya tetapi langsung mencetaknya ke FileOutputStream. Bisakah Anda menunjukkan kepada kami di mana poin MemExc?   -  person akarnokd    schedule 09.07.2009


Jawaban (9)


Ini pertanyaan yang menarik, tapi daripada menekankan mengapa Java menggunakan begitu banyak memori, mengapa tidak mencoba desain yang tidak mengharuskan program Anda memuat seluruh file ke dalam memori?

person Chris W. Rea    schedule 06.07.2009
comment
Saya terkejut saya mendapat suara negatif pada tanggapan ini. Sungguh, terkadang kami para pengembang membuang-buang waktu untuk mencoba mencari tahu mengapa cara tertentu dalam melakukan sesuatu tidak berjalan sesuai harapan, padahal kami mungkin harus mundur dan mencoba pendekatan yang berbeda. Saya pikir setiap kali seseorang berurusan dengan file yang sangat besar dan memuat semuanya ke dalam memori, pertanyaan pertama adalah mengapa? - person Chris W. Rea; 07.07.2009
comment
Ketika seorang pengembang meminta solusi, jelas ada alasannya. Jangan berasumsi setiap pertanyaan yang diajukan berasal dari siswa sekolah menengah. - person erotsppa; 07.07.2009
comment
@erotsppa: Jadi...apa alasannya? - person Andy Mikula; 07.07.2009
comment
@erotsppa: Setuju. Itu sebabnya saya bertanya mengapa tidak, bukannya menyatakan Anda harus melakukannya. Saya sendiri mengajukan pertanyaan mengapa pendekatan lain tidak dipertimbangkan. Jangan berasumsi setiap jawaban merendahkan :-) - person Chris W. Rea; 07.07.2009
comment
Anda tidak harus menjadi siswa sekolah menengah untuk terjebak dalam detail dan kehilangan gambaran yang lebih besar/solusi alternatif.. - person Andrew Coleson; 07.07.2009
comment
@Andreas_D: Tidak setuju. Dapat menyelesaikan suatu masalah tanpa langsung menjawab pertanyaannya. Seringkali pertanyaannya adalah masalahnya! - person Chris W. Rea; 07.07.2009
comment
@Andreas_D: Sangat tidak setuju. Saya pikir tanggapan cwrea valid, dan suara Anda yang tidak setuju harus dibatalkan. - person duffymo; 07.07.2009
comment
Anda dapat bertaruh bahwa tidak akan ada suara negatif untuk jawaban cwrea jika Jon Skeet memposting hal yang sama. - person duffymo; 07.07.2009
comment
@duffymo: Tidak, tolong jangan membatalkan... Saya dengan senang hati menerima kritik, itu bagian dari apa yang membuat komunitas berfungsi :-) - person Chris W. Rea; 07.07.2009
comment
Saya pikir ini adalah poin yang valid tetapi tidak memberikan banyak/bantuan apa pun sehingga 6 suara positif tampaknya agak berlebihan. - person Adamski; 07.07.2009
comment
@Andreas_D: Tergantung jika Anda melihat pertanyaannya sebagai mengapa saya melihat pengecualian ini dan bukan bagaimana saya bisa menghindari pengecualian ini. Jika pertanyaannya adalah yang terakhir, maka jawaban yang menyarankan untuk mendesain ulang program untuk menghindari konsumsi memori yang besar akan berguna. Menawarkan penjelasan tentang internal Java tidak akan membantu OP dengan fakta bahwa apa pun penyesuaian mikro mereka, pendekatan mendasar dalam memuat file ke memori memiliki sisi buruk efek: program tidak akan berkembang dan pada akhirnya akan menemui jalan buntu, meskipun ada penyetelan mikro. - person Chris W. Rea; 07.07.2009
comment
Ini bukan jawaban tapi komentar yang sangat berguna. Seharusnya ada di bagian komentar, bukan di bagian jawaban dan tidak boleh di-upvote (karena tidak menjawab pertanyaan) bit .ly/MohSi - person OscarRyz; 07.07.2009
comment
@cwrea: Saya berpendapat bahwa sulit untuk menilai apakah pendekatan ini pada dasarnya salah (dan bahwa program akan menemui jalan buntu) tanpa mengetahui lebih banyak tentang aplikasinya. Bisa jadi aplikasi hanya membaca/menyimpan satu file di memori, mesin host dapat memiliki memori 256 Gb, ukuran file tidak akan melebihi X, dll. - person Adamski; 07.07.2009
comment
@Adamski: Setuju, itu lagi kenapa saya tanya kenapa tidak [...]? Saya tidak hanya mengutarakan jawaban saya dalam bentuk pertanyaan karena saya terlalu banyak menonton Jeopardy! :-) - person Chris W. Rea; 07.07.2009
comment
Lihatlah nilai pengembalian metode ini - pendekatan ini pada dasarnya salah dengan kepastian hampir 100% dan ini adalah satu-satunya jawaban yang masuk akal. - person Michael Borgwardt; 07.07.2009
comment
Jawabannya mengatasi masalah, mungkin bukan pertanyaan spesifiknya, tapi siapa yang peduli jika jawaban tersebut memecahkan masalah yang ada? Sejauh ini bukan inti dari omong kosong SO, rupanya pengguna SO tidak setuju karena ini adalah jawaban dengan suara terbanyak. - person Ed S.; 07.07.2009
comment
Meskipun menurut saya jawaban yang menawarkan pendekatan lain terhadap suatu masalah... Saya rasa ini sering kali berguna untuk menjawab pertanyaan sebenarnya. Jauh lebih baik untuk memahami mengapa satu pendekatan lebih baik daripada pendekatan lainnya, daripada hanya mengambil pendekatan lain karena pendekatan tersebut berhasil. Saya pikir OP mungkin perlu mempertimbangkan desain yang berbeda, tetapi pada dasarnya mencoba memahami hal-hal tentang memori di java. Pelajaran dari kode yang diposting akan terbukti bermanfaat di masa depan. Saya rasa jawaban seperti ini tidak sepenuhnya salah, namun saya tentu berharap ini tidak menjadi jawaban yang diterima. @Ed Swangren: tidak lagi :-). - person Tom; 07.07.2009
comment
Ini mungkin bukan jawaban yang diterima atau teratas, tetapi akan menjadi jawaban yang paling banyak dikomentari, LOL! - person Chris W. Rea; 07.07.2009
comment
Izinkan saya menjelaskannya seperti ini; jika saya mengajukan pertanyaan, dan beberapa pengguna SO mengatakan hei, Anda melakukan semuanya dengan salah pada awalnya, coba ini!, dan saya melakukannya, dan itu berfungsi dengan baik, saya senang. - person Ed S.; 07.07.2009

Ini mungkin ada hubungannya dengan bagaimana StringBuffer diubah ukurannya ketika mencapai kapasitas - Ini melibatkan pembuatan char[] baru yang berukuran dua kali lipat dari yang sebelumnya dan kemudian menyalin konten ke dalam array baru. Bersama dengan poin yang telah dibuat tentang karakter di Java yang disimpan sebagai 2 byte, hal ini pasti akan menambah penggunaan memori Anda.

Untuk mengatasi hal ini, Anda dapat membuat StringBuffer dengan kapasitas yang cukup untuk memulai, mengingat Anda mengetahui ukuran file (dan karenanya perkiraan jumlah karakter untuk dibaca). Namun, berhati-hatilah karena alokasi array juga akan terjadi jika Anda mencoba mengubah StringBuffer besar ini menjadi String.

Hal lain: Anda biasanya sebaiknya memilih StringBuilder daripada StringBuffer karena pengoperasiannya lebih cepat.

Anda dapat mempertimbangkan untuk mengimplementasikan "CharBuffer" Anda sendiri, misalnya menggunakan LinkedList dari char[] untuk menghindari operasi alokasi/penyalinan array yang mahal. Anda dapat membuat kelas ini mengimplementasikan CharSequence dan mungkin menghindari konversi ke String sama sekali. Saran lain untuk representasi yang lebih ringkas: Jika Anda membaca teks bahasa Inggris yang berisi banyak kata berulang, Anda dapat membaca dan menyimpan setiap kata, menggunakan fungsi String.intern() untuk mengurangi penyimpanan secara signifikan.

person Adamski    schedule 06.07.2009
comment
Saat membuat char[] baru yang berukuran dua kali lipat dari ukuran sebelumnya, apakah semua memori dialokasikan sekaligus?? Misalkan char[] sebelumnya adalah 1GB, ia akan segera mencoba mengalokasikan memori untuk 2Gb? Atau kapan sebenarnya terisi? - person erotsppa; 07.07.2009
comment
Ini hanya akan mengalokasikan array baru ketika array lama sudah penuh. - person Adamski; 07.07.2009
comment
jadi array lama adalah 1GB, array lama menjadi penuh, membuat array baru 2GB menyalin array 1GB ke array 2GB (namun saat ini Anda memiliki memori 3GB di tangan Anda) 1GB kehilangan referensi menunggu pengumpulan sampah, array 2GB menjadi penyimpanan baru dan tersisa ruang (menjadi 1GB sejak 1GB pertama disalin dari array lama) mulai digunakan. - person Sekhat; 07.07.2009
comment
Tepat sekali - Ini benar-benar pembunuh. - person Adamski; 07.07.2009
comment
Jadi jawabannya seperti use initial capacity = file.size() ? jika memungkinkan? - person OscarRyz; 07.07.2009
comment
Tampaknya setidaknya file.size() * 2, ditambah jumlah baris baru (untuk \r tambahan yang dimasukkan). - person Yishai; 07.07.2009
comment
Ya, jika Anda mengetahui ukurannya terlebih dahulu (yang mana harus menambah karakter yang ditambahkan), mengalokasikan ukuran penuh terlebih dahulu adalah ide yang bagus. - person Michael Borgwardt; 07.07.2009
comment
@Adamski, @Yishai: Mengapa file.size() * 2? Kapasitas StringBuffer dihitung dalam karakter, bukan byte, dan hampir tidak ada lebih banyak karakter dalam file daripada jumlah byte (dengan asumsi tidak ada pengkodean eksotik yang digunakan). Kapasitas awal file.size() + expectedLineCount * 2 akan lebih ekonomis. - person gustafc; 07.07.2009
comment
@Gustafc - Maaf; kamu benar. Saya akan menghapus komentar saya agar tidak menimbulkan kebingungan. - person Adamski; 07.07.2009
comment
@Adamski: Anda biasanya tidak memilih StringBuilder daripada StringBuffer karena lebih cepat. Lebih khusus lagi, StringBuffer lebih lambat karena threadsafe. StringBuilder tidak aman untuk thread. Jika Anda tidak berurusan dengan banyak thread, Anda harus menggunakan StringBuilder karena lebih cepat. - person Tom; 07.07.2009
comment
@Tom: Terima kasih - Maksud saya menulis lebih cepat, karena tidak melakukan sinkronisasi.. - person Adamski; 07.07.2009

Untuk memulai dengan string Java adalah UTF-16 (yaitu 2 byte per karakter), jadi dengan asumsi file input Anda adalah ASCII atau format satu byte per karakter serupa maka holder akan berukuran ~2x ukuran data input, ditambah tambahan \r\n per baris dan overhead tambahan apa pun. Langsung ada ~800MB, dengan asumsi overhead penyimpanan yang sangat rendah di StringBuffer.

Saya juga percaya bahwa konten file Anda di-buffer dua kali - sekali di level I/O dan sekali di BufferedReader.

Namun, untuk mengetahui dengan pasti, mungkin yang terbaik adalah melihat apa yang sebenarnya ada di heap - gunakan alat seperti HPROF untuk melihat secara pasti ke mana perginya memori Anda.

Saya ingin menyelesaikan ini, saya sarankan Anda memproses baris demi baris, menuliskan setiap baris setelah Anda menambahkan penghentian baris. Dengan begitu, penggunaan memori Anda harus sebanding dengan panjang satu baris, bukan keseluruhan file.

person DaveR    schedule 06.07.2009
comment
Saya sudah mempertimbangkannya, tetapi masih belum menjelaskan mengapa melampaui 2Gb (dan mungkin lebih, belum diuji melewati 2Gb) - person erotsppa; 07.07.2009
comment
Aplikasi Anda memiliki tumpukan yang jauh lebih sedikit daripada 2Gb. misalnya pada Windows ruang alamat dari satu proses hanya 2Gb secara default. Dalam 2Gb itu Anda harus menyesuaikan pemetaan untuk semua .dll, java vm mungkin menyediakan ruang untuk dirinya sendiri, dll. Di bagian yang tersisa, Anda akan mengalami fragmentasi memori - mencegah realokasi objek BESAR - seperti array Anda dari dialokasikan kembali (yang perlu menyalin semuanya lalu membebaskan yang asli) karena tidak ada cukup tempat untuk benda sebesar itu - hanya lubang kecil ruang kosong yang dapat menampung benda-benda kecil. - person nos; 08.07.2009

Anda memiliki sejumlah masalah di sini:

  • Unicode: karakter membutuhkan ruang dua kali lebih banyak di memori dibandingkan di disk (dengan asumsi pengkodean 1 byte)
  • Mengubah ukuran StringBuffer: dapat menggandakan (secara permanen) dan melipatgandakan (sementara) memori yang terisi, meskipun ini adalah kasus terburuk
  • StringBuffer.toString() untuk sementara menggandakan memori yang terisi sejak membuat salinan

Gabungan semua ini berarti Anda dapat memerlukan RAM sementara hingga 8 kali ukuran file Anda, yaitu 3,2G untuk file 400M. Meskipun mesin Anda secara fisik memiliki RAM sebanyak itu, mesin tersebut harus menjalankan OS 64bit dan JVM agar benar-benar mendapatkan tumpukan sebanyak itu untuk JVM.

Secara keseluruhan, menyimpan String sebesar itu di memori adalah ide yang buruk - dan itu sama sekali tidak diperlukan - karena metode Anda mengembalikan InputStream, yang Anda perlukan hanyalah FilterInputStream yang menambahkan jeda baris dengan cepat.

person Michael Borgwardt    schedule 06.07.2009
comment
Bagaimana cara mengimplementasikan subkelas FilterInputStream yang menambahkan jeda baris dengan cepat? - person erotsppa; 07.07.2009
comment
Cukup perluas FilterInputStream dan timpa metode read()-nya untuk mendeteksi jeda baris dan kembalikan \r\n sebelum melanjutkan dengan aliran yang mendasarinya. Akan menjadi sedikit rumit jika Anda ingin mendukung tandai/reset, tetapi Anda mungkin tidak memerlukannya. - person Michael Borgwardt; 07.07.2009
comment
Pertanyaan lain: apa yang sebenarnya ingin Anda capai? Normalisasikan jeda baris? Tampaknya hanya itulah yang sebenarnya dilakukan oleh metode ini. - person Michael Borgwardt; 07.07.2009
comment
StringBuffer.toString() tidak selalu membuat salinan. Ini copy-on-write, yang berarti penyalinan ditunda hingga Anda memodifikasi StringBuffer lagi. - person finnw; 07.07.2009
comment
Sumber JDK 1.6.0u12 saya tidak setuju dengan Anda. - person Michael Borgwardt; 07.07.2009
comment
Michael Borgwardt: metode baca mana yang harus ditimpa? Ada banyak. Bisakah Anda memberikan kode contoh? - person erotsppa; 07.07.2009
comment
Anda harus menimpa semuanya, tetapi Anda dapat meminta yang berbasis array memanggil yang tanpa parameter dan yang terakhir berisi semua logika Anda. - person Michael Borgwardt; 07.07.2009
comment
read() mengembalikan satu int, jadi bagaimana saya bisa mengembalikan \r\n? - person erotsppa; 07.07.2009
comment
Dengan mengingat (dalam bidang objek) apakah Anda baru saja menemukan baris baru dan kemudian mengembalikan karakter ini dalam panggilan berturut-turut. - person Michael Borgwardt; 08.07.2009
comment
Oke terakhir, bagaimana saya harus mengimplementasikan logika berbasis array di atas logika tanpa parameter? - person erotsppa; 08.07.2009
comment
Sudahlah, saya menyalin dari kode sumber Java. Tidak yakin apakah ini cara terbaik untuk melakukannya. - person erotsppa; 08.07.2009

Itu adalah StringBuffer. Konstruktor kosong membuat StringBuffer dengan panjang awal 16 Bytes. Sekarang jika Anda menambahkan sesuatu dan kapasitasnya tidak mencukupi, ia akan menyalin Array dari String Array internal ke buffer baru.

Jadi sebenarnya, dengan setiap baris yang ditambahkan, StringBuffer harus membuat salinan Array internal lengkap yang hampir menggandakan memori yang diperlukan saat menambahkan baris terakhir. Bersama dengan representasi UTF-16, hal ini menghasilkan permintaan memori yang diamati.

Edit

Michael benar, ketika mengatakan, bahwa buffer internal tidak bertambah dalam porsi kecil - masing-masing ukurannya kira-kira dua kali lipat karena Anda memerlukan lebih banyak memori. Namun tetap saja, dalam kasus terburuk, katakanlah buffer perlu menambah kapasitas hanya dengan penambahan terakhir, ia akan membuat array baru dua kali ukuran sebenarnya - jadi dalam kasus ini, untuk sesaat Anda memerlukan kira-kira tiga kali lipat jumlahnya memori.

Bagaimanapun, saya telah mendapat pelajaran: StringBuffer (dan Builder) dapat menyebabkan kesalahan OutOfMemory yang tidak terduga dan saya akan selalu menginisialisasinya dengan ukuran, setidaknya ketika saya harus menyimpan String yang besar. Terima kasih atas pertanyaannya :)

person Andreas Dolk    schedule 06.07.2009
comment
-1 tidak benar; StringBuffer akan berlipat ganda ukurannya ketika ukuran saat ini tidak mencukupi, tidak sedikit demi sedikit. - person Michael Borgwardt; 07.07.2009
comment
@Andreas, saya hanya memiliki JDK 1.5, tetapi dokumen java publik mengatakan bahwa kapasitas ditingkatkan setidaknya dua kali lipat, jadi saya rasa mereka tidak mengubahnya. Periksa metode sureCapacity. Bisa jadi Anda salah membacanya. - person Yishai; 07.07.2009
comment
Tidak, perbedaannya adalah antara panjang rangkaian karakter abstrak, yang tentu saja bertambah persis dengan jumlah karakter yang ditambahkan, dan ukuran array yang mendasarinya, yang mungkin jauh lebih besar dan diperluas dalam langkah-langkah besar untuk mengurangi jumlah penyalinan. - person Michael Borgwardt; 07.07.2009

Pada penyisipan terakhir ke dalam StringBuffer, Anda memerlukan alokasi memori tiga kali lipat, karena StringBuffer selalu bertambah (ukuran + 1) * 2 (yang sudah dua kali lipat karena unicode). Jadi file 400GB memerlukan alokasi 800GB * 3 == 2,4GB di akhir penyisipan. Mungkin ada sesuatu yang kurang, itu tergantung pada kapan tepatnya ambang batas tersebut tercapai.

Saran untuk menggabungkan String daripada menggunakan Buffer atau Builder ada di sini. Akan ada banyak pengumpulan sampah dan pembuatan objek (sehingga akan lambat), namun jejak memorinya jauh lebih rendah.

[Atas permintaan Michael, saya menyelidiki ini lebih jauh, dan concat tidak akan membantu di sini, karena menyalin buffer char, jadi meskipun tidak memerlukan triple, pada akhirnya akan memerlukan memori dua kali lipat.]

Anda dapat terus menggunakan Buffer (atau lebih baik lagi Builder dalam hal ini) jika Anda mengetahui ukuran maksimum file dan menginisialisasi ukuran Buffer saat pembuatan dan Anda yakin metode ini hanya akan dipanggil dari satu thread pada satu waktu. .

Namun sebenarnya pendekatan memuat file sebesar itu ke dalam memori sekaligus hanya boleh dilakukan sebagai upaya terakhir.

person Yishai    schedule 06.07.2009
comment
Wow, pertanyaan ini menghasilkan banyak suara negatif pada jawabannya. Namun jika Anda memberi suara negatif, setidaknya sebutkan alasannya. - person Yishai; 07.07.2009
comment
Menggunakan penggabungan string akan memakan waktu yang SANGAT lama. Sangat mungkin bertahun-tahun. Tidak, saya tidak melebih-lebihkan. - person Michael Borgwardt; 07.07.2009

Saya menyarankan Anda menggunakan cache file OS daripada menyalin data ke memori Java melalui karakter dan kembali ke byte lagi. Jika Anda membaca ulang file sesuai kebutuhan (mungkin mengubahnya seiring berjalannya waktu), file tersebut akan lebih cepat dan kemungkinan besar akan lebih sederhana

Anda memerlukan lebih dari 2 GB karena huruf 1 byte menggunakan char (2-byte) di memori dan ketika StringBuffer Anda diubah ukurannya, Anda memerlukan dua kali lipat (untuk menyalin array lama ke array baru yang lebih besar) Array baru biasanya 50% lebih besar sehingga Anda perlu hingga 6x ukuran file aslinya. Jika kinerjanya tidak cukup buruk, Anda menggunakan StringBuffer daripada StringBuilder yang menyinkronkan setiap panggilan ketika jelas-jelas tidak diperlukan. (Ini hanya memperlambat Anda, tetapi menggunakan jumlah memori yang sama)

person Peter Lawrey    schedule 07.07.2009

Orang lain telah menjelaskan mengapa Anda kehabisan memori. Mengenai cara mengatasi masalah ini, saya sarankan menulis subkelas FilterInputStream khusus. Kelas ini akan membaca satu baris pada satu waktu, menambahkan karakter "\r\n" dan menyangga hasilnya. Setelah baris tersebut dibaca oleh konsumen FilterInputStream Anda, Anda akan membaca baris lainnya. Dengan cara ini Anda hanya memiliki satu baris dalam memori dalam satu waktu.

person David    schedule 07.07.2009

Saya juga merekomendasikan untuk memeriksa Commons IO FileUtils kelas untuk ini. Khususnya: org.apache.commons.io.FileUtils#readFileToString. Anda juga dapat menentukan pengkodean jika Anda tahu Anda hanya menggunakan ASCII.

person joeslice    schedule 07.07.2009