Menemukan kebocoran memori dengan Eclipse MAT

Mungkin, semua pengembang java yang terlibat dalam proyek komersial cepat atau lambat menghadapi masalah kebocoran memori, yang memerlukan kinerja aplikasi yang lambat dan hampir pasti mengarah ke OutOfMemoryError yang terkenal. Artikel ini akan mempertimbangkan contoh nyata dari situasi seperti itu dan bagaimana menemukan penyebabnya menggunakan Eclipce Memory Analyzer.



pengantar



Kebocoran memori biasanya disebut situasi ketika jumlah memori yang ditempati di heap bertambah selama operasi aplikasi jangka panjang dan tidak berkurang setelah Pengumpul Sampah keluar. Seperti yang Anda ketahui, memori jvm dibagi menjadi heap dan stack. Tumpukan menyimpan nilai variabel tipe sederhana dan referensi ke objek dalam konteks aliran, dan tumpukan menyimpan objek itu sendiri. Juga di heap ada ruang yang disebut Metaspace, yang menyimpan data tentang kelas yang dimuat dan data yang terikat ke kelas itu sendiri, dan bukan instansinya, khususnya, nilai variabel statis. Pengumpul Sampah (selanjutnya disebut GC), yang diluncurkan secara berkala oleh mesin java, menemukan objek di heap yang tidak lagi direferensikan dan membebaskan memori yang ditempati oleh objek tersebut. Algoritme kerja GC berbeda dan kompleks, khususnya,saat GC dimulai lagi, ia tidak "memeriksa" seluruh heap setiap kali menemukan objek yang tidak digunakan, jadi tidak ada gunanya mengandalkan fakta bahwa objek yang tidak digunakan lagi akan dihapus dari memori setelah satu GC dimulai, tetapi jika jumlah memori yang digunakan oleh aplikasi stabil tumbuh tanpa alasan yang jelas untuk waktu yang lama, maka inilah saatnya untuk memikirkan apa yang bisa menyebabkan situasi seperti itu.



Jvm mencakup VM Visual utilitas multifungsi (selanjutnya disebut VM). VM memungkinkan Anda untuk mengamati secara visual dinamika indikator kunci jvm dalam grafik, khususnya, jumlah memori kosong dan yang ditempati di heap, jumlah kelas yang dimuat, utas, dll. Selain itu, dengan menggunakan VM, Anda dapat mengambil dan memeriksa dump memori. Tentu saja, VM juga memungkinkan dumping thread dan pembuatan profil aplikasi, tetapi ringkasan fitur ini berada di luar cakupan artikel ini. Yang kita butuhkan dari VM dalam contoh ini adalah menghubungkan ke mesin virtual dan pertama-tama melihat gambaran umum penggunaan memori. Saya ingin mencatat bahwa untuk menghubungkan VM ke server jarak jauh, parameter jmxremote harus dikonfigurasi di atasnya, karena koneksi dibuat melalui jmx.Untuk penjelasan tentang parameter ini, Anda dapat merujuk ke dokumentasi resmi Oracle atau berbagai artikel di Habré.



Jadi, mari kita asumsikan bahwa kita telah berhasil terhubung ke server aplikasi menggunakan VM dan lihat grafiknya.







Pada tab Heap, Anda dapat melihat total dan memori yang digunakan jvm. Perlu dicatat bahwa tab ini juga memperhitungkan memori jenis Metaspace (nah, bagaimana lagi, karena ini juga heap). Tab Metaspace menampilkan informasi hanya tentang memori yang ditempati oleh metadata (oleh kelas itu sendiri dan objek yang terikat padanya).



Melihat grafik, kita dapat melihat bahwa total memori heap ~ 10GB, ruang yang ditempati saat ini ~ 5,8GB. Bubungan dalam grafik sesuai dengan panggilan GC, garis yang hampir lurus (tanpa bubungan) yang dimulai sekitar pukul 10:18 dapat (tetapi belum tentu!) Menunjukkan bahwa server aplikasi hampir tidak berjalan sejak saat itu, karena tidak ada alokasi dan rilis aktif Penyimpanan. Secara umum, grafik ini sesuai dengan operasi normal server aplikasi (jika, tentu saja, menilai pekerjaan hanya dari memori). Grafik masalah akan menjadi grafik di mana garis biru horizontal lurus tanpa punggung akan berada di sekitar tingkat oranye, yang mewakili jumlah maksimum memori di tumpukan.



Sekarang mari kita lihat grafik lainnya.







Di sini kita datang langsung ke analisis contoh, yang merupakan topik utama artikel ini. Grafik Kelas menunjukkan jumlah kelas yang dimuat ke dalam Metaspace, dan itu ~ 73 ribu objek. Saya ingin menarik perhatian Anda pada fakta bahwa kita tidak berbicara tentang instance kelas, tetapi tentang kelas itu sendiri, yaitu objek bertipe Class <?>. Grafik tidak menunjukkan berapa banyak contoh dari masing-masing tipe KelasA atau KelasB yang dimuat ke dalam memori. Mungkin jumlah kelas identik tipe ClassA untuk beberapa alasan berlipat ganda? Saya harus mengatakan bahwa dalam contoh yang akan dijelaskan di bawah ini, 73.000 kelas unik adalah situasi yang benar-benar normal.



Faktanya adalah bahwa dalam salah satu proyek di mana penulis artikel ini mengambil bagian, mekanisme dikembangkan untuk deskripsi universal entitas domain (seperti di 1C) yang disebut sistem kamus, dan analis yang menyesuaikan sistem untuk pelanggan tertentu atau untuk area bisnis tertentu, memiliki kesempatan, melalui editor khusus, untuk mensimulasikan model bisnis dengan membuat entitas baru dan mengubah entitas yang ada, beroperasi tidak pada tingkat tabel, tetapi dengan konsep seperti "Dokumen", "Akun", "Karyawan", dll. Kernel sistem membuat tabel dalam DBMS relasional untuk data entitas, dan beberapa tabel dapat dibuat untuk setiap entitas, karena sistem universal memungkinkan penyimpanan nilai atribut secara historis dan banyak lagi yang membutuhkan pembuatan tabel layanan tambahan dalam database.



Saya percaya bahwa mereka yang harus bekerja dengan kerangka kerja ORM telah menebak tentang apa penulis itu, teralihkan dari topik utama artikel dengan berbicara tentang tabel. Proyek ini menggunakan Hibernate dan untuk setiap tabel harus ada kelas kacang Entitas. Pada saat yang sama, karena tabel baru dibuat secara dinamis selama kerja sistem oleh analis, kelas kacang Hibernate dibuat, dan tidak ditulis secara manual oleh pengembang. Dan dengan setiap generasi berikutnya, sekitar 50-60 ribu kelas baru dibuat. Ada lebih sedikit tabel dalam sistem (sekitar 5-6 ribu), tetapi untuk setiap tabel, tidak hanya kelas kacang Entitas yang dihasilkan, tetapi lebih banyak kelas tambahan, yang pada akhirnya mengarah ke gambar umum.



Mekanisme kerjanya adalah sebagai berikut. Pada awal sistem, kelas kacang entitas dan kelas tambahan (selanjutnya hanya kelas kacang) dibuat berdasarkan metadata dalam database. Ketika sistem berjalan, pabrik sesi Hibernate membuat sesi, sesi membuat contoh objek kelas kacang. Saat mengubah struktur (menambah, mengubah tabel), kelas kacang dibuat ulang dan pabrik sesi baru dibuat. Setelah regenerasi, pabrik baru membuat sesi baru yang menggunakan kelas kacang baru, pabrik lama dan sesi ditutup, dan kelas kacang lama dibongkar oleh GC, karena tidak lagi direferensikan dari objek infrastruktur Hibernate.











Di beberapa titik, masalah muncul bahwa jumlah kelas bin mulai meningkat setelah setiap regenerasi berikutnya. Jelas, ini disebabkan oleh fakta bahwa kumpulan kelas lama, yang seharusnya tidak lagi digunakan, karena alasan tertentu tidak diturunkan dari memori. Untuk memahami alasan perilaku sistem ini, Eclipse Memory Analizer (MAT) datang membantu kami.



Menemukan kebocoran memori



MAT dapat bekerja dengan dump memori, menemukan potensi masalah di dalamnya, tetapi pertama-tama Anda perlu mendapatkan dump memori ini, tetapi dalam lingkungan nyata ada beberapa perbedaan dalam mendapatkan dump.



Menghapus dump memori



Seperti disebutkan di atas, dump memori dapat dihapus langsung dari VM dengan menekan tombol







But, karena ukuran dump yang besar, VM mungkin tidak dapat mengatasi tugas ini, membekukan beberapa saat setelah menekan tombol Heap Dump. Selain itu, sama sekali bukan fakta bahwa dimungkinkan untuk terhubung melalui jmx ke server aplikasi produk yang diperlukan untuk VM. Dalam hal ini, utilitas jvm lain yang disebut jMap datang untuk menyelamatkan kami. Ini berjalan pada baris perintah, langsung pada server di mana JVM berjalan, dan memungkinkan Anda untuk mengatur parameter tambahan untuk



pembuangan : jmap dump: hidup, format = b, file = / tmp / heapdump.bin 14.616



The dump: hidup parameter yang sangat penting, karena memungkinkan Anda mengurangi ukurannya secara signifikan, mengecualikan objek yang tidak lagi dirujuk.



Situasi umum lainnya adalah ketika pembuangan manual tidak dimungkinkan karena fakta bahwa jvm itu sendiri crash dengan OutOfMemoryError. Dalam situasi ini, opsi -XX: + HeapDumpOnOutOfMemoryError hadir untuk menyelamatkan dan, sebagai tambahan, -XX: HeapDumpPath , yang memungkinkan Anda menentukan jalur ke dump yang ditangkap.



Selanjutnya, buka dump yang ditangkap menggunakan Eclipse Memory Analizer. File bisa berukuran besar (beberapa gigabyte), jadi Anda perlu menyediakan memori yang cukup di file



MemoryAnalyzer.ini : -Xmx4096m



Melokalkan masalah menggunakan MAT



Jadi, mari pertimbangkan situasi di mana jumlah kelas yang dimuat meningkat kelipatannya dibandingkan dengan tingkat awal dan tidak berkurang bahkan setelah panggilan paksa ke pengumpulan sampah (ini dapat dilakukan dengan menekan tombol yang sesuai di VM).



Di atas, proses regenerasi kelas kacang dan penggunaannya dijelaskan secara konseptual. Pada tingkat yang lebih teknis, ini terlihat seperti ini:

 

  • Semua sesi Hibernate ditutup (kelas SessionImpl)
  • Pabrik sesi lama (SessionFactoryImpl) ditutup dan referensi ke sana dari LocalSessionFactoryBean disetel ulang
  • ClassLoader dibuat ulang
  • Referensi ke kelas kacang lama di kelas generator dibatalkan
  • Kelas kacang dibuat ulang


Dengan tidak adanya referensi ke kelas kacang lama, jumlah kelas tidak boleh bertambah setelah pengumpulan sampah.



Jalankan MAT dan buka file dump memori yang diperoleh sebelumnya. Setelah membuka dump, MAT menampilkan rantai objek terbesar dalam memori.







Setelah mengklik Leak Suspects, kita melihat detailnya:



 2 segmen lingkaran 265 M masing-masing adalah 2 instance SessionFactoryImpl. Tidak jelas mengapa ada 2 instance dari mereka dan, kemungkinan besar, masing-masing instance menyimpan referensi ke set lengkap kelas kacang Entitas. MAT memberi tahu kita tentang potensi masalah sebagai berikut.





 

Saya segera mencatat bahwa Masalah Tersangka 3 sebenarnya bukan masalah. Proyek ini telah menerapkan parser dari bahasanya sendiri, yang merupakan add-on multiplatform melalui SQL dan memungkinkan Anda untuk beroperasi tidak dengan tabel, tetapi dengan entitas sistem, dan 121M menggunakan cache kuerinya.



Mari kembali ke dua contoh SessionFactoryImpl. Klik Duplicate Classes dan lihat bahwa sebenarnya ada 2 instance dari setiap class kacang Entitas. Artinya, tautan ke kelas lama kacang Entitas tetap ada dan, kemungkinan besar, ini adalah tautan dari SesssionFactoryImpl. Berdasarkan kode sumber kelas ini, referensi ke kelas kacang harus disimpan di bidang classMetaData.



Klik Problem Suspect 1, lalu pada class SessionFactoryImpl dan pilih List Objects-> With outgouing reference dari menu konteks. Dengan cara ini kita bisa melihat semua objek yang direferensikan oleh SessionFactoryImpl.







Perluas objek classMetaData dan pastikan objek tersebut benar-benar menyimpan array kelas kacang Entitas.







Sekarang kita perlu memahami apa yang mencegah pengumpul sampah membuang satu contoh SessionFactoryImpl. Jika kita kembali ke Leak Suspects-> Leaks-> Problem Suspect 1, kita akan melihat setumpuk tautan yang mengarah ke tautan ke SessionFactoryImpl.



 



Kita melihat bahwa variabel entityManager dari kacang SessionInfoImpl yang berisi konteks sesi HTTP berisi larik dbTransactionListeners, yang menggunakan objek sesi Hibernate SessionImpl sebagai kunci, dan sesi merujuk ke SessionFactoryImpl.



Faktanya adalah bahwa objek sesi di-cache di dbTransactionListeners untuk tujuan tertentu, dan sebelum kelas kacang dibuat ulang, referensi ke objek tersebut dapat tetap berada dalam larik ini. Sesi, pada gilirannya, mereferensikan pabrik sesi, yang menyimpan berbagai referensi ke semua kelas kacang. Selain itu, sesi menyimpan referensi ke instance kelas entitas, dan mereka mereferensikan kelas kacang itu sendiri.



Dengan demikian, titik masuk ke masalah tersebut ditemukan. Ternyata itu adalah referensi ke sesi lama dari dbTransactionListeners. Setelah kesalahan diperbaiki dan larik dbTransactionListeners mulai dikosongkan, masalah diperbaiki.



Fitur Eclipse Memory Analizer



 

Jadi, Eclipse Memory Analyzer memungkinkan Anda untuk:



  • Cari tahu rantai objek mana yang menempati jumlah memori maksimum dan tentukan titik masuk ke dalam rantai ini (Tersangka Kebocoran)
  • Lihat pohon dari semua referensi objek yang masuk (Jalur Terpendek Ke titik akumulasi)
  • Lihat pohon dari semua referensi outgouing dari suatu objek (Object-> List Objects-> With outgouing reference)
  • Lihat kelas duplikat yang dimuat oleh ClassLoader yang berbeda (Kelas Duplikat)



All Articles