Mengapa metode ini mencetak 4?

111

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 StackOverflowErrorsebelum sesuatu dicetak), tetapi mengapa ia memiliki nilai tertentu 4, masing-masing 0,3,8,55 atau sesuatu yang lain di lain sistem.

flrnb
sumber
4
Di lokal saya, saya mendapatkan "0" seperti yang diharapkan.
Reddy
2
Ini mungkin berhubungan dengan banyak hal arsitektur. Jadi lebih baik posting keluaran Anda dengan versi jdk Bagi saya keluaran adalah 0 pada jdk 1.7
Lokesh
3
Saya mendapatkan 5, 6dan 38dengan Java 1.7.0_10
Kon
8
@Elist Tidak akan menjadi keluaran yang sama ketika Anda melakukan trik yang melibatkan arsitektur yang mendasari;)
m0skit0
3
@flrnb Ini hanya gaya yang saya gunakan untuk menyejajarkan kawat gigi. Ini memudahkan saya untuk mengetahui di mana kondisi dan fungsi dimulai dan diakhiri. Anda bisa mengubahnya jika mau, tapi menurut saya lebih mudah dibaca dengan cara ini.
syb0rg

Jawaban:

41

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

  • X menjadi ukuran tumpukan total
  • M menjadi ruang stack yang digunakan saat kita masuk main pertama kali
  • R menjadi peningkatan ruang stack setiap kali kita masuk ke main
  • P menjadi ruang tumpukan yang diperlukan untuk menjalankan 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 printlnakhirnya 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

  • X = 100
  • M = 1
  • R = 2
  • P = 1

Kemudian C = lantai ((XM) / R) = 49, dan cnt = langit-langit ((P - (X - M - C * R)) / R) = 0.

Contoh 2: Misalkan

  • X = 100
  • M = 1
  • R = 5
  • P = 12

Kemudian C = 19, dan cnt = 2.

Contoh 3: Misalkan

  • X = 101
  • M = 1
  • R = 5
  • P = 12

Maka C = 20, dan cnt = 3.

Contoh 4: Misalkan

  • X = 101
  • M = 2
  • R = 5
  • P = 12

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 catchdibutuhkan untuk memulai. Selama tidak ada cukup ruang catch, 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, dan printlnberjalan 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

John Tseng
sumber
Terima kasih atas ringkasan yang bagus, saya rasa semuanya bermuara pada pertanyaan: apa dampak M, R dan P (karena X dapat diatur oleh VM-option -Xss)?
flrnb
@flrnb M, R, dan P adalah khusus sistem. Anda tidak dapat dengan mudah mengubahnya. Saya berharap mereka juga berbeda di antara beberapa rilis.
John Tseng
Lalu, mengapa saya mendapatkan hasil yang berbeda dengan mengubah Xss (alias X)? Mengubah X dari 100 menjadi 10000, mengingat bahwa M, R dan P tetap sama, seharusnya tidak mempengaruhi cnt menurut rumus Anda, atau apakah saya salah?
flrnb
@flrnb X saja tidak mengubah cnt karena sifat diskrit dari variabel-variabel ini. Contoh 2 dan 3 hanya berbeda pada X, tetapi cnt berbeda.
John Tseng
1
@ JohnTseng Saya juga menganggap jawaban Anda paling mudah dipahami dan lengkap saat ini - bagaimanapun, saya akan sangat tertarik dengan tampilan sebenarnya dari tumpukan saat StackOverflowErrordilempar 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.
flrnb
20

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-

java -Xss1024k RandomNumberGenerator

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-

static int cnt = 0;

public static void main(String[] args) {                  

    try {     

        main(args);   

    } catch (Throwable ignore) {

        cnt++;

        try { 

            System.out.println(cnt);

        } catch (Throwable t) {   

        }        
    }        
}

MEMPERBARUI:

Karena ini mendapatkan lebih banyak perhatian, mari kita punya contoh lain untuk membuat segalanya lebih jelas-

static int cnt = 0;

public static void overflow(){

    try {     

      overflow();     

    } catch (Throwable t) {

      cnt++;                      

    }

}

public static void main(String[] args) {

    overflow();
    System.out.println(cnt);

}

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.

Sajal Dutta
sumber
13

Perilaku ini bergantung pada ukuran tumpukan (yang dapat disetel secara manual menggunakan Xss. Ukuran tumpukan adalah khusus arsitektur. Dari kode sumber JDK 7 :

// Ukuran tumpukan default pada Windows ditentukan oleh file yang dapat dieksekusi (java.exe
// memiliki nilai default 320K / 1MB [32bit / 64bit]). Bergantung pada versi Windows, mengubah
// ThreadStackSize menjadi bukan nol dapat berdampak signifikan pada penggunaan memori.
// Lihat komentar di os_windows.cpp.

Jadi ketika StackOverflowErrordilempar, kesalahan tersebut terperangkap di blok tangkap. Berikut println()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 javapanggilan dengan -Xss 4Mmemberi saya 41. Oleh karena itu korelasinya.

Jatin
sumber
4
Saya tidak mengerti mengapa ukuran tumpukan harus mempengaruhi hasil, karena sudah terlampaui ketika kami mencoba mencetak nilai cnt. Dengan demikian, satu-satunya perbedaan bisa datang dari "ukuran tumpukan dari setiap pemanggilan fungsi". Dan saya tidak mengerti mengapa ini harus bervariasi di antara 2 mesin yang menjalankan versi JVM yang sama.
flrnb
Perilaku yang tepat hanya dapat diperoleh dari sumber JVM. Tapi alasannya bisa jadi ini. Ingat even catchadalah 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 lagi catchdan seterusnya. Ini mungkin perilakunya. Ini hanya spekulasi.
Jatin
Dan ukuran tumpukan dapat berbeda di dua mesin yang berbeda. Ukuran tumpukan bergantung pada banyak faktor berbasis os yaitu ukuran halaman memori dll
Jatin
6

Saya pikir nomor yang ditampilkan adalah berapa kali System.out.printlnpanggilan tersebut melakukan Stackoverflowpengecualian.

Ini mungkin tergantung pada implementasi printlndan jumlah panggilan susun yang dibuat di dalamnya.

Sebagai ilustrasi:

The main()panggilan memicu Stackoverflowpengecualian di panggilan saya. Panggilan i-1 dari main menangkap pengecualian dan panggilan printlnyang memicu detik Stackoverflow. cntdapatkan peningkatan menjadi 1. Panggilan i-2 dari tangkapan utama sekarang pengecualian dan panggilan println. Dalam printlnsuatu metode disebut memicu pengecualian ke-3. cntdapatkan increment ke 2. ini terus berlanjut hingga printlndapat membuat semua panggilan yang dibutuhkan dan akhirnya menampilkan nilai cnt.

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 printlnimplementasi tidak membuat panggilan baik operasi ++ dilakukan setelahnya yang printlnpanggilan dengan demikian by pass oleh pengecualian.

Kazaag
sumber
Itulah yang saya maksud dengan "Saya pikir mungkin itu karena System.out.println membutuhkan 3 segmen pada tumpukan panggilan" - tetapi saya bingung mengapa tepatnya nomor ini dan sekarang saya bahkan lebih bingung mengapa angkanya sangat berbeda di antara yang berbeda (virtual) mesin
flrnb
Saya setuju sebagian untuk itu tetapi apa yang saya tidak setuju adalah pada pernyataan `tergantung dari implementasi aktual println.` Ini ada hubungannya dengan ukuran tumpukan di setiap jvm daripada implementasi.
Jatin
6
  1. mainberulang dengan sendirinya sampai meluap tumpukan pada kedalaman rekursi R.
  2. Blok tangkapan di kedalaman rekursi R-1dijalankan.
  3. Blok tangkapan pada kedalaman rekursi R-1mengevaluasi cnt++.
  4. Blok tangkapan di R-1panggilan kedalaman println, menempatkan cntnilai lama di tumpukan. printlnsecara internal akan memanggil metode lain dan menggunakan variabel dan hal-hal lokal. Semua proses ini membutuhkan ruang tumpukan.
  5. Karena tumpukan sudah mencapai batasnya, dan panggilan / eksekusi printlnmemerlukan ruang tumpukan, tumpukan overflow baru dipicu di kedalaman, R-1bukan di kedalaman R.
  6. Langkah 2-5 terjadi lagi, tetapi pada kedalaman rekursi R-2.
  7. Langkah 2-5 terjadi lagi, tetapi pada kedalaman rekursi R-3.
  8. Langkah 2-5 terjadi lagi, tetapi pada kedalaman rekursi R-4.
  9. Langkah 2-4 terjadi lagi, tetapi pada kedalaman rekursi R-5.
  10. Kebetulan ada cukup ruang tumpukan sekarang untuk printlndiselesaikan (perhatikan bahwa ini adalah detail implementasi, ini mungkin berbeda).
  11. cntitu pasca-bertambah pada kedalaman R-1, R-2, R-3, R-4, dan akhirnya di R-5. Kenaikan pos kelima menghasilkan empat, yang dicetak.
  12. Dengan mainberhasil diselesaikan di kedalaman R-5, seluruh tumpukan terlepas tanpa lebih banyak blok tangkapan yang dijalankan dan program selesai.
Craig Gidney
sumber
1

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 StackOverflowErrorwasiat 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,

Jika tidak ada cukup memori yang tersedia untuk membuat bingkai aktivasi seperti itu, StackOverflowError akan dilempar.

Kedua, kita harus memperjelas apa yang " tidak tersedia cukup memori untuk membuat bingkai aktivasi seperti itu ". Menurut Spesifikasi Mesin Virtual Java untuk JAVA 6 ,

frame mungkin merupakan heap yang dialokasikan.

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 ketika StackOverflowErrordibuang, 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 menggunakan cnt++. 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.printlnadalah sekitar 51 kali main, oleh karena itu kembali 51 kali dan mencetak 50.

Jay
sumber
Pikiran pertama saya juga menghitung tingkat pemanggilan metode (dan Anda benar, saya tidak memperhatikan fakta bahwa saya memposting increment cnt), tetapi jika solusinya sesederhana itu, mengapa hasilnya sangat bervariasi di seluruh platform dan implementasi VM?
flrnb
@flrnb Itu karena platform yang berbeda dapat mempengaruhi ukuran stack frame dan versi jre yang berbeda akan mempengaruhi implementasi System.out.printatau strategi eksekusi metode. Seperti dijelaskan di atas, implementasi VM juga memengaruhi tempat penyimpanan frame sebenarnya.
Jay
0

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

public static void main( String[] args ){
    System.out.println(cnt++);
    main(args);
}

Kemudian Anda akan melihat semua nilai cntant pengecualian yang dilempar (di jdk 1.7).

Saya menggunakan netbeans untuk melihat hasilnya, karena cmd tidak akan menampilkan semua keluaran dan pengecualian yang dilempar.

me_digvijay
sumber