Saya baru saja wawancara, dan saya diminta membuat kebocoran memori dengan Java.
Tak perlu dikatakan, saya merasa sangat bodoh karena tidak memiliki petunjuk tentang cara bahkan mulai membuat satu.
Apa yang akan menjadi contoh?
Saya baru saja wawancara, dan saya diminta membuat kebocoran memori dengan Java.
Tak perlu dikatakan, saya merasa sangat bodoh karena tidak memiliki petunjuk tentang cara bahkan mulai membuat satu.
Apa yang akan menjadi contoh?
Jawaban:
Berikut adalah cara yang baik untuk membuat kebocoran kehabisan memori (objek tidak dapat diakses dengan menjalankan kode tetapi masih tersimpan dalam memori) di Java murni:
ClassLoader
.new byte[1000000]
), menyimpan referensi yang kuat untuk itu di bidang statis, dan kemudian menyimpan referensi untuk dirinya sendiri di a ThreadLocal
. Mengalokasikan memori tambahan adalah opsional (membocorkan instance kelas sudah cukup), tetapi itu akan membuat kebocoran bekerja lebih cepat.ClassLoader
mana itu diambil.Karena cara ThreadLocal
ini diterapkan di JDK Oracle, ini menciptakan kebocoran memori:
Thread
memiliki bidang pribadi threadLocals
, yang sebenarnya menyimpan nilai-nilai lokal thread.ThreadLocal
objek, jadi setelah ThreadLocal
objek tersebut dikumpulkan dari sampah, entri tersebut dihapus dari peta.ThreadLocal
objek yang merupakan kuncinya , objek tersebut tidak akan dikumpulkan atau dibuang dari peta selama utasnya hidup.Dalam contoh ini, rantai referensi kuat terlihat seperti ini:
Thread
objek → threadLocals
peta → contoh kelas contoh → kelas contoh → ThreadLocal
bidang statis → ThreadLocal
objek.
(The ClassLoader
tidak benar-benar berperan dalam menciptakan kebocoran, itu hanya membuat kebocoran lebih buruk karena rantai referensi tambahan ini: contoh kelas → ClassLoader
→ semua kelas yang telah dimuat. Itu bahkan lebih buruk di banyak implementasi JVM, terutama sebelum Java 7, karena kelas dan kelas ClassLoader
dialokasikan langsung ke permgen dan tidak pernah dikumpulkan sama sekali.)
Variasi pada pola ini adalah mengapa wadah aplikasi (seperti Tomcat) dapat membocorkan memori seperti saringan jika Anda sering menggunakan kembali aplikasi yang kebetulan menggunakan ThreadLocal
s yang dengan cara tertentu kembali ke diri mereka sendiri. Ini dapat terjadi karena sejumlah alasan halus dan seringkali sulit untuk di-debug dan / atau diperbaiki.
Pembaruan : Karena banyak orang terus memintanya, inilah beberapa contoh kode yang menunjukkan perilaku ini dalam tindakan .
Referensi objek memegang bidang statis [esp bidang terakhir]
class MemorableClass {
static final ArrayList list = new ArrayList(100);
}
Memanggil String.intern()
String yang 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 tertutup) aliran terbuka (file, jaringan dll ...)
try {
BufferedReader br = new BufferedReader(new FileReader(inputFile));
...
...
} catch (Exception e) {
e.printStacktrace();
}
Koneksi tidak tertutup
try {
Connection conn = ConnectionFactory.getConnection();
...
...
} catch (Exception e) {
e.printStacktrace();
}
Area yang tidak dapat dijangkau dari pengumpul sampah JVM , seperti memori yang dialokasikan melalui metode asli
Dalam aplikasi web, beberapa objek disimpan dalam ruang lingkup aplikasi hingga aplikasi secara eksplisit dihentikan atau dihapus.
getServletContext().setAttribute("SOME_MAP", map);
Opsi JVM salah atau tidak tepat , seperti noclassgc
opsi pada IBM JDK yang mencegah pengumpulan sampah kelas yang tidak digunakan
Lihat pengaturan IBM jdk .
close()
biasanya tidak dipanggil dalam finalizer utas karena mungkin operasi pemblokiran). Ini adalah praktik buruk untuk tidak menutup, tetapi tidak menyebabkan kebocoran. Java.sql.Connection tertutup adalah sama.
intern
konten hashtable -nya . Dengan demikian, ini adalah sampah yang dikumpulkan dengan benar dan bukan kebocoran. (but IANAJP) mindprod.com/jgloss/interned.html#GC
Hal sederhana yang harus dilakukan adalah menggunakan HashSet dengan yang salah (atau tidak ada) hashCode()
atau equals()
, dan kemudian terus menambahkan "duplikat". Alih-alih mengabaikan duplikat sebagaimana mestinya, set hanya akan pernah tumbuh dan Anda tidak akan dapat menghapusnya.
Jika Anda ingin kunci / elemen buruk ini berkeliaran 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.
Di bawah ini akan ada kasus yang tidak jelas di mana Java bocor, di samping kasus standar pendengar yang dilupakan, referensi statis, kunci palsu / yang dapat dimodifikasi dalam hashmaps, atau hanya utas yang macet tanpa ada kesempatan untuk mengakhiri siklus hidup mereka.
File.deleteOnExit()
- selalu membocorkan string, char[]
, jadi nanti tidak berlaku ; @Daniel, tidak perlu suara, meskipun.Saya akan berkonsentrasi pada utas untuk menunjukkan bahaya utas yang sebagian besar tidak dikelola, bahkan tidak ingin menyentuh ayunan.
Runtime.addShutdownHook
dan tidak menghapus ... dan kemudian bahkan dengan removeShutdownHook karena bug di kelas ThreadGroup tentang utas yang belum dimulai itu mungkin tidak dikumpulkan, secara efektif bocor ThreadGroup. JGroup mengalami kebocoran di GossipRouter.
Menciptakan, tetapi tidak memulai, a Thread
masuk ke kategori yang sama seperti di atas.
Membuat utas mewarisi ContextClassLoader
dan AccessControlContext
, ditambah dengan ThreadGroup
dan apa saja InheritedThreadLocal
, semua referensi tersebut adalah potensi kebocoran, bersama dengan seluruh kelas yang dimuat oleh classloader dan semua referensi statis, dan ja-ja. Efeknya terutama terlihat dengan seluruh kerangka kerja jucExecutor yang menampilkan ThreadFactory
antarmuka super sederhana , namun sebagian besar pengembang tidak memiliki petunjuk tentang bahaya yang mengintai. Juga banyak perpustakaan yang memulai utas atas permintaan (terlalu banyak perpustakaan populer industri).
ThreadLocal
cache; mereka jahat dalam banyak kasus. Saya yakin semua orang telah melihat sedikit cache sederhana berdasarkan ThreadLocal, nah berita buruknya: jika utas terus berjalan lebih dari yang diharapkan dalam konteks ClassLoader, itu adalah kebocoran kecil murni yang bagus. Jangan gunakan cache ThreadLocal kecuali benar-benar diperlukan.
Memanggil ThreadGroup.destroy()
ketika ThreadGroup tidak memiliki utas itu sendiri, tetapi ThreadGroup masih tetap anak. Kebocoran buruk yang akan mencegah ThreadGroup untuk menghapus dari orang tuanya, tetapi semua anak menjadi tidak dapat disebutkan.
Menggunakan WeakHashMap dan nilai (dalam) langsung merujuk kunci. Ini yang sulit ditemukan tanpa tumpukan sampah. Itu berlaku untuk semua perluasan 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 utas baru di sistem ThreadGroup yang bocor classloader konteks utas saat ini. Utas dibuat berdasarkan permintaan pertama saat tidak ada utas aktif, jadi Anda bisa beruntung atau bocor. Kebocoran sudah diperbaiki di Java 7 dan kode yang membuat utas dengan benar menghapus konteks classloader. Ada beberapa kasus lagi (seperti ImageFetcher, juga diperbaiki ) untuk membuat utas serupa.
Menggunakan InflaterInputStream
passing new java.util.zip.Inflater()
dalam konstruktor ( PNGImageDecoder
misalnya) dan tidak memanggil end()
inflater. Nah, jika Anda meneruskan konstruktor dengan adil new
, tidak ada kesempatan ... Dan ya, memanggil close()
aliran tidak menutup inflater jika dilewatkan secara manual sebagai parameter konstruktor. Ini bukan kebocoran sebenarnya karena akan dirilis oleh finalizer ... ketika itu dianggap perlu. Sampai saat itu memakan memori asli sehingga buruk dapat menyebabkan Linux oom_killer untuk membunuh proses dengan impunitas. Masalah utama adalah bahwa finalisasi di Jawa sangat tidak dapat diandalkan dan G1 memperburuknya hingga 7.0.2. Moral dari cerita: lepaskan sumber daya asli sesegera mungkin; finalizer terlalu miskin.
Kasus yang sama dengan java.util.zip.Deflater
. Yang satu ini jauh lebih buruk karena Deflater kehabisan memori di Jawa, yaitu selalu menggunakan 15 bit (maks) dan 8 level memori (9 maks) yang mengalokasikan beberapa ratus KB memori asli. Untungnya, Deflater
tidak banyak digunakan dan setahu saya JDK tidak mengandung penyalahgunaan. Selalu panggil end()
jika Anda secara manual membuat a 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!
Creating but not starting a Thread...
Astaga, saya digigit oleh yang satu ini beberapa abad yang lalu! (Jawa 1.3)
unstarted
jumlah tetapi itu mencegah kelompok utas dari menghancurkan (kejahatan yang lebih rendah tetapi masih bocor)
ThreadGroup.destroy()
ketika ThreadGroup tidak memiliki utas sendiri ..." adalah bug yang sangat halus; Saya sudah mengejar ini selama berjam-jam, tersesat karena menghitung utas dalam kendali saya GUI tidak menunjukkan apa-apa, tetapi kelompok utas dan, mungkin, setidaknya satu kelompok anak tidak akan pergi.
Sebagian besar contoh di sini "terlalu rumit". Mereka adalah kasus tepi. Dengan contoh-contoh ini, programmer membuat kesalahan (seperti tidak mendefinisikan ulang sama dengan / kode hash), atau telah digigit oleh kasus sudut JVM / JAVA (memuat kelas dengan statis ...). Saya pikir itu bukan tipe contoh yang diinginkan pewawancara atau bahkan kasus yang paling umum.
Tetapi ada kasus yang benar-benar 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 yang berumur panjang cenderung memiliki status berbagi. Itu bisa apa saja, statika, lajang ... Seringkali aplikasi non-sepele cenderung membuat grafik objek yang kompleks. Hanya lupa untuk menetapkan referensi ke nol atau lebih sering lupa untuk menghapus satu objek dari koleksi sudah cukup untuk membuat memori bocor.
Tentu saja semua jenis pendengar (seperti pendengar UI), cache, atau keadaan berbagi 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 adalah masalah desain. Kami merancang bahwa kami menambahkan pendengar ke objek yang berumur panjang, tetapi kami tidak menghapus pendengar saat tidak lagi diperlukan. Kami men-cache objek, tetapi kami tidak memiliki strategi untuk menghapusnya dari cache.
Kami mungkin memiliki grafik kompleks yang menyimpan keadaan sebelumnya yang diperlukan oleh perhitungan. Tetapi negara sebelumnya sendiri terkait dengan negara sebelum dan seterusnya.
Seperti kita harus menutup koneksi atau file SQL. Kita perlu mengatur referensi yang tepat ke null dan menghapus elemen dari koleksi. Kami akan memiliki strategi caching yang tepat (ukuran memori maksimum, jumlah elemen, atau timer). Semua objek yang memungkinkan pendengar diberi tahu harus menyediakan metode addListener dan removeListener. Dan ketika pemberi notifikasi 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 kasing sudut. Kebocoran memori merupakan indikator bahwa ada sesuatu yang hilang atau bahkan masalah desain.
WeakReference
) ada dari satu ke yang lain. Jika referensi objek memiliki bit cadangan, mungkin bermanfaat untuk memiliki indikator "peduli tentang target" ...
PhantomReference
) jika suatu objek ditemukan tidak memiliki orang yang peduli. WeakReference
datang agak dekat, tetapi harus dikonversi ke referensi yang kuat sebelum dapat digunakan; jika siklus GC terjadi ketika referensi kuat ada, target akan dianggap berguna.
Jawabannya sepenuhnya tergantung pada apa yang pewawancara pikir mereka bertanya.
Apakah mungkin dalam praktiknya membuat Java bocor? Tentu saja, dan ada banyak contoh dalam jawaban lainnya.
Tetapi ada beberapa pertanyaan meta yang mungkin ditanyakan?
Saya membaca pertanyaan meta Anda sebagai "Apa jawaban yang bisa saya gunakan dalam situasi wawancara ini". Dan karenanya, saya akan fokus pada keterampilan wawancara daripada Jawa. Saya percaya Anda lebih cenderung mengulangi situasi tidak mengetahui jawaban atas sebuah pertanyaan dalam sebuah wawancara daripada Anda harus berada di tempat yang perlu tahu bagaimana membuat Jawa bocor. Jadi, semoga ini 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 maksud mereka. Ini tidak hanya memungkinkan Anda menjawab pertanyaan mereka seperti yang mereka inginkan, tetapi juga menunjukkan bahwa Anda memiliki beberapa keterampilan komunikasi yang vital. Dan ketika sampai pada pilihan antara banyak pengembang yang sama berbakatnya, saya akan mempekerjakan orang yang mendengarkan, berpikir, dan memahami sebelum mereka merespons setiap waktu.
Berikut ini adalah contoh yang cukup berguna, jika Anda tidak mengerti JDBC . Atau setidaknya bagaimana JDBC mengharapkan pengembang untuk menutup Connection
, Statement
dan ResultSet
contoh sebelum membuang mereka atau kehilangan referensi kepada mereka, bukannya bergantung pada implementasi 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 memanggil finalize
metode, tetapi ada driver JDBC yang tidak mengimplementasikan finalize
, setidaknya tidak dengan cara yang sama yang Connection.close
diterapkan. Perilaku yang dihasilkan adalah bahwa sementara memori akan direklamasi karena objek yang tidak terjangkau dikumpulkan, sumber daya (termasuk memori) yang terkait dengan Connection
objek mungkin tidak dapat direklamasi.
Dalam peristiwa semacam itu di mana Connection
's finalize
metode tidak bersih semuanya, orang mungkin benar-benar menemukan bahwa koneksi fisik ke server database akan berlangsung beberapa siklus pengumpulan sampah, sampai database server akhirnya angka bahwa sambungan tidak hidup (jika tidak), dan harus ditutup.
Bahkan jika driver JDBC menerapkan finalize
, ada kemungkinan pengecualian dilemparkan selama finalisasi. Perilaku yang dihasilkan adalah bahwa setiap memori yang terkait dengan objek yang sekarang "tidak aktif" tidak akan direklamasi, karena finalize
dijamin akan dipanggil hanya sekali.
Skenario bertemu pengecualian di atas selama finalisasi objek terkait dengan skenario lain yang mungkin dapat menyebabkan kebocoran memori - kebangkitan objek. Kebangkitan objek sering dilakukan dengan sengaja dengan membuat referensi yang kuat untuk objek dari penyelesaian, dari objek lain. Ketika objek kebangkitan disalahgunakan akan menyebabkan kebocoran memori dalam kombinasi dengan sumber kebocoran memori lainnya.
Ada banyak lagi contoh yang dapat Anda bayangkan - seperti
List
contoh di mana Anda hanya menambah daftar dan tidak menghapusnya (meskipun Anda harus menyingkirkan elemen yang tidak lagi Anda perlukan), atauSocket
s atau File
s, tapi tidak menutup mereka ketika mereka tidak lagi diperlukan (mirip dengan contoh di atas yang melibatkan Connection
kelas).Connection.close
ke dalam blok semua panggilan SQL saya. Untuk kesenangan ekstra, saya menelepon beberapa prosedur tersimpan Oracle yang sudah berjalan lama yang membutuhkan kunci di sisi Java untuk mencegah terlalu banyak panggilan ke database.
Mungkin salah satu contoh paling sederhana dari kebocoran memori potensial, dan cara menghindarinya, adalah penerapan 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 menerapkannya sendiri, apakah Anda berpikir untuk menghapus elemen array yang tidak lagi digunakan ( elementData[--size] = null
)? Referensi itu mungkin membuat benda besar tetap hidup ...
Setiap kali Anda menyimpan referensi di sekitar objek yang tidak lagi Anda perlukan, Anda mengalami kebocoran memori. Lihat Menangani kebocoran memori dalam program Java untuk contoh bagaimana kebocoran memori memanifestasikan dirinya di Jawa dan apa yang dapat Anda lakukan.
...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language.
Saya tidak melihat bagaimana Anda menarik kesimpulan itu. Ada beberapa cara untuk membuat kebocoran memori di Jawa dengan definisi apa pun. Ini jelas masih pertanyaan yang valid.
Anda dapat membuat kebocoran memori dengan kelas sun.misc.Unsafe . Bahkan kelas layanan ini digunakan di kelas standar yang berbeda (misalnya di kelas java.nio ). Anda tidak dapat membuat turunan dari kelas ini secara langsung , tetapi Anda dapat menggunakan refleksi untuk melakukannya .
Kode tidak dikompilasi dalam Eclipse IDE - kompilasi dengan menggunakan perintah javac
(saat kompilasi Anda akan mendapatkan peringatan)
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();
}
}
}
Saya dapat menyalin jawaban saya dari sini: Cara termudah untuk menyebabkan kebocoran memori di Jawa?
"Kebocoran memori, dalam ilmu komputer (atau kebocoran, dalam konteks ini), terjadi ketika sebuah program komputer mengkonsumsi memori tetapi tidak dapat melepaskannya kembali ke sistem operasi." (Wikipedia)
Jawaban yang mudah adalah: Anda tidak bisa. Java melakukan manajemen memori otomatis dan akan membebaskan sumber daya yang tidak diperlukan untuk Anda. Anda tidak dapat menghentikan hal ini terjadi. Ini akan SELALU dapat melepaskan sumber daya. Dalam program dengan manajemen memori manual, ini berbeda. Anda tidak bisa mendapatkan memori di C menggunakan malloc (). Untuk membebaskan memori, Anda memerlukan pointer yang dikembalikan malloc dan memanggil bebas () di atasnya. Tetapi jika Anda tidak memiliki pointer lagi (ditimpa, atau seumur hidup terlampaui), maka Anda sayangnya tidak mampu membebaskan memori ini dan dengan demikian Anda memiliki kebocoran memori.
Semua jawaban lain sejauh ini dalam definisi saya bukan kebocoran memori. Mereka semua bertujuan untuk mengisi memori dengan hal-hal yang tidak berguna dengan sangat cepat. Tetapi kapan saja Anda masih bisa melakukan referensi objek yang Anda buat dan dengan demikian membebaskan memori -> TANPA KEBOCORAN. jawaban acconrad datang cukup dekat karena saya harus mengakui karena solusinya secara efektif hanya "menabrak" pengumpul sampah dengan memaksanya dalam lingkaran tanpa akhir).
Jawaban panjangnya adalah: Anda bisa mendapatkan kebocoran memori dengan menulis perpustakaan untuk Java menggunakan JNI, yang dapat memiliki manajemen memori manual dan dengan demikian memiliki kebocoran memori. Jika Anda memanggil perpustakaan ini, proses java Anda akan membocorkan memori. Atau, Anda dapat memiliki bug di JVM, sehingga JVM kehilangan memori. Mungkin ada bug di JVM, bahkan mungkin ada beberapa yang diketahui karena pengumpulan sampah tidak sepele, tapi masih bug. Dengan desain ini tidak mungkin. Anda mungkin meminta beberapa kode java yang dipengaruhi oleh bug semacam itu. Maaf saya tidak tahu satu dan mungkin juga bukan bug lagi di versi Java berikutnya.
Ini yang sederhana / seram via 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);
}
}
Karena substring mengacu pada representasi internal dari string asli, yang jauh lebih lama, yang asli tetap berada dalam memori. Jadi, selama Anda memiliki StringLeaker dalam permainan, Anda memiliki seluruh string asli dalam memori, juga, meskipun Anda mungkin berpikir Anda hanya berpegang pada string karakter tunggal.
Cara untuk menghindari penyimpanan referensi yang tidak diinginkan ke string asli adalah dengan melakukan sesuatu seperti ini:
...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...
Untuk tambahan kejahatan, Anda mungkin juga .intern()
mengambil substring:
...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...
Melakukannya akan menjaga string panjang asli dan substring yang diperoleh dalam memori bahkan setelah instance StringLeaker telah dibuang.
muchSmallerString
dibebaskan (karena StringLeaker
objek dihancurkan), string yang panjang akan dibebaskan juga. Apa yang saya sebut kebocoran memori adalah memori yang tidak pernah bisa dibebaskan dalam instance JVM ini. Namun, Anda telah menunjukkan diri sendiri bagaimana untuk membebaskan memori: this.muchSmallerString=new String(this.muchSmallerString)
. Dengan kebocoran memori nyata, tidak ada yang bisa Anda lakukan.
intern
case mungkin lebih merupakan "kejutan memori" daripada "kebocoran memori". .intern()
Namun, substring tentu saja menciptakan situasi di mana referensi ke string yang lebih panjang dipertahankan dan tidak dapat dibebaskan.
Contoh umum dari hal ini dalam kode GUI adalah ketika membuat widget / komponen dan menambahkan pendengar ke beberapa objek lingkup / aplikasi statis dan kemudian tidak menghapus pendengar ketika widget dihancurkan. Anda tidak hanya mendapatkan kebocoran memori, tetapi juga kinerja yang memukau ketika apa pun yang Anda dengarkan untuk acara kebakaran, semua pendengar lama Anda juga dipanggil.
Ambil aplikasi web yang berjalan dalam wadah servlet (Tomcat, Jetty, Glassfish, apa pun ...). Gunakan kembali aplikasi 10 atau 20 kali berturut-turut (mungkin cukup dengan hanya menyentuh PERANG dalam direktori autodeploy server.
Kecuali ada orang yang benar-benar menguji ini, kemungkinan besar bahwa Anda akan mendapatkan OutOfMemoryError setelah beberapa penugasan kembali, karena aplikasi tidak mau membersihkan setelah itu sendiri. Anda bahkan dapat menemukan bug di server Anda dengan tes ini.
Masalahnya adalah, masa pakai wadah lebih lama dari masa aplikasi Anda. Anda harus memastikan bahwa semua referensi yang mungkin dimiliki wadah atau objek dari kelas aplikasi Anda dapat dikumpulkan dari sampah.
Jika hanya ada satu referensi yang selamat dari pengabaian aplikasi web Anda, classloader yang sesuai dan akibatnya semua kelas aplikasi web Anda tidak dapat dikumpulkan dengan sampah.
Utas dimulai oleh aplikasi Anda, variabel ThreadLocal, pendaftar pendaftar adalah beberapa tersangka yang biasa menyebabkan kebocoran classloader.
Mungkin dengan menggunakan kode asli eksternal melalui JNI?
Dengan Java murni, hampir tidak mungkin.
Tapi itu tentang tipe "standar" kebocoran memori, ketika Anda tidak dapat mengakses memori lagi, tetapi masih dimiliki oleh aplikasi. Sebagai gantinya, Anda dapat menyimpan referensi ke objek yang tidak digunakan, atau membuka aliran tanpa menutupnya setelahnya.
Saya memiliki "kebocoran memori" yang bagus sehubungan dengan PermGen dan XML parsing sekali. Parser XML yang kami gunakan (saya tidak ingat yang mana) melakukan String.intern () pada nama tag, untuk membuat perbandingan lebih cepat. Salah satu pelanggan kami memiliki ide bagus untuk menyimpan nilai data bukan dalam atribut XML atau teks, tetapi sebagai tagnames, jadi kami memiliki dokumen seperti:
<data>
<1>bla</1>
<2>foo</>
...
</data>
Faktanya, mereka tidak menggunakan angka tetapi ID tekstual yang lebih panjang (sekitar 20 karakter), yang unik dan masuk pada tingkat 10-15 juta per hari. Itu membuat 200 MB sampah sehari, yang tidak pernah dibutuhkan lagi, dan tidak pernah GCed (karena ada di PermGen). Kami memiliki permgen yang disetel ke 512 MB, jadi butuh sekitar dua hari untuk pengecualian kehabisan memori (OOME) untuk tiba ...
Apa itu kebocoran memori:
Contoh umum:
Cache objek adalah titik awal yang baik untuk mengacaukan segalanya.
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;
}
Cache Anda tumbuh dan tumbuh. Dan segera seluruh basis data tersedot ke dalam memori. Desain yang lebih baik menggunakan LRUMap (Hanya menyimpan objek yang baru saja digunakan dalam cache).
Tentu, Anda dapat membuat banyak hal lebih rumit:
Yang sering terjadi:
Jika objek Info ini memiliki referensi ke objek lain, yang lagi-lagi memiliki referensi ke objek lain. Di satu sisi Anda juga bisa menganggap ini sebagai semacam kebocoran memori, (disebabkan oleh desain yang buruk).
Saya pikir itu menarik bahwa tidak ada yang menggunakan contoh kelas internal. Jika Anda memiliki kelas internal; itu secara inheren mempertahankan referensi ke kelas yang mengandung. Tentu saja itu bukan kebocoran memori secara teknis karena Java AKAN akhirnya membersihkannya; tetapi ini dapat menyebabkan kelas-kelas lebih lama dari yang diperkirakan.
public class Example1 {
public Example2 getNewExample2() {
return this.new Example2();
}
public class Example2 {
public Example2() {}
}
}
Sekarang jika Anda memanggil Example1 dan mendapatkan Example1 membuang Example1, Anda secara inheren masih memiliki tautan ke objek Example1.
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.
}
}
Saya juga mendengar desas-desus bahwa jika Anda memiliki variabel yang ada lebih lama dari jumlah waktu tertentu; Java mengasumsikan bahwa ia akan selalu ada dan sebenarnya tidak akan pernah mencoba untuk membersihkannya jika tidak dapat dijangkau dalam kode lagi. Tapi itu sama sekali tidak diverifikasi.
Baru-baru ini saya mengalami situasi kebocoran memori yang disebabkan oleh log4j.
Log4j memiliki mekanisme yang disebut Nested Diagnostic Context (NDC) yang merupakan instrumen untuk membedakan keluaran log yang disisipkan dari sumber yang berbeda. Granularitas tempat NDC bekerja adalah utas, sehingga ia membedakan keluaran log dari utas berbeda secara terpisah.
Untuk menyimpan tag spesifik thread, kelas NDC log4j menggunakan Hashtable yang dikunci oleh objek Thread itu sendiri (sebagai lawan untuk mengatakan id utas), dan dengan demikian sampai tag NDC tetap dalam memori semua objek yang menggantung dari utas objek juga tinggal di memori. Dalam aplikasi web kami, kami menggunakan NDC untuk menandai logoutput dengan id permintaan untuk membedakan log dari satu permintaan secara terpisah. Wadah yang mengaitkan tag NDC dengan utas, juga menghapusnya sambil mengembalikan respons dari permintaan. Masalah terjadi ketika selama pemrosesan permintaan, sebuah child thread muncul, kira-kira seperti kode berikut:
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();
}
}
Jadi konteks NDC dikaitkan dengan inline thread yang muncul. Objek utas yang merupakan kunci untuk konteks NDC ini, adalah utas inline yang menggantung objek besar daftar itu. Karenanya bahkan setelah utas selesai melakukan apa yang dilakukannya, referensi ke daftar besar tetap hidup oleh konteks NDC yang Dapat Diperbaharui, sehingga menyebabkan kebocoran memori.
Pewawancara mungkin mencari referensi melingkar seperti kode di bawah ini (yang kebetulan hanya membocorkan memori di JVM yang sangat lama yang menggunakan penghitungan referensi, yang tidak lagi menjadi masalah). Tapi itu pertanyaan yang cukup samar, jadi ini adalah kesempatan utama untuk memamerkan pemahaman Anda tentang manajemen memori 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 */
}
Kemudian Anda dapat menjelaskan bahwa dengan penghitungan referensi, kode di atas akan membocorkan memori. Tetapi sebagian besar JVM modern tidak lagi menggunakan penghitungan referensi, sebagian besar menggunakan pengumpul sampah, yang sebenarnya akan mengumpulkan memori ini.
Selanjutnya Anda dapat menjelaskan membuat Obyek yang memiliki sumber daya asli yang mendasarinya, seperti ini:
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. */
}
}
Kemudian Anda dapat menjelaskan ini secara teknis kebocoran memori, tetapi sebenarnya kebocoran tersebut disebabkan oleh kode asli di JVM yang mengalokasikan sumber daya asli yang mendasarinya, yang tidak dibebaskan oleh kode Java Anda.
Pada akhirnya, dengan JVM modern, Anda perlu menulis beberapa kode Java yang mengalokasikan sumber daya asli di luar ruang lingkup normal kesadaran JVM.
Semua orang selalu lupa rute kode asli. Berikut ini rumus sederhana untuk kebocoran:
malloc
. Jangan panggil free
.Ingat, alokasi memori dalam kode asli berasal dari tumpukan JVM.
Buat Peta statis dan terus tambahkan referensi keras untuk itu. Itu tidak akan pernah menjadi GC.
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); }
}
Anda dapat membuat kebocoran memori bergerak dengan membuat instance baru kelas dalam metode final kelas itu. Poin bonus jika finalizer menciptakan banyak instance. Berikut adalah program sederhana yang membocorkan seluruh tumpukan dalam waktu antara beberapa detik dan beberapa menit tergantung pada ukuran tumpukan Anda:
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());
}
}
}
Saya tidak berpikir ada yang mengatakan ini: Anda dapat menghidupkan kembali objek dengan menimpa metode finalize () sehingga finalize () menyimpan referensi di suatu tempat. Pengumpul sampah hanya akan dipanggil sekali pada objek sehingga setelah itu objek tidak akan pernah hancur.
finalize()
tidak akan dipanggil tetapi objek akan dikumpulkan setelah tidak akan ada lagi referensi. Pengumpul sampah juga tidak 'dipanggil'.
finalize()
metode ini hanya dapat dipanggil sekali oleh JVM, tetapi ini tidak berarti bahwa itu tidak dapat dikumpulkan kembali sampah yang dikumpulkan jika objek dibangkitkan dan kemudian direferensikan lagi. Jika ada kode sumber daya penutupan dalam finalize()
metode maka kode ini tidak akan dijalankan lagi, ini dapat menyebabkan kebocoran memori.
Saya menemukan jenis kebocoran sumber daya yang lebih halus baru-baru ini. Kami membuka sumber daya melalui getResourceAsStream pemuat kelas dan kebetulan aliran input input tidak ditutup.
Uhm, bisa dibilang, idiot sekali.
Nah, yang membuat ini menarik adalah: dengan cara ini, Anda bisa membocorkan memori tumpukan proses yang mendasarinya, bukan dari tumpukan JVM.
Yang Anda butuhkan hanyalah file jar dengan file di dalamnya yang akan dirujuk dari kode Java. Semakin besar file jar, semakin cepat memori dialokasikan.
Anda dapat dengan mudah membuat tabung seperti itu dengan kelas berikut:
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();
}
}
Cukup tempel ke file bernama BigJarCreator.java, kompilasi dan jalankan dari baris perintah:
javac BigJarCreator.java
java -cp . BigJarCreator
Et voilà: Anda menemukan arsip jar di direktori kerja Anda saat ini dengan dua file di dalamnya.
Mari kita buat kelas kedua:
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);
}
}
Kelas ini pada dasarnya tidak melakukan apa-apa, tetapi membuat objek InputStream yang tidak direferensikan. Benda-benda itu akan menjadi sampah yang dikumpulkan segera dan dengan demikian, tidak berkontribusi pada ukuran tumpukan. Penting bagi contoh kita untuk memuat sumber daya yang ada dari file jar, dan ukuran memang penting di sini!
Jika Anda ragu, coba kompilasi dan mulai kelas di atas, tetapi pastikan untuk memilih ukuran tumpukan yang layak (2 MB):
javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak
Anda tidak akan menemukan kesalahan OOM di sini, karena tidak ada referensi yang disimpan, aplikasi akan tetap berjalan tidak peduli seberapa besar Anda memilih ITERASI dalam contoh di atas. Konsumsi memori dari proses Anda (terlihat di atas (RES / RSS) atau explorer proses) tumbuh kecuali aplikasi sampai ke perintah tunggu. Pada pengaturan di atas, itu akan mengalokasikan sekitar 150 MB dalam memori.
Jika Anda ingin aplikasi bermain aman, tutup aliran input tepat di tempat itu dibuat:
MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();
dan proses Anda tidak akan melebihi 35 MB, terlepas dari jumlah iterasi.
Cukup sederhana dan mengejutkan.
Seperti yang disarankan oleh banyak orang, Kebocoran Sumberdaya cukup mudah disebabkan - seperti contoh JDBC. Kebocoran Memori Aktual sedikit lebih sulit - terutama jika Anda tidak mengandalkan pecahan JVM untuk melakukannya untuk Anda ...
Gagasan membuat objek yang memiliki jejak sangat besar dan kemudian tidak dapat mengaksesnya juga bukan kebocoran memori nyata. Jika tidak ada yang dapat mengaksesnya maka itu akan menjadi sampah yang dikumpulkan, dan jika sesuatu dapat mengaksesnya maka itu bukan kebocoran ...
Salah satu cara yang dulu bekerja - dan saya tidak tahu apakah masih ada - adalah memiliki rantai melingkar sedalam tiga. Seperti dalam Objek A memiliki referensi ke Obyek B, Obyek B memiliki referensi ke Obyek C dan Obyek C memiliki referensi ke Obyek A. GC cukup pintar untuk mengetahui bahwa dua rantai dalam - seperti dalam A <--> B - dapat dikumpulkan dengan aman jika A dan B tidak dapat diakses oleh hal lain, tetapi tidak dapat menangani rantai tiga arah ...
Cara lain untuk membuat kebocoran memori yang berpotensi besar adalah untuk memegang referensi untuk Map.Entry<K,V>
sebuah TreeMap
.
Sulit untuk menilai mengapa ini hanya berlaku untuk TreeMap
s, tetapi dengan melihat implementasi alasannya mungkin adalah: a TreeMap.Entry
menyimpan referensi ke saudara kandungnya, oleh karena itu jika TreeMap
siap untuk dikumpulkan, tetapi beberapa kelas lain memiliki referensi ke salah satu nya Map.Entry
, maka seluruh Peta akan disimpan ke dalam memori.
Skenario kehidupan nyata:
Bayangkan memiliki permintaan db yang mengembalikan TreeMap
struktur data besar . Orang biasanya menggunakan TreeMap
s sebagai urutan penyisipan elemen dipertahankan.
public static Map<String, Integer> pseudoQueryDatabase();
Jika permintaan dipanggil berkali-kali dan, untuk setiap permintaan (jadi, untuk setiap Map
pengembalian) Anda menyimpan suatu Entry
tempat, memori akan terus tumbuh.
Pertimbangkan kelas pembungkus berikut:
class EntryHolder {
Map.Entry<String, Integer> entry;
EntryHolder(Map.Entry<String, Integer> entry) {
this.entry = entry;
}
}
Aplikasi:
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();
}
}
Setelah setiap pseudoQueryDatabase()
panggilan, map
instance harus siap untuk dikumpulkan, tetapi itu tidak akan terjadi, karena setidaknya satu Entry
disimpan di tempat lain.
Tergantung pada jvm
pengaturan Anda , aplikasi mungkin macet di tahap awal karena a OutOfMemoryError
.
Anda dapat melihat dari visualvm
grafik ini bagaimana memori terus bertambah.
Hal yang sama tidak terjadi dengan struktur data hash ( HashMap
).
Ini adalah grafik saat menggunakan a HashMap
.
Solusinya? Langsung saja simpan kunci / nilai (seperti yang mungkin sudah Anda lakukan) daripada menyimpannya Map.Entry
.
Saya telah menulis patokan yang lebih luas di sini .
Thread tidak dikumpulkan sampai mereka berakhir. Mereka berfungsi sebagai akar pengumpulan sampah. Mereka adalah salah satu dari sedikit objek yang tidak akan direklamasi hanya dengan melupakannya atau membersihkan referensi untuk itu.
Pertimbangkan: pola dasar untuk mengakhiri thread pekerja adalah mengatur beberapa variabel kondisi yang dilihat oleh thread. Thread dapat memeriksa variabel secara berkala dan menggunakannya sebagai sinyal untuk mengakhiri. Jika variabel tidak dideklarasikan volatile
, maka perubahan ke variabel mungkin tidak terlihat oleh utas, sehingga tidak akan tahu untuk mengakhiri. Atau bayangkan jika beberapa utas ingin memperbarui objek yang dibagikan, tetapi jalan buntu saat mencoba untuk menguncinya.
Jika Anda hanya memiliki sedikit utas, bug ini mungkin akan terlihat jelas karena program Anda akan berhenti bekerja dengan baik. Jika Anda memiliki kumpulan utas yang menciptakan lebih banyak utas sesuai kebutuhan, maka utas usang / macet mungkin tidak diperhatikan, dan akan terakumulasi tanpa batas waktu, menyebabkan kebocoran memori. Utas cenderung menggunakan data lain dalam aplikasi Anda, jadi juga akan mencegah apa pun yang mereka rujuk secara langsung untuk tidak pernah dikumpulkan.
Sebagai contoh mainan:
static void leakMe(final Object object) {
new Thread() {
public void run() {
Object o = object;
for (;;) {
try {
sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {}
}
}
}.start();
}
Sebut System.gc()
semua yang Anda suka, tetapi objek yang dilewati leakMe
tidak akan pernah mati.
(* diedit *)
Saya pikir contoh yang valid bisa menggunakan variabel ThreadLocal di lingkungan di mana utas dikumpulkan.
Misalnya, menggunakan variabel ThreadLocal di Servlets untuk berkomunikasi dengan komponen web lainnya, membuat utas dibuat oleh wadah dan memelihara yang menganggur di kumpulan. Variabel ThreadLocal, jika tidak dibersihkan dengan benar, akan tinggal di sana sampai, mungkin, komponen web yang sama menimpa nilainya.
Tentu saja, setelah diidentifikasi, masalah dapat diselesaikan dengan mudah.
Pewawancara mungkin sedang mencari solusi referensi melingkar:
public static void main(String[] args) {
while (true) {
Element first = new Element();
first.next = new Element();
first.next.next = first;
}
}
Ini adalah masalah klasik dengan referensi penghitungan pengumpul sampah. Anda kemudian akan dengan sopan menjelaskan bahwa JVM menggunakan algoritma yang jauh lebih canggih yang tidak memiliki batasan ini.
-Kami Tarle
first
tidak berguna dan harus dikumpulkan. Dalam referensi menghitung pengumpul sampah, objek tidak akan dibebaskan karena ada referensi aktif di atasnya (dengan sendirinya). Infinite loop ada di sini untuk menghilangkan kebocoran: ketika Anda menjalankan program, memori akan naik tanpa batas.