Bagaimana mencegah overrun memori saat menggunakan koleksi Java

Halo semuanya!

Eksperimen kami dengan langkah-langkah dalam kursus Pengembang Java berlanjut dan, anehnya, bahkan cukup berhasil (semacam): ternyata, perencanaan leverage beberapa bulan dengan transisi berikutnya ke langkah baru pada waktu yang nyaman jauh lebih nyaman daripada jika Alokasikan hampir enam bulan untuk kursus yang sulit. Jadi ada kecurigaan bahwa justru kursus kompleks yang akan segera kita mulai transfer perlahan ke sistem seperti itu.

Tapi ini saya tentang kita, tentang otusovsky, saya minta maaf. Seperti biasa, kami terus mempelajari topik-topik menarik itu, meskipun tidak dibahas dalam program kami, tetapi yang dibahas bersama kami, jadi kami menyiapkan terjemahan artikel yang paling menarik menurut pendapat kami tentang salah satu pertanyaan yang diajukan guru kami.

Ayo pergi!



Koleksi di JDK adalah implementasi standar perpustakaan daftar dan peta. Jika Anda melihat snapshot dari aplikasi Java besar yang khas, Anda akan melihat ribuan atau bahkan jutaan instance java.util.ArrayList , java.util.HashMap , dll. Koleksi sangat diperlukan untuk menyimpan dan memanipulasi data. Tetapi apakah Anda pernah berpikir tentang apakah semua koleksi dalam aplikasi Anda memanfaatkan memori secara optimal? Dengan kata lain, jika aplikasi Anda mengalami crash dengan OutOfMemoryError memalukan atau menyebabkan jeda lama di pengumpul sampah, apakah Anda pernah memeriksa pengumpulan bekas untuk kebocoran.

Pertama, harus dicatat bahwa koleksi internal JDK bukanlah semacam sihir. Mereka ditulis dalam bahasa Jawa. Kode sumber mereka datang dengan JDK, sehingga Anda dapat membukanya di IDE Anda. Kode mereka juga dapat dengan mudah ditemukan di Internet. Dan, ternyata, sebagian besar koleksi tidak begitu elegan dalam hal mengoptimalkan jumlah memori yang dikonsumsi.

Pertimbangkan, misalnya, salah satu koleksi paling sederhana dan paling populer - kelas java.util.ArrayList . Secara internal, setiap ArrayList beroperasi dengan array Object[] elementData . Di sinilah daftar item disimpan. Mari kita lihat bagaimana array ini diproses.

Ketika Anda membuat ArrayList dengan konstruktor default, yaitu, panggil new ArrayList() , elementData menunjuk ke array generik ukuran nol ( elementData juga dapat diatur ke null , tetapi array memberikan beberapa manfaat implementasi kecil). Saat Anda menambahkan elemen pertama ke daftar, array unik yang unik dari elementData , dan objek yang disediakan dimasukkan ke dalamnya. Untuk menghindari perubahan ukuran array setiap kali, saat menambahkan elemen baru, elemen itu dibuat dengan panjang sama dengan 10 (“kapasitas default”). Jadi ternyata: jika Anda tidak lagi menambahkan elemen ke ArrayList ini, 9 dari 10 slot dalam array elementData akan tetap kosong. Dan bahkan jika Anda menghapus daftar, ukuran array internal tidak akan berkurang. Berikut ini adalah diagram dari siklus hidup ini:



Berapa banyak memori yang terbuang di sini? Secara absolut, itu dihitung sebagai (ukuran penunjuk objek). Jika Anda menggunakan JVM HotSpot (yang datang dengan Oracle JDK), ukuran pointer akan tergantung pada ukuran heap maksimum (untuk lebih jelasnya lihat https://blog.codecentric.de/en/2014/02/35gb-heap-less- 32gb-java-jvm-memory-oddities / ). Biasanya, jika Anda menentukan -Xmx kurang dari 32 gigabytes, ukuran pointer akan menjadi 4 byte; untuk tumpukan besar - 8 byte. Jadi, sebuah ArrayList , diinisialisasi oleh konstruktor default, dengan tambahan hanya satu elemen, membuang 36 atau 72 byte.

Bahkan, ArrayList kosong juga membuang-buang memori karena tidak membawa beban kerja apa pun, tetapi ukuran ArrayList itu sendiri tidak nol dan lebih besar dari yang Anda kira. Ini karena, di satu sisi, setiap objek yang dikelola oleh HotSpot JVM memiliki header 12 atau 16 byte, yang digunakan oleh JVM untuk keperluan internal. Selanjutnya, sebagian besar objek dalam koleksi berisi bidang size , penunjuk ke array internal atau objek "media beban kerja" lainnya, bidang modCount untuk melacak perubahan konten, dll. Dengan demikian, bahkan objek terkecil yang mungkin mewakili koleksi kosong mungkin memerlukan setidaknya 32 byte memori. Beberapa, seperti ConcurrentHashMap , membutuhkan lebih banyak.

Pertimbangkan koleksi umum lainnya - kelas java.util.HashMap . Siklus hidupnya mirip dengan siklus hidup ArrayList :



Seperti yang Anda lihat, HashMap yang hanya berisi satu pasangan nilai kunci menghabiskan 15 sel internal array, yang sesuai dengan 60 atau 120 byte. Angka-angka ini kecil, tetapi tingkat kehilangan memori penting untuk semua koleksi dalam aplikasi Anda. Dan ternyata beberapa aplikasi dapat menghabiskan cukup banyak memori dengan cara ini. Sebagai contoh, beberapa komponen Hadoop open-source populer yang penulis analisis kehilangan sekitar 20 persen dari tumpukan mereka dalam beberapa kasus! Untuk produk yang dikembangkan oleh insinyur yang kurang berpengalaman yang tidak menjalani tinjauan kinerja reguler, kehilangan memori bisa lebih tinggi. Ada cukup banyak kasus di mana, misalnya, 90% dari simpul dalam pohon besar hanya mengandung satu atau dua keturunan (atau tidak sama sekali), dan situasi lain di mana tumpukan tersumbat dengan koleksi elemen, 0, 1, atau 2 elemen.

Jika Anda menemukan koleksi yang tidak terpakai atau kurang digunakan dalam aplikasi Anda, bagaimana cara memperbaikinya? Di bawah ini adalah beberapa resep umum. Di sini, diasumsikan bahwa koleksi kami yang bermasalah adalah ArrayList dirujuk oleh bidang data Foo.list .

Jika sebagian besar daftar tidak pernah digunakan, coba inisialisasi dengan malas. Jadi kode yang sebelumnya tampak seperti ...

 void addToList(Object x) { list.add(x); } 

... harus diulang menjadi sesuatu seperti

 void addToList(Object x) { getOrCreateList().add(x); } private list getOrCreateList() { //   ,         if (list == null) list = new ArrayList(); return list; } 

Ingatlah bahwa kadang-kadang Anda perlu mengambil tindakan tambahan untuk mengatasi potensi persaingan. Misalnya, jika Anda mendukung ConcurrentHashMap , yang dapat diperbarui oleh beberapa utas secara bersamaan, kode yang menginisialisasi itu seharusnya tidak memungkinkan dua utas untuk membuat dua salinan peta ini secara acak:

 private Map getOrCreateMap() { if (map == null) { //,       synchronized (this) { if (map == null) map = new ConcurrentHashMap(); } } return map; } 

Jika sebagian besar contoh daftar atau peta Anda hanya berisi beberapa item, coba inisialisasi dengan kapasitas awal yang lebih cocok, misalnya.

 list = new ArrayList(4); //       4 

Jika koleksi Anda kosong atau hanya mengandung satu elemen (atau pasangan nilai kunci) dalam kebanyakan kasus, Anda dapat mempertimbangkan satu bentuk optimalisasi yang ekstrem. Ini hanya berfungsi jika koleksi dikelola sepenuhnya dalam kelas saat ini, yaitu, kode lain tidak dapat mengaksesnya secara langsung. Idenya adalah bahwa Anda mengubah jenis bidang data Anda, misalnya, dari daftar ke objek yang lebih umum, sehingga sekarang dapat menunjuk ke daftar nyata atau langsung ke item daftar tunggal. Berikut ini sketsa singkatnya:

 // ***   *** private List<Foo> list = new ArrayList<>(); void addToList(Foo foo) { list.add(foo); } // ***   *** //   ,    null.      , //      .       //   ArrayList. private Object listOrSingleEl; void addToList(Foo foo) { if (listOrSingleEl == null) { //   listOrSingleEl = foo; } else if (listOrSingleEl instanceof Foo) { //  Foo firstEl = (Foo) listOrSingleEl; ArrayList<Foo> list = new ArrayList<>(); listOrSingleEl = list; list.add(firstEl); list.add(foo); } else { //      ((ArrayList<Foo>) listOrSingleEl).add(foo); } } 

Jelas, kode dengan optimasi ini kurang jelas dan sulit untuk dipertahankan. Tapi ini bisa berguna jika Anda yakin ini akan menghemat banyak memori atau membuang jeda panjang dari pengumpul sampah.

Anda mungkin sudah bertanya-tanya: bagaimana cara mengetahui koleksi mana di aplikasi saya yang menggunakan memori dan berapa banyak?

Singkatnya: sulit untuk mengetahui tanpa alat yang tepat. Mencoba menebak jumlah memori yang digunakan atau dihabiskan oleh struktur data dalam aplikasi kompleks besar hampir tidak akan pernah mengarah pada apa pun. Dan, tidak tahu persis ke mana ingatannya pergi, Anda dapat menghabiskan banyak waktu mengejar tujuan yang salah, sementara aplikasi Anda dengan keras kepala terus OutOfMemoryError dengan OutOfMemoryError .

Oleh karena itu, Anda harus memeriksa banyak aplikasi menggunakan alat khusus. Dari pengalaman, cara paling optimal untuk menganalisis memori JVM (diukur sebagai jumlah informasi yang tersedia dibandingkan dengan efek alat ini pada kinerja aplikasi) adalah untuk mendapatkan heap dump dan kemudian melihatnya secara offline. Heap dump pada dasarnya adalah snapshot lengkap dari heap. Anda bisa mendapatkannya kapan saja dengan memanggil utilitas jmap, atau Anda dapat mengonfigurasi JVM untuk secara otomatis dibuang jika aplikasi crash dengan OutOfMemoryError . Jika Anda google "JVM heap dump", Anda akan segera melihat sejumlah besar artikel yang menjelaskan secara detail cara mendapatkan dump.

Tumpukan tumpukan adalah file biner ukuran tumpukan JVM, sehingga hanya dapat dibaca dan dianalisis menggunakan alat khusus. Ada beberapa alat, baik open source maupun komersial. Alat open source paling populer adalah Eclipse MAT; ada juga VisualVM dan beberapa alat yang kurang kuat dan kurang terkenal. Alat komersial termasuk profiler Java untuk keperluan umum: JProfiler dan YourKit, serta satu alat yang dirancang khusus untuk analisis tumpukan timbunan - JXRay (penafian: terakhir dikembangkan oleh penulis).

Tidak seperti alat-alat lain, JXRay segera menganalisis tumpukan timbunan untuk sejumlah besar masalah umum, seperti garis berulang dan objek lainnya, serta struktur data yang kurang efisien. Masalah dengan koleksi yang dijelaskan di atas termasuk dalam kategori yang terakhir. Alat ini menghasilkan laporan dengan semua informasi yang dikumpulkan dalam format HTML. Keuntungan dari pendekatan ini adalah Anda dapat melihat hasil analisis di mana saja kapan saja dan dengan mudah membagikannya kepada orang lain. Anda juga dapat menjalankan alat di mesin apa pun, termasuk mesin besar dan kuat, tetapi "tanpa kepala" di pusat data.

JXRay menghitung overhead (berapa banyak memori yang akan Anda simpan jika Anda menyingkirkan masalah tertentu) dalam byte dan sebagai persentase dari tumpukan yang digunakan. Ini menggabungkan koleksi dari kelas yang sama yang memiliki masalah yang sama ...



... dan kemudian mengelompokkan koleksi bermasalah yang dapat diakses dari beberapa akar pengumpul sampah melalui rantai tautan yang sama, seperti dalam contoh di bawah ini



Mengetahui rantai tautan mana dan / atau bidang data individual (misalnya, INodeDirectory.children atas) menunjukkan koleksi yang menghabiskan sebagian besar memori mereka memungkinkan Anda untuk dengan cepat dan akurat mengidentifikasi kode yang bertanggung jawab atas masalah tersebut, dan kemudian membuat perubahan yang diperlukan.

Dengan demikian, koleksi Java yang tidak dikonfigurasi dengan benar dapat menghabiskan banyak memori. Dalam banyak situasi, masalah ini mudah diselesaikan, tetapi kadang-kadang Anda mungkin perlu mengubah kode Anda dengan cara yang tidak sepele untuk mencapai peningkatan yang signifikan. Sangat sulit untuk menebak koleksi mana yang perlu dioptimalkan agar memiliki dampak terbesar. Agar tidak membuang waktu mengoptimalkan bagian kode yang salah, Anda perlu mendapatkan heap dump JVM dan menganalisisnya menggunakan alat yang sesuai.

AKHIR

Kami, seperti biasa, tertarik dengan pendapat dan pertanyaan Anda, yang dapat Anda tinggalkan di sini atau mampir dengan pelajaran terbuka dan bertanya kepada guru - guru kami di sana.

Source: https://habr.com/ru/post/id420245/


All Articles