Pertanyaan Membuat kebocoran memori dengan Java


Saya baru saja wawancara, dan saya diminta untuk membuat kebocoran memori dengan Java. Tak perlu dikatakan, saya merasa agak bodoh karena tidak tahu bagaimana cara memulai membuatnya.

Apa yang akan menjadi contoh?


2634


asal


Jawaban:


Berikut ini cara yang baik untuk membuat kebocoran memori yang sebenarnya (objek tidak dapat diakses dengan menjalankan kode tetapi masih disimpan dalam memori) di Java murni:

  1. Aplikasi ini menciptakan thread yang sudah lama berjalan (atau menggunakan kolam thread untuk bocor lebih cepat).
  2. Untaian memuat kelas melalui ClassLoader (opsional) khusus.
  3. Kelas mengalokasikan sebagian besar memori (mis. new byte[1000000]), menyimpan referensi yang kuat ke dalam bidang statis, dan kemudian menyimpan referensi ke dirinya sendiri di ThreadLocal. Mengalokasikan memori ekstra bersifat opsional (bocor contoh Kelas sudah cukup), tetapi itu akan membuat kebocoran bekerja lebih cepat.
  4. Thread menghapus semua referensi ke kelas kustom atau ClassLoader yang dimuatnya.
  5. Ulangi.

Ini berfungsi karena ThreadLocal menyimpan referensi ke objek, yang menyimpan referensi ke Kelasnya, yang pada gilirannya menyimpan referensi ke ClassLoader-nya. ClassLoader, pada gilirannya, menyimpan referensi ke semua Kelas yang telah dimuatnya.

(Itu lebih buruk di banyak implementasi JVM, terutama sebelum Java 7, karena Classes dan ClassLoaders dialokasikan langsung ke permgen dan tidak pernah sama sekali GC'd. Namun, terlepas dari bagaimana JVM menangani class unloading, ThreadLocal akan tetap mencegah Objek kelas dari direklamasi.)

Variasi pada pola ini adalah mengapa wadah aplikasi (seperti Tomcat) dapat membocorkan memori seperti saringan jika Anda sering memindahkan aplikasi yang kebetulan menggunakan ThreadLocals dengan cara apa pun. (Karena kontainer aplikasi menggunakan Threads seperti yang dijelaskan, dan setiap kali Anda memindahkan aplikasi, ClassLoader baru digunakan.)

Memperbarui: Karena banyak orang yang terus bertanya, inilah beberapa contoh kode yang menunjukkan perilaku ini dalam tindakan.


1931



Static field holding object reference [esp bidang final]

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}

Panggilan String.intern() pada String panjang

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

(Tidak ditutup) membuka aliran (file, jaringan, dll ...)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Koneksi tidak ditutup

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Area yang tidak dapat dijangkau oleh pengumpul sampah JVM, seperti memori yang dialokasikan melalui metode asli

Dalam aplikasi web, beberapa objek disimpan dalam lingkup aplikasi hingga aplikasi secara eksplisit dihentikan atau dihapus.

getServletContext().setAttribute("SOME_MAP", map);

Pilihan JVM yang salah atau tidak tepat, seperti noclassgc pilihan pada IBM JDK yang mencegah pengumpulan sampah kelas yang tidak terpakai

Lihat Pengaturan IBM jdk.


1059



Hal sederhana yang harus dilakukan adalah menggunakan HashSet dengan salah (atau tidak ada) hashCode() atau equals(), dan kemudian terus menambahkan "duplikat". Alih-alih mengabaikan duplikat sebagaimana mestinya, set hanya akan berkembang dan Anda tidak akan dapat menghapusnya.

Jika Anda ingin kunci / elemen buruk ini bertahan, Anda dapat menggunakan bidang statis seperti

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.

390



Di bawah ini akan ada kasus yang tidak jelas di mana Jawa bocor, selain kasus standar pendengar yang terlupakan, referensi statis, kunci palsu / dapat dimodifikasi dalam hashmaps, atau hanya utas yang macet tanpa ada kesempatan untuk mengakhiri siklus hidup mereka.

  • File.deleteOnExit()- selalu bocor talinya, jika string adalah substring, kebocoran bahkan lebih buruk (char yang mendasari [] juga bocor) - di Java 7 substring juga menyalin char[], jadi nanti tidak berlaku; @Daniel, tidak perlu suara.

Saya akan berkonsentrasi pada benang untuk menunjukkan bahaya sebagian besar benang yang tidak dikelola, bahkan tidak ingin menyentuh ayunan.

  • Runtime.addShutdownHook dan tidak menghapus ... dan kemudian bahkan dengan removeShutdownHook karena bug di kelas ThreadGroup mengenai thread yang belum dipetakan mungkin tidak terkumpul, secara efektif bocor ThreadGroup. JGroup memiliki kebocoran di GossipRouter.

  • Membuat, tetapi tidak memulai, a Thread masuk ke dalam kategori yang sama seperti di atas.

  • Membuat thread mewarisi ContextClassLoader dan AccessControlContext, ditambah ThreadGroup dan apa saja InheritedThreadLocal, semua referensi itu adalah kebocoran potensial, bersama dengan seluruh kelas yang dimuat oleh classloader dan semua referensi statis, dan ja-ja. Efeknya terutama terlihat dengan seluruh kerangka j.u.c.Executor yang menampilkan super sederhana ThreadFactory antarmuka, namun sebagian besar pengembang tidak memiliki petunjuk tentang bahaya yang mengintai. Juga banyak pustaka yang memulai untaian atas permintaan (terlalu banyak pustaka populer industri).

  • ThreadLocal cache; mereka jahat dalam banyak kasus. Saya yakin semua orang telah melihat cukup banyak cache sederhana berdasarkan ThreadLocal, baik berita buruk: jika thread terus berjalan lebih dari yang diharapkan dalam konteks ClassLoader, itu adalah kebocoran kecil yang bagus. Jangan gunakan cache ThreadLocal kecuali benar-benar diperlukan.

  • Panggilan ThreadGroup.destroy() ketika ThreadGroup tidak memiliki utas sendiri, tetapi tetap menyimpan ThreadGroups anak. Kebocoran yang buruk yang akan mencegah ThreadGroup untuk menghapus dari induknya, tetapi semua anak menjadi tidak terhitung.

  • Menggunakan WeakHashMap dan nilai (dalam) secara langsung mereferensikan kunci. Ini adalah yang sulit ditemukan tanpa timbunan sampah. Itu berlaku untuk semua diperpanjang Weak/SoftReference yang mungkin menyimpan referensi keras kembali ke objek yang dijaga.

  • Menggunakan java.net.URL dengan protokol HTTP (S) dan memuat sumber daya dari (!). Yang ini spesial, KeepAliveCache membuat thread baru dalam sistem ThreadGroup yang bocor classloader konteks thread saat ini. Thread dibuat atas permintaan pertama ketika tidak ada benang hidup, jadi Anda mungkin beruntung atau hanya bocor. Kebocoran sudah diperbaiki di Java 7 dan kode yang membuat untaian dengan benar menghapus classloader konteks. Ada beberapa kasus lagi (seperti ImageFetcher, juga diperbaiki) membuat utas serupa.

  • Menggunakan InflaterInputStream lewat new java.util.zip.Inflater() di konstruktor (PNGImageDecoder misalnya) dan tidak menelepon end() dari inflater. Nah, jika Anda melewati konstruktor dengan adil new, tidak ada peluang ... Dan ya, menelepon close() pada aliran tidak menutup inflater jika secara manual dilewatkan sebagai parameter konstruktor. Ini bukan kebocoran yang sebenarnya karena akan dirilis oleh finalis ... ketika dianggap perlu. Sampai saat itu memakan memori asli sangat buruk itu dapat menyebabkan Linux oom_killer untuk membunuh proses dengan impunitas. Masalah utamanya adalah bahwa finalisasi di Jawa sangat tidak dapat diandalkan dan G1 membuatnya lebih buruk hingga 7.0.2. Moral dari cerita: lepaskan sumber daya asli secepat yang Anda bisa; finalizer terlalu miskin.

  • Kasus yang sama dengan java.util.zip.Deflater. Yang satu ini jauh lebih buruk karena Deflater adalah haus memori di Java, yaitu selalu menggunakan 15 bit (max) dan 8 level memori (9 adalah max) mengalokasikan beberapa ratusan KB memori asli. Untung, Deflater tidak banyak digunakan dan sepengetahuan saya JDK tidak mengandung kesalahan. Selalu hubungi end() jika Anda membuat secara manual Deflater atau Inflater. Bagian terbaik dari dua yang terakhir: Anda tidak dapat menemukannya melalui alat profil normal yang tersedia.

(Saya dapat menambahkan lebih banyak pemboros waktu yang saya temui atas permintaan.)

Semoga beruntung dan tetap jaga keselamatan; kebocoran itu jahat!


228



Sebagian besar contoh di sini "terlalu rumit". Mereka adalah kasus tepi. Dengan contoh-contoh ini, programmer membuat kesalahan (seperti tidak mendefinisikan ulang sama / hashcode), atau telah digigit oleh kasus sudut JVM / JAVA (beban kelas dengan statis ...). Saya pikir itu bukan jenis contoh yang diinginkan pewawancara atau bahkan kasus yang paling umum.

Tetapi ada kasus yang benar-benar lebih sederhana untuk kebocoran memori. Pengumpul sampah hanya membebaskan apa yang tidak lagi dirujuk. Kami sebagai pengembang Java tidak peduli dengan memori. Kami mengalokasikannya saat dibutuhkan dan membiarkannya dibebaskan secara otomatis. Baik.

Tetapi aplikasi berumur panjang cenderung memiliki kondisi yang sama. Dapat berupa apa saja, statika, lajang ... Seringkali aplikasi yang tidak sepele cenderung membuat grafik objek yang rumit. Hanya lupa untuk mengatur referensi ke null atau lebih sering lupa untuk menghapus satu objek dari koleksi cukup untuk membuat kebocoran memori.

Tentu saja semua jenis pendengar (seperti pendengar UI), cache, atau negara bersama yang berumur panjang cenderung menghasilkan kebocoran memori jika tidak ditangani dengan benar. Apa yang harus dipahami adalah bahwa ini bukan kasus sudut Jawa, atau masalah dengan pengumpul sampah. Ini masalah desain. Kami merancang agar kami menambahkan pendengar ke objek berumur panjang, tetapi kami tidak menghapus pendengar saat tidak lagi diperlukan. Kami menyimpan objek, tetapi kami tidak memiliki strategi untuk menghapusnya dari cache.

Kami mungkin memiliki grafik kompleks yang menyimpan keadaan sebelumnya yang diperlukan oleh komputasi. Tetapi negara sebelumnya itu sendiri terkait dengan negara sebelum dan seterusnya.

Seperti kita harus menutup koneksi atau file SQL. Kita perlu mengatur referensi yang tepat untuk null dan menghapus elemen dari koleksi. Kami akan memiliki strategi cache yang tepat (ukuran memori maksimum, jumlah elemen, atau pengatur waktu). Semua objek yang memungkinkan pendengar untuk diberitahu harus menyediakan metode addListener dan removeListener. Dan ketika pemberi tahu ini tidak lagi digunakan, mereka harus menghapus daftar pendengar mereka.

Kebocoran memori memang benar-benar mungkin dan dapat diprediksi dengan sempurna. Tidak perlu fitur bahasa khusus atau kasus sudut. Kebocoran memori merupakan indikator bahwa ada sesuatu yang mungkin hilang atau bahkan masalah desain.


150



Jawabannya sepenuhnya tergantung pada apa yang dipikirkan pewawancara.

Apakah mungkin dalam prakteknya untuk membuat kebocoran Java? Tentu saja, dan ada banyak contoh dalam jawaban lainnya.

Tetapi ada beberapa meta-pertanyaan yang mungkin telah ditanyakan?

  • Apakah implementasi Java "sempurna" secara teoritis rentan terhadap kebocoran?
  • Apakah kandidat memahami perbedaan antara teori dan kenyataan?
  • Apakah kandidat memahami cara kerja pengumpulan sampah?
  • Atau bagaimana pengumpulan sampah seharusnya bekerja dalam kasus yang ideal?
  • Apakah mereka tahu mereka dapat memanggil bahasa lain melalui antarmuka asli?
  • Apakah mereka tahu untuk membocorkan memori dalam bahasa-bahasa lain itu?
  • Apakah kandidat bahkan tahu apa itu manajemen memori, dan apa yang terjadi di belakang layar di Jawa?

Saya membaca meta-pertanyaan Anda sebagai "Apa jawaban yang bisa saya gunakan dalam situasi wawancara ini". Dan karenanya, saya akan fokus pada keterampilan wawancara daripada Java. Saya percaya Anda lebih mungkin untuk mengulangi situasi tidak mengetahui jawaban atas pertanyaan dalam wawancara daripada Anda berada di tempat yang perlu tahu bagaimana membuat Java bocor. Jadi, semoga ini akan membantu.

Salah satu keterampilan paling penting yang dapat Anda kembangkan untuk wawancara adalah belajar untuk secara aktif mendengarkan pertanyaan dan bekerja dengan pewawancara untuk mengekstrak niat mereka. Tidak hanya ini membiarkan Anda menjawab pertanyaan mereka seperti yang mereka inginkan, tetapi juga menunjukkan bahwa Anda memiliki beberapa keterampilan komunikasi yang penting. Dan ketika datang ke pilihan antara banyak pengembang yang sama-sama berbakat, saya akan merekrut orang yang mendengarkan, berpikir, dan memahami sebelum mereka merespon setiap waktu.


133



Berikut ini adalah contoh yang sangat tidak berarti, jika Anda tidak mengerti JDBC. Atau setidaknya bagaimana JDBC mengharapkan pengembang untuk menutupnya Connection, Statement dan ResultSet contoh sebelum membuangnya atau kehilangan referensi ke mereka, daripada bergantung pada penerapan finalize.

void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}

Masalah dengan di atas adalah bahwa Connection objek tidak ditutup, dan karenanya koneksi fisik akan tetap terbuka, sampai pengumpul sampah datang dan melihat bahwa itu tidak dapat dijangkau. GC akan memohon finalize metode, tetapi ada driver JDBC yang tidak mengimplementasikan finalize, setidaknya tidak dengan cara yang sama Connection.close diimplementasikan. Perilaku yang dihasilkan adalah bahwa sementara memori akan direklamasi karena benda yang tidak dapat dijangkau sedang dikumpulkan, sumber daya (termasuk memori) yang terkait dengan Connection objek mungkin tidak akan direklamasi.

Dalam peristiwa semacam itu di mana Connection's finalize metode tidak membersihkan semuanya, orang mungkin benar-benar menemukan bahwa koneksi fisik ke server database akan berlangsung beberapa siklus pengumpulan sampah, sampai server database akhirnya mengetahui bahwa koneksi tidak hidup (jika tidak), dan harus ditutup.

Bahkan jika driver JDBC menerapkan finalize, mungkin pengecualian akan dibuang selama finalisasi. Perilaku yang dihasilkan adalah bahwa setiap memori yang terkait dengan objek "tidak aktif" sekarang tidak akan direklamasi, seperti finalize dijamin akan dipanggil hanya sekali.

Skenario di atas menghadapi pengecualian selama finalisasi objek terkait dengan skenario lain yang mungkin dapat menyebabkan kebocoran memori - objek kebangkitan. Kebangkitan objek sering dilakukan secara sengaja dengan menciptakan referensi yang kuat untuk objek dari yang sedang diselesaikan, dari objek lain. Ketika objek kebangkitan disalahgunakan akan menyebabkan kebocoran memori dalam kombinasi dengan sumber lain dari kebocoran memori.

Ada banyak lagi contoh yang bisa Anda bayangkan - seperti

  • Mengelola a List contoh di mana Anda hanya menambahkan ke daftar dan tidak menghapusnya (meskipun Anda harus menyingkirkan elemen yang tidak lagi Anda butuhkan), atau
  • Pembukaan Sockets atau Files, tetapi tidak menutupnya ketika mereka tidak lagi diperlukan (mirip dengan contoh di atas yang melibatkan Connection kelas).
  • Tidak membongkar Singletons ketika menurunkan aplikasi Java EE. Rupanya, Classloader yang memuat kelas tunggal akan mempertahankan referensi ke kelas, dan karenanya contoh tunggal tidak akan pernah dikumpulkan. Ketika sebuah contoh baru dari aplikasi dikerahkan, sebuah loader kelas baru biasanya dibuat, dan mantan loader kelas akan terus ada karena tunggal.

113



Mungkin salah satu contoh paling sederhana dari kebocoran memori potensial, dan bagaimana menghindarinya, adalah implementasi 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;
}

Jika Anda mengimplementasikannya sendiri, apakah Anda akan berpikir untuk menghapus elemen array yang tidak lagi digunakan (elementData[--size] = null)? Referensi itu mungkin membuat objek besar hidup ...


102