Apakah kebocoran memori terjadi jika MemoryStream di .NET tidak ditutup?

112

Saya memiliki kode berikut:

MemoryStream foo(){
    MemoryStream ms = new MemoryStream();
    // write stuff to ms
    return ms;
}

void bar(){
    MemoryStream ms2 = foo();
    // do stuff with ms2
    return;
}

Apakah ada kemungkinan MemoryStream yang telah saya alokasikan akan gagal dibuang nanti?

Saya mendapat tinjauan sejawat yang memaksa saya menutup ini secara manual, dan saya tidak dapat menemukan informasi untuk mengetahui apakah dia benar atau tidak.

Pembuat kode
sumber
41
Tanyakan kepada pengulas Anda secara persis mengapa menurutnya Anda harus menutupnya. Jika dia berbicara tentang praktik umum yang baik, dia mungkin pintar. Jika dia berbicara tentang melepaskan ingatan lebih awal, dia salah.
Jon Skeet

Jawaban:

60

Jika ada sesuatu yang Dapat Disposable, Anda harus selalu Buang. Anda harus menggunakan usingpernyataan dalam bar()metode Anda untuk memastikan ms2dibuang.

Ini pada akhirnya akan dibersihkan oleh pengumpul sampah, tetapi panggilan Buang selalu merupakan praktik yang baik. Jika Anda menjalankan FxCop pada kode Anda, itu akan menandainya sebagai peringatan.

Rob Prouse
sumber
16
Penggunaan panggilan blokir untuk Anda.
Nick
20
@Grauenwolf: pernyataan Anda merusak enkapsulasi. Sebagai konsumen, Anda tidak perlu peduli apakah ini tidak ada operasi: jika IDisposable, tugas Anda adalah Dispose ().
Marc Gravell
4
Ini tidak berlaku untuk kelas StreamWriter: Ini akan membuang aliran yang terhubung hanya jika Anda membuang StreamWriter - itu tidak akan pernah membuang aliran jika sampah dikumpulkan dan finalisasinya dipanggil - ini memang disengaja.
springy76
4
Saya tahu pertanyaan ini berasal dari tahun 2008, tetapi hari ini kami memiliki pustaka Tugas .NET 4.0. Dispose () tidak diperlukan dalam banyak kasus saat menggunakan Tasks. Meskipun saya setuju bahwa IDisposable seharusnya berarti "Sebaiknya Anda membuang ini setelah selesai," itu tidak berarti itu lagi.
Phil
7
Contoh lain bahwa Anda tidak boleh membuang objek IDisposable adalah HttpClient aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong. Contoh lain dari BCL di mana ada objek IDisposable dan Anda tidak perlu (atau bahkan tidak boleh) membuangnya. Itu. Ini hanya untuk mengingat bahwa biasanya ada beberapa pengecualian dari aturan umum, bahkan di BCL;)
Mariusz Pawelski
166

Anda tidak akan membocorkan apa pun - setidaknya dalam implementasi saat ini.

Memanggil Dispose tidak akan membersihkan memori yang digunakan oleh MemoryStream lebih cepat. Ini akan menghentikan streaming Anda agar tidak layak untuk panggilan Baca / Tulis setelah panggilan, yang mungkin berguna atau mungkin tidak berguna bagi Anda.

Jika Anda benar-benar yakin tidak ingin berpindah dari MemoryStream ke jenis aliran lain, tidak ada salahnya jika Anda tidak memanggil Buang. Namun, itu praktek yang umumnya baik sebagian karena jika Anda pernah melakukan perubahan untuk menggunakan Stream yang berbeda, Anda tidak ingin mendapatkan digigit oleh bug sulit menemukan karena Anda memilih jalan keluar yang mudah awal. (Di sisi lain, ada argumen YAGNI ...)

Alasan lain untuk tetap melakukannya adalah bahwa implementasi baru dapat memperkenalkan sumber daya yang akan dibebaskan di Buang.

Jon Skeet
sumber
Dalam kasus ini, fungsi mengembalikan MemoryStream karena menyediakan "data yang dapat diinterpretasikan secara berbeda bergantung pada parameter pemanggil", sehingga bisa saja berupa array byte, tetapi lebih mudah karena alasan lain untuk dilakukan sebagai MemoryStream. Jadi itu pasti bukan kelas Stream lain.
Coderer
Dalam hal ini, saya masih akan mencoba membuangnya hanya berdasarkan prinsip umum - membangun kebiasaan baik, dll - tetapi saya tidak akan terlalu khawatir jika itu menjadi rumit.
Jon Skeet
1
Jika seseorang benar-benar khawatir tentang membebaskan sumber daya secepatnya, hapus referensi segera setelah blok "using" Anda, sehingga sumber daya yang tidak terkelola (jika ada) dibersihkan, dan objek tersebut memenuhi syarat untuk pengumpulan sampah. Jika metode tersebut langsung kembali, mungkin tidak akan membuat banyak perbedaan, tetapi jika Anda terus melakukan hal lain dalam metode tersebut seperti meminta lebih banyak memori, maka itu pasti dapat membuat perbedaan.
Triynko
@Triynko Tidak terlalu benar: Lihat: stackoverflow.com/questions/574019/… untuk detailnya.
George Stocker
10
Argumen YAGNI dapat diambil dua arah - karena memutuskan untuk tidak membuang sesuatu yang diimplementasikan IDisposableadalah kasus khusus yang bertentangan dengan praktik terbaik normal, Anda dapat berargumen bahwa kasus tersebut tidak boleh Anda lakukan sampai Anda benar-benar membutuhkannya, di bawah YAGNI prinsip.
Jon Hanna
26

Ya ada sebuah kebocoran , tergantung pada bagaimana Anda mendefinisikan LEAK dan berapa banyak KEMUDIAN Anda maksud ...

Jika yang Anda maksud dengan kebocoran adalah "memori tetap dialokasikan, tidak tersedia untuk digunakan, meskipun Anda sudah selesai menggunakannya" dan yang terakhir Anda maksudkan kapan saja setelah memanggil buang, maka ya mungkin ada kebocoran, meskipun tidak permanen (mis. Untuk umur runtime aplikasi Anda).

Untuk membebaskan memori terkelola yang digunakan oleh MemoryStream, Anda perlu membatalkan referensi, dengan membatalkan referensi ke sana, sehingga memori tersebut memenuhi syarat untuk pengumpulan sampah segera. Jika Anda gagal melakukan ini, maka Anda membuat kebocoran sementara sejak Anda selesai menggunakannya, sampai referensi Anda keluar dari ruang lingkup, karena sementara itu memori tidak akan tersedia untuk alokasi.

Manfaat dari pernyataan using (lebih dari sekedar memanggil buang) adalah bahwa Anda dapat MENYATAKAN referensi Anda dalam pernyataan using. Ketika pernyataan using selesai, tidak hanya dispose dipanggil, tetapi referensi Anda keluar dari ruang lingkup, secara efektif meniadakan referensi dan membuat objek Anda memenuhi syarat untuk pengumpulan sampah segera tanpa mengharuskan Anda untuk mengingat untuk menulis kode "reference = null".

Meskipun tidak langsung membatalkan referensi bukanlah kebocoran memori "permanen" klasik, hal itu pasti memiliki efek yang sama. Misalnya, jika Anda menyimpan referensi ke MemoryStream (bahkan setelah memanggil buang), dan sedikit lebih jauh dalam metode Anda, Anda mencoba mengalokasikan lebih banyak memori ... memori yang digunakan oleh aliran memori yang masih direferensikan tidak akan tersedia kepada Anda sampai Anda membatalkan referensi atau keluar dari ruang lingkup, meskipun Anda menelepon buang dan selesai menggunakannya.

Triynko
sumber
6
Saya suka tanggapan ini. Kadang-kadang orang lupa tugas ganda menggunakan: reklamasi sumber daya yang bersemangat dan dereferensi yang bersemangat.
Kit
1
Memang, meskipun saya mendengar bahwa tidak seperti Java, compiler C # mendeteksi "kemungkinan penggunaan terakhir", jadi jika variabel ditakdirkan untuk keluar dari ruang lingkup setelah referensi terakhirnya, itu mungkin memenuhi syarat untuk pengumpulan sampah tepat setelah kemungkinan penggunaan terakhirnya .. . sebelum benar-benar keluar dari ruang lingkup. Lihat stackoverflow.com/questions/680550/explicit-nulling
Triynko
2
Pengumpul sampah dan jitter tidak bekerja seperti itu. Scope adalah konstruksi bahasa dan bukan sesuatu yang akan dipatuhi oleh runtime. Faktanya, Anda mungkin memperpanjang waktu referensi dalam memori, dengan menambahkan panggilan ke .Dispose () saat blok berakhir. Lihat ericlippert.com/2015/05/18/…
Pablo Montilla
8

Menelepon .Dispose()(atau membungkus dengan Using) tidak diperlukan.

Alasan Anda menelepon .Dispose()adalah untuk merilis sumber daya secepat mungkin .

Pikirkan dalam hal, katakanlah, server Stack Overflow, di mana kami memiliki sekumpulan memori terbatas dan ribuan permintaan masuk. Kami tidak ingin menunggu pengumpulan sampah terjadwal, kami ingin melepaskan memori itu secepatnya sehingga tersedia untuk permintaan baru yang masuk.

Jeff Atwood
sumber
24
Memanggil Buang di MemoryStream tidak akan melepaskan memori apa pun. Faktanya, Anda masih bisa mendapatkan data di MemoryStream setelah Anda menelepon Dispose - coba :)
Jon Skeet
12
-1 Meskipun benar untuk MemoryStream, sebagai saran umum, ini benar-benar salah. Buang adalah untuk melepaskan sumber daya yang tidak dikelola , seperti pegangan file atau koneksi database. Memori tidak termasuk dalam kategori itu. Anda hampir selalu harus menunggu pengumpulan sampah terjadwal untuk mengosongkan memori.
Joe
1
Apa keuntungan dari mengadopsi satu gaya pengkodean untuk mengalokasikan dan membuang FileStreamobjek dan gaya yang berbeda untuk MemoryStreamobjek?
Robert Rossney
3
FileStream melibatkan sumber daya yang tidak dikelola yang sebenarnya dapat segera dibebaskan setelah memanggil Buang. Sebaliknya, MemoryStream menyimpan array byte terkelola dalam variabel _buffer-nya, yang tidak dibebaskan pada waktu pembuangan. Faktanya, _buffer bahkan tidak dinihilkan dalam metode Dispose MemoryStream, yang merupakan IMO BUG MALU karena membatalkan referensi dapat membuat memori memenuhi syarat untuk hak GC pada waktu pembuangan. Sebagai gantinya, referensi MemoryStream yang tersisa (tetapi dibuang) masih memegang memori. Oleh karena itu, setelah Anda membuangnya, Anda juga harus membatalkannya jika masih dalam ruang lingkup.
Triynko
@ Triynko - "Oleh karena itu, setelah Anda membuangnya, Anda juga harus membatalkannya jika masih dalam ruang lingkup" - Saya tidak setuju. Jika digunakan lagi setelah panggilan ke Buang, ini akan menyebabkan NullReferenceException. Jika tidak digunakan lagi setelah Buang, tidak perlu membatalkannya; GC cukup pintar.
Joe
8

Ini sudah dijawab, tetapi saya hanya akan menambahkan bahwa prinsip menyembunyikan informasi kuno yang baik berarti Anda mungkin pada suatu saat ingin melakukan refactor:

MemoryStream foo()
{    
    MemoryStream ms = new MemoryStream();    
    // write stuff to ms    
    return ms;
}

untuk:

Stream foo()
{    
   ...
}

Ini menekankan bahwa penelepon tidak boleh peduli jenis Stream apa yang dikembalikan, dan memungkinkan untuk mengubah implementasi internal (misalnya, saat mengejek pengujian unit).

Anda kemudian akan mendapat masalah jika Anda belum pernah menggunakan Buang dalam penerapan bilah Anda:

void bar()
{    
    using (Stream s = foo())
    {
        // do stuff with s
        return;
    }
}
Joe
sumber
5

Semua aliran menerapkan IDisposable. Bungkus aliran Memori Anda dalam pernyataan penggunaan dan Anda akan baik-baik saja dan keren. Blok penggunaan akan memastikan aliran Anda ditutup dan dibuang apa pun yang terjadi.

di mana pun Anda memanggil Foo, Anda dapat melakukannya menggunakan (MemoryStream ms = foo ()) dan saya pikir Anda masih harus baik-baik saja.

Nick
sumber
1
Satu masalah yang saya alami dengan kebiasaan ini adalah Anda harus yakin bahwa streaming tidak sedang digunakan di tempat lain. Misalnya saya membuat JpegBitmapDecoder yang menunjuk ke MemoryStream dan mengembalikan Frames [0] (mengira itu akan menyalin data ke penyimpanan internalnya sendiri) tetapi menemukan bahwa bitmap hanya akan muncul 20% dari waktu - ternyata itu karena Saya membuang aliran memori.
devios1
Jika aliran memori Anda harus tetap ada (yaitu menggunakan blok tidak masuk akal), maka Anda harus memanggil Buang dan segera setel variabel ke nol. Jika Anda membuangnya, maka itu tidak dimaksudkan untuk digunakan lagi, jadi Anda juga harus segera mengaturnya ke nol. Apa yang chaiguy gambarkan terdengar seperti masalah manajemen sumber daya, karena Anda tidak boleh membagikan referensi ke sesuatu kecuali hal yang Anda serahkan bertanggung jawab untuk membuangnya, dan hal yang membagikan referensi tahu itu tidak lagi bertanggung jawab untuk itu. melakukannya.
Triynko
2

Anda tidak akan membocorkan memori, tetapi peninjau kode Anda benar untuk menunjukkan bahwa Anda harus menutup aliran Anda. Itu sopan untuk dilakukan.

Satu-satunya situasi di mana Anda mungkin membocorkan memori adalah ketika Anda secara tidak sengaja meninggalkan referensi ke aliran dan tidak pernah menutupnya. Anda masih tidak benar-benar membocorkan memori, tetapi Anda tidak perlu memperpanjang waktu yang Anda klaim untuk menggunakannya.

OwenP
sumber
1
> Anda masih belum benar-benar membocorkan memori, tetapi Anda tidak perlu memperpanjang waktu yang Anda klaim untuk menggunakannya. Apakah kamu yakin Buang tidak melepaskan memori dan memanggilnya di akhir fungsi sebenarnya dapat memperpanjang waktu tidak dapat dikumpulkan.
Jonathan Allen
2
Ya, Jonathan ada benarnya. Menempatkan panggilan ke Dispose di akhir fungsi sebenarnya dapat menyebabkan compiler berpikir bahwa Anda perlu mengakses instance stream (untuk menutupnya) sangat terlambat dalam fungsinya. Ini bisa lebih buruk daripada tidak memanggil buang sama sekali (karenanya menghindari referensi fungsi yang terlambat ke variabel aliran), karena kompiler dapat menghitung titik rilis optimal (alias "titik penggunaan terakhir yang mungkin") di awal fungsi .
Triynko
2

Saya akan merekomendasikan membungkus MemoryStream bar()dalam sebuah usingpernyataan terutama untuk konsistensi:

  • Saat ini MemoryStream tidak mengosongkan memori .Dispose(), tetapi ada kemungkinan bahwa di beberapa titik di masa mendatang mungkin, atau Anda (atau orang lain di perusahaan Anda) mungkin menggantinya dengan MemoryStream kustom Anda sendiri yang melakukannya, dll.
  • Ini membantu untuk menetapkan pola dalam proyek Anda untuk memastikan semua Aliran dibuang - garis dibuat lebih tegas dengan mengatakan "semua Aliran harus dibuang" daripada "beberapa Aliran harus dibuang, tetapi yang tertentu tidak harus dibuang" ...
  • Jika Anda pernah mengubah kode untuk memungkinkan pengembalian jenis Aliran lainnya, Anda harus mengubahnya untuk membuangnya.

Hal lain yang biasanya saya lakukan dalam kasus-kasus seperti foo()saat membuat dan mengembalikan IDisposable adalah memastikan bahwa setiap kegagalan antara membangun objek dan returnditangkap oleh pengecualian, membuang objek, dan meluncurkan kembali pengecualian:

MemoryStream x = new MemoryStream();
try
{
    // ... other code goes here ...
    return x;
}
catch
{
    // "other code" failed, dispose the stream before throwing out the Exception
    x.Dispose();
    throw;
}
Chris R. Donnelly
sumber
1

Jika sebuah objek mengimplementasikan IDisposable, Anda harus memanggil metode .Dispose setelah selesai.

Di beberapa objek, Buang artinya sama dengan Tutup dan sebaliknya, dalam hal ini, baik.

Sekarang, untuk pertanyaan khusus Anda, tidak, Anda tidak akan membocorkan memori.

Lasse V. Karlsen
sumber
3
"Harus" adalah kata yang sangat kuat. Kapan pun ada aturan, ada baiknya mengetahui konsekuensi jika melanggarnya. Untuk MemoryStream, ada sedikit konsekuensi.
Jon Skeet
-1

Saya bukan ahli .net, tapi mungkin masalahnya di sini adalah sumber daya, yaitu pegangan file, dan bukan memori. Saya kira pengumpul sampah pada akhirnya akan membebaskan aliran, dan menutup pegangan, tetapi saya pikir akan selalu menjadi praktik terbaik untuk menutupnya secara eksplisit, untuk memastikan Anda membuang konten ke disk.

Steve
sumber
MemoryStream semuanya ada di dalam memori - tidak ada pegangan file di sini.
Jon Skeet
-2

Pembuangan sumber daya yang tidak dikelola bersifat non-deterministik dalam bahasa pengumpulan sampah. Bahkan jika Anda memanggil Buang secara eksplisit, Anda sama sekali tidak memiliki kendali atas kapan memori pendukung benar-benar dibebaskan. Buang secara implisit dipanggil saat sebuah objek keluar dari ruang lingkup, apakah itu dengan keluar dari pernyataan using, atau memunculkan callstack dari metode subordinat. Ini semua dikatakan, kadang-kadang objek sebenarnya bisa menjadi pembungkus untuk sumber daya yang dikelola (misalnya file). Inilah mengapa praktik yang baik untuk menutup secara eksplisit di pernyataan akhirnya atau menggunakan pernyataan using. Bersulang


sumber
1
Tidak sepenuhnya benar. Buang dipanggil saat keluar dari pernyataan using. Buang tidak dipanggil saat objek keluar dari ruang lingkup.
Alexander Abramov
-3

MemorySteram tidak lain adalah array byte, yang merupakan obyek yang dikelola. Lupa untuk membuang atau menutup ini tidak memiliki efek samping selain di atas kepala penyelesaian.
Cukup periksa metode penyusun atau siram MemoryStream di reflektor dan akan jelas mengapa Anda tidak perlu khawatir tentang menutup atau membuangnya selain hanya untuk mengikuti praktik yang baik.

pengguna1957438
sumber
6
-1: Jika Anda akan memposting pertanyaan berusia 4+ tahun dengan jawaban yang diterima, cobalah membuatnya menjadi sesuatu yang bermanfaat.
Tieson T.