Saya bertanya-tanya apa yang terjadi ketika Anda mencoba menangkap StackOverflowError dan muncul dengan metode berikut:
class RandomNumberGenerator {
static int cnt = 0;
public static void main(String[] args) {
try {
main(args);
} catch (StackOverflowError ignore) {
System.out.println(cnt++);
}
}
}
Sekarang pertanyaan saya:
Mengapa metode ini mencetak '4'?
Saya pikir mungkin itu karena System.out.println()
membutuhkan 3 segmen pada tumpukan panggilan, tetapi saya tidak tahu dari mana angka 3 itu berasal. Saat Anda melihat kode sumber (dan bytecode) System.out.println()
, biasanya hal itu akan menyebabkan lebih banyak pemanggilan metode daripada 3 (jadi 3 segmen pada tumpukan panggilan tidak akan cukup). Jika itu karena optimasi yang diterapkan VM Hotspot (metode inlining), saya ingin tahu apakah hasilnya akan berbeda di VM lain.
Edit :
Karena outputnya tampaknya sangat spesifik JVM, saya mendapatkan hasil 4 menggunakan
Java (TM) SE Runtime Environment (build 1.6.0_41-b02)
Java HotSpot (TM) 64-Bit Server VM (build 20.14-b01, mode campuran)
Penjelasan mengapa menurut saya pertanyaan ini berbeda dari Memahami tumpukan Java :
Pertanyaan saya bukan tentang mengapa ada cnt> 0 (jelas karena System.out.println()
membutuhkan ukuran tumpukan dan melempar yang lain StackOverflowError
sebelum sesuatu dicetak), tetapi mengapa ia memiliki nilai tertentu 4, masing-masing 0,3,8,55 atau sesuatu yang lain di lain sistem.
sumber
5
,6
dan38
dengan Java 1.7.0_10Jawaban:
Saya pikir yang lain telah melakukan pekerjaan yang baik dalam menjelaskan mengapa cnt> 0, tetapi tidak ada cukup detail mengenai mengapa cnt = 4, dan mengapa cnt sangat bervariasi di antara pengaturan yang berbeda. Saya akan mencoba mengisi kekosongan itu di sini.
Membiarkan
System.out.println
Saat pertama kali kita masuk ke main, space yang tersisa adalah XM. Setiap panggilan rekursif membutuhkan R lebih banyak memori. Jadi untuk 1 panggilan rekursif (1 lebih banyak dari aslinya), penggunaan memori adalah M + R. Misalkan StackOverflowError dilempar setelah C panggilan rekursif berhasil, yaitu, M + C * R <= X dan M + C * (R + 1)> X. Pada saat StackOverflowError pertama, ada memori X - M - C * R yang tersisa.
Untuk dapat menjalankannya
System.out.prinln
, kita membutuhkan sejumlah P ruang yang tersisa di stack. Jika kebetulan X - M - C * R> = P, maka 0 akan dicetak. Jika P membutuhkan lebih banyak ruang, maka kami menghapus bingkai dari tumpukan, mendapatkan memori R dengan biaya cnt ++.Ketika
println
akhirnya bisa dijalankan, X - M - (C - cnt) * R> = P. Jadi jika P besar untuk sistem tertentu, maka cnt akan besar.Mari kita lihat ini dengan beberapa contoh.
Contoh 1: Misalkan
Kemudian C = lantai ((XM) / R) = 49, dan cnt = langit-langit ((P - (X - M - C * R)) / R) = 0.
Contoh 2: Misalkan
Kemudian C = 19, dan cnt = 2.
Contoh 3: Misalkan
Maka C = 20, dan cnt = 3.
Contoh 4: Misalkan
Kemudian C = 19, dan cnt = 2.
Jadi, kita melihat bahwa sistem (M, R, dan P) dan ukuran tumpukan (X) mempengaruhi cnt.
Sebagai catatan tambahan, tidak peduli berapa banyak ruang yang
catch
dibutuhkan untuk memulai. Selama tidak ada cukup ruangcatch
, maka cnt tidak akan bertambah, jadi tidak ada efek eksternal.EDIT
Saya menarik kembali apa yang saya katakan
catch
. Itu memang memainkan peran. Misalkan itu membutuhkan jumlah T ruang untuk memulai. cnt mulai bertambah ketika ruang sisa lebih besar dari T, danprintln
berjalan ketika ruang sisa lebih besar dari T + P. Ini menambahkan langkah ekstra ke perhitungan dan selanjutnya mengaburkan analisis yang sudah berlumpur.EDIT
Saya akhirnya menemukan waktu untuk menjalankan beberapa eksperimen untuk mendukung teori saya. Sayangnya, teori tersebut tampaknya tidak cocok dengan eksperimen. Apa yang sebenarnya terjadi sangatlah berbeda.
Pengaturan percobaan: Server Ubuntu 12.04 dengan java default dan default-jdk. XSS mulai dari 70.000 dengan kenaikan 1 byte menjadi 460.000.
Hasilnya tersedia di: https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM Saya telah membuat versi lain di mana setiap titik data berulang dihapus. Dengan kata lain, hanya poin yang berbeda dari sebelumnya yang ditampilkan. Ini membuatnya lebih mudah untuk melihat anomali. https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA
sumber
StackOverflowError
dilempar dan bagaimana hal ini memengaruhi output. Jika hanya berisi referensi ke bingkai tumpukan di heap (seperti yang disarankan Jay), maka keluarannya harus cukup dapat diprediksi untuk sistem tertentu.Ini adalah korban panggilan rekursif yang buruk. Saat Anda bertanya-tanya mengapa nilai cnt bervariasi, itu karena ukuran tumpukan bergantung pada platform. Java SE 6 di Windows memiliki ukuran tumpukan default 320k di VM 32-bit dan 1024k di VM 64-bit. Anda dapat membaca lebih lanjut di sini .
Anda dapat menjalankan menggunakan ukuran tumpukan yang berbeda dan Anda akan melihat nilai cnt yang berbeda sebelum tumpukan meluap-
Anda tidak melihat nilai cnt dicetak beberapa kali meskipun nilainya lebih besar dari 1 kadang-kadang karena pernyataan cetak Anda juga menampilkan kesalahan yang dapat Anda debug untuk memastikannya melalui Eclipse atau IDE lainnya.
Anda dapat mengubah kode berikut untuk men-debug per eksekusi pernyataan jika Anda lebih suka-
MEMPERBARUI:
Karena ini mendapatkan lebih banyak perhatian, mari kita punya contoh lain untuk membuat segalanya lebih jelas-
Kami membuat metode lain bernama overflow untuk melakukan rekursi yang buruk dan menghapus pernyataan println dari blok catch sehingga tidak mulai memunculkan serangkaian kesalahan lain saat mencoba mencetak. Ini bekerja seperti yang diharapkan. Anda dapat mencoba meletakkan System.out.println (cnt); pernyataan setelah cnt ++ di atas dan kompilasi. Kemudian jalankan beberapa kali. Bergantung pada platform Anda, Anda mungkin mendapatkan nilai cnt yang berbeda .
Inilah sebabnya mengapa umumnya kita tidak menangkap kesalahan karena misteri dalam kode bukanlah fantasi.
sumber
Perilaku ini bergantung pada ukuran tumpukan (yang dapat disetel secara manual menggunakan
Xss
. Ukuran tumpukan adalah khusus arsitektur. Dari kode sumber JDK 7 :Jadi ketika
StackOverflowError
dilempar, kesalahan tersebut terperangkap di blok tangkap. Berikutprintln()
adalah panggilan tumpukan lain yang memunculkan pengecualian lagi. Ini diulangi.Berapa kali berulang? - Tergantung pada saat JVM menganggapnya bukan lagi stackoverflow. Dan itu tergantung pada ukuran tumpukan dari setiap pemanggilan fungsi (sulit ditemukan) dan
Xss
. Seperti disebutkan di atas, ukuran dan ukuran total default dari setiap panggilan fungsi (tergantung pada ukuran halaman memori, dll.) Adalah spesifik platform. Oleh karena itu perilakunya berbeda.Memanggil
java
panggilan dengan-Xss 4M
memberi saya41
. Oleh karena itu korelasinya.sumber
catch
adalah satu blok dan itu menempati memori di tumpukan. Berapa banyak memori yang dibutuhkan setiap panggilan metode tidak dapat diketahui. Saat tumpukan dibersihkan, Anda menambahkan satu blok lagicatch
dan seterusnya. Ini mungkin perilakunya. Ini hanya spekulasi.Saya pikir nomor yang ditampilkan adalah berapa kali
System.out.println
panggilan tersebut melakukanStackoverflow
pengecualian.Ini mungkin tergantung pada implementasi
println
dan jumlah panggilan susun yang dibuat di dalamnya.Sebagai ilustrasi:
The
main()
panggilan memicuStackoverflow
pengecualian di panggilan saya. Panggilan i-1 dari main menangkap pengecualian dan panggilanprintln
yang memicu detikStackoverflow
.cnt
dapatkan peningkatan menjadi 1. Panggilan i-2 dari tangkapan utama sekarang pengecualian dan panggilanprintln
. Dalamprintln
suatu metode disebut memicu pengecualian ke-3.cnt
dapatkan increment ke 2. ini terus berlanjut hinggaprintln
dapat membuat semua panggilan yang dibutuhkan dan akhirnya menampilkan nilaicnt
.Ini kemudian bergantung pada implementasi aktual
println
.Untuk JDK7, baik itu mendeteksi panggilan bersepeda dan melontarkan pengecualian lebih awal, baik itu menyimpan beberapa sumber daya tumpukan dan membuang pengecualian sebelum mencapai batas untuk memberikan ruang bagi logika remediasi, baik
println
implementasi tidak membuat panggilan baik operasi ++ dilakukan setelahnya yangprintln
panggilan dengan demikian by pass oleh pengecualian.sumber
main
berulang dengan sendirinya sampai meluap tumpukan pada kedalaman rekursiR
.R-1
dijalankan.R-1
mengevaluasicnt++
.R-1
panggilan kedalamanprintln
, menempatkancnt
nilai lama di tumpukan.println
secara internal akan memanggil metode lain dan menggunakan variabel dan hal-hal lokal. Semua proses ini membutuhkan ruang tumpukan.println
memerlukan ruang tumpukan, tumpukan overflow baru dipicu di kedalaman,R-1
bukan di kedalamanR
.R-2
.R-3
.R-4
.R-5
.println
diselesaikan (perhatikan bahwa ini adalah detail implementasi, ini mungkin berbeda).cnt
itu pasca-bertambah pada kedalamanR-1
,R-2
,R-3
,R-4
, dan akhirnya diR-5
. Kenaikan pos kelima menghasilkan empat, yang dicetak.main
berhasil diselesaikan di kedalamanR-5
, seluruh tumpukan terlepas tanpa lebih banyak blok tangkapan yang dijalankan dan program selesai.sumber
Setelah mencari-cari beberapa saat, saya tidak dapat mengatakan bahwa saya menemukan jawabannya, tetapi saya pikir itu sudah cukup dekat sekarang.
Pertama, kita perlu tahu kapan surat
StackOverflowError
wasiat dilempar. Faktanya, tumpukan untuk utas java menyimpan bingkai, yang berisi semua data yang diperlukan untuk menjalankan metode dan melanjutkan. Menurut Spesifikasi Bahasa Java untuk JAVA 6 , saat menjalankan metode,Kedua, kita harus memperjelas apa yang " tidak tersedia cukup memori untuk membuat bingkai aktivasi seperti itu ". Menurut Spesifikasi Mesin Virtual Java untuk JAVA 6 ,
Jadi, ketika bingkai dibuat, harus ada cukup ruang heap untuk membuat bingkai tumpukan dan cukup ruang tumpukan untuk menyimpan referensi baru yang mengarah ke bingkai tumpukan baru jika bingkai dialokasikan heap.
Sekarang mari kembali ke pertanyaan. Dari penjelasan di atas, kita dapat mengetahui bahwa ketika sebuah metode dieksekusi, mungkin biayanya sama dengan jumlah ruang tumpukan. Dan pemanggilan
System.out.println
(may) membutuhkan 5 tingkat pemanggilan metode, jadi 5 bingkai perlu dibuat. Kemudian ketikaStackOverflowError
dibuang, itu harus kembali 5 kali untuk mendapatkan cukup ruang tumpukan untuk menyimpan referensi 5 bingkai. Oleh karena itu 4 dicetak. Mengapa tidak 5? Karena Anda menggunakancnt++
. Ubah menjadi++cnt
, dan Anda akan mendapatkan 5.Dan Anda akan melihat bahwa ketika ukuran tumpukan naik ke tingkat yang tinggi, terkadang Anda akan mendapatkan 50. Itu karena jumlah ruang heap yang tersedia perlu dipertimbangkan kemudian. Jika ukuran tumpukan terlalu besar, mungkin ruang tumpukan akan habis sebelum tumpukan. Dan (mungkin) ukuran sebenarnya dari bingkai tumpukan
System.out.println
adalah sekitar 51 kalimain
, oleh karena itu kembali 51 kali dan mencetak 50.sumber
System.out.print
atau strategi eksekusi metode. Seperti dijelaskan di atas, implementasi VM juga memengaruhi tempat penyimpanan frame sebenarnya.Ini sebenarnya bukan jawaban untuk pertanyaan tetapi saya hanya ingin menambahkan sesuatu ke pertanyaan asli yang saya temukan dan bagaimana saya memahami masalahnya:
Dalam masalah aslinya, pengecualian ditangkap di tempat yang memungkinkan:
Misalnya dengan jdk 1.7 itu tertangkap di tempat kemunculan pertama.
tetapi di versi sebelumnya dari jdk sepertinya pengecualian tidak tertangkap di tempat pertama kemunculannya, maka 4, 50 dll ..
Sekarang jika Anda menghapus blok coba tangkap sebagai berikut
Kemudian Anda akan melihat semua nilai
cnt
ant pengecualian yang dilempar (di jdk 1.7).Saya menggunakan netbeans untuk melihat hasilnya, karena cmd tidak akan menampilkan semua keluaran dan pengecualian yang dilempar.
sumber