Selamat datang di bagian lain Under The Hood . Kolom ini memberikan gambaran sekilas kepada pengembang Java tentang mekanisme misterius yang mengklik dan berputar di bawah program Java yang sedang berjalan. Artikel bulan ini melanjutkan diskusi tentang set instruksi bytecode dari mesin virtual Java (JVM). Fokusnya adalah cara JVM menangani finally
klausa dan bytecode yang relevan dengan klausa ini.
Akhirnya: Sesuatu untuk disemangati
Saat mesin virtual Java mengeksekusi bytecode yang mewakili program Java, ia mungkin keluar dari blok kode - pernyataan di antara dua tanda kurung kurawal yang cocok - dengan salah satu dari beberapa cara. Untuk satu, JVM hanya bisa mengeksekusi melewati kurung kurawal penutup dari blok kode. Atau, ia bisa menemukan pernyataan break, continue, atau return yang menyebabkannya melompat keluar dari blok kode dari suatu tempat di tengah blok. Akhirnya, pengecualian bisa dilemparkan yang menyebabkan JVM melompat ke klausa catch yang cocok, atau, jika tidak ada klausa catch yang cocok, untuk menghentikan utas. Dengan potensi titik keluar yang ada dalam satu blok kode, diharapkan ada cara mudah untuk menyatakan bahwa sesuatu telah terjadi tidak peduli bagaimana blok kode keluar. Di Jawa, keinginan seperti itu diungkapkan dengan atry-finally
ayat.
Untuk menggunakan try-finally
klausa:
menyertakan
try
kode yang memiliki beberapa titik keluar, danletakkan di
finally
blok kode yang harus terjadi tidak peduli bagaimanatry
blok itu keluar.
Sebagai contoh:
coba {// Blok kode dengan beberapa titik keluar} akhirnya {// Blok kode yang selalu dijalankan saat blok try keluar, // tidak peduli bagaimana blok try keluar}
Jika Anda memiliki catch
klausa yang terkait dengan try
pemblokiran, Anda harus meletakkan finally
klausa setelah semua catch
klausa, seperti di:
coba {// Blok kode dengan beberapa titik keluar} catch (Cold e) {System.out.println ("Caught cold!"); } catch (APopFly e) {System.out.println ("Menangkap lalat pop!"); } menangkap (SomeonesEye e) {System.out.println ("Menarik perhatian seseorang!"); } akhirnya {// Blok kode yang selalu dieksekusi saat blok try keluar, // tidak peduli bagaimana blok try keluar. System.out.println ("Apakah itu sesuatu yang menarik?"); }
Jika selama eksekusi kode di dalam try
blok, pengecualian dilemparkan yang ditangani oleh catch
klausa yang terkait dengan try
blok, finally
klausa akan dieksekusi setelah catch
klausa. Misalnya, jika Cold
pengecualian dilemparkan selama eksekusi pernyataan (tidak ditampilkan) di try
blok di atas, teks berikut akan ditulis ke keluaran standar:
Tertangkap dingin! Apakah itu sesuatu untuk disemangati?
Klausa coba-akhirnya dalam bytecode
Dalam bytecode, finally
klausa bertindak sebagai subrutin miniatur dalam suatu metode. Pada setiap titik keluar di dalam try
blok dan catch
klausa yang terkait , subrutin miniatur yang sesuai dengan finally
klausa dipanggil. Setelah finally
klausa selesai - selama klausa diselesaikan dengan mengeksekusi melewati pernyataan terakhir dalam finally
klausa, bukan dengan membuat pengecualian atau menjalankan pengembalian, lanjutkan, atau hentikan - subrutin miniatur itu sendiri kembali. Eksekusi berlanjut melewati titik di mana subrutin miniatur dipanggil di tempat pertama, sehingga try
blok dapat keluar dengan cara yang sesuai.
Opcode yang menyebabkan JVM melompat ke subrutin miniatur adalah instruksi jsr . The JSR instruksi mengambil operan dua-byte, offset dari lokasi JSR instruksi mana miniatur subroutine dimulai. Varian kedua dari instruksi jsr adalah jsr_w , yang melakukan fungsi yang sama dengan jsr tetapi membutuhkan operan lebar (empat byte). Ketika JVM bertemu dengan JSR atau jsr_w instruksi, itu mendorong alamat pengirim ke stack, kemudian berlanjut eksekusi pada awal miniatur subrutin. Alamat pengirim adalah offset dari bytecode segera setelah jsr ataujsr_w dan operannya .
Setelah subrutin miniatur selesai, ia memanggil instruksi ret , yang kembali dari subrutin tersebut. The ret instruksi mengambil satu operan, indeks ke dalam variabel lokal di mana alamat pengirim disimpan. Opcode yang menangani finally
klausa diringkas dalam tabel berikut:
Opcode | Operand (s) | Deskripsi |
---|---|---|
jsr |
branchbyte1, branchbyte2 | mendorong alamat pengirim, cabang ke offset |
jsr_w |
branchbyte1, branchbyte2, branchbyte3, branchbyte4 | mendorong alamat pengirim, cabang ke offset lebar |
ret |
indeks | kembali ke alamat yang disimpan dalam indeks variabel lokal |
Jangan bingung antara subrutin miniatur dengan metode Java. Metode Java menggunakan sekumpulan instruksi yang berbeda. Instruksi seperti invokevirtual atau invokenonvirtual menyebabkan metode Java dipanggil, dan instruksi seperti return , areturn , atau ireturn menyebabkan metode Java kembali. The JSR instruksi tidak menyebabkan metode Java akan dipanggil. Sebaliknya, ini menyebabkan lompatan ke opcode berbeda dalam metode yang sama. Demikian juga, instruksi ret tidak kembali dari metode; sebaliknya, ia kembali ke opcode dalam metode yang sama yang segera mengikuti instruksi pemanggil jsr dan operannya . Bytecode yang mengimplementasikan filefinally
klausa disebut subrutin miniatur karena mereka bertindak seperti subrutin kecil dalam aliran bytecode dari satu metode.
Anda mungkin berpikir bahwa instruksi ret harus mengeluarkan alamat pengirim dari tumpukan, karena di situlah ia didorong oleh instruksi jsr . Tapi ternyata tidak. Sebagai gantinya, di awal setiap subrutin, alamat pengembalian muncul dari bagian atas tumpukan dan disimpan dalam variabel lokal - variabel lokal yang sama dari mana instruksi ret kemudian mendapatkannya. Cara asimetris ini bekerja dengan alamat pengirim diperlukan karena akhirnya klausa (dan oleh karena itu, subrutin miniatur) sendiri bisa melemparkan pengecualian atau menyertakan return
, break
atau continue
pernyataan. Karena kemungkinan ini, alamat pengembalian ekstra yang didorong ke tumpukan oleh jsrinstruksi harus dikeluarkan dari tumpukan segera, sehingga tidak akan tetap ada jika finally
klausul keluar dengan break
, continue
, return
, atau pengecualian dilemparkan. Oleh karena itu, alamat pengirim disimpan ke dalam variabel lokal di awal finally
subrutin miniatur klausa mana pun .
Sebagai ilustrasi, pertimbangkan kode berikut, yang menyertakan finally
klausa yang keluar dengan pernyataan break. Hasil dari kode ini adalah, terlepas dari parameter bVal yang diteruskan ke metode surpriseTheProgrammer()
, metode tersebut mengembalikan false
:
static boolean surpriseTheProgrammer (boolean bVal) {while (bVal) {coba {return true; } akhirnya {break; }} return false; }
Contoh di atas menunjukkan mengapa alamat pengirim harus disimpan ke dalam variabel lokal di awal finally
klausa. Karena finally
klausa keluar dengan jeda, itu tidak pernah menjalankan instruksi ret . Akibatnya, JVM tidak pernah kembali untuk menyelesaikan return true
pernyataan " ". Sebaliknya, itu hanya berjalan ke depan break
dan turun melewati kurung kurawal penutup while
pernyataan. Pernyataan berikutnya adalah " return false
," yang persis seperti yang dilakukan JVM.
Perilaku yang ditunjukkan oleh finally
klausa yang keluar dengan a break
juga ditunjukkan oleh finally
klausa yang keluar dengan return
atau continue
, atau dengan melakukan pengecualian. Jika finally
klausa keluar karena salah satu alasan ini, instruksi ret di akhir finally
klausa tidak pernah dijalankan. Karena instruksi ret tidak dijamin akan dieksekusi, itu tidak dapat diandalkan untuk menghapus alamat pengirim dari tumpukan. Oleh karena itu, alamat pengirim disimpan ke dalam variabel lokal di awal finally
subrutin miniatur klausa.
Untuk contoh lengkap, pertimbangkan metode berikut, yang berisi try
blok dengan dua titik keluar. Dalam contoh ini, kedua titik keluar adalah return
pernyataan:
static int giveMeThatOldFashionedBoolean (boolean bVal) {coba {if (bVal) {return 1; } kembali 0; } akhirnya {System.out.println ("Got old fashioned."); }}
Metode di atas mengkompilasi ke bytecode berikut:
// Urutan bytecode untuk blok percobaan: 0 iload_0 // Dorong variabel lokal 0 (arg diteruskan sebagai pembagi) 1 ifeq 11 // Dorong variabel lokal 1 (arg dilewatkan sebagai dividen) 4 iconst_1 // Dorong int 1 5 istore_3 // Masukkan int (1), simpan ke dalam variabel lokal 3 6 jsr 24 // Lompat ke subrutin-mini untuk klausa terakhir 9 iload_3 // Dorong variabel lokal 3 (yang 1) 10 ireturn // Kembalikan int di atas stack (the 1) 11 iconst_0 // Push int 0 12 istore_3 // Pop an int (the 0), simpan ke dalam variabel lokal 3 13 jsr 24 // Lompat ke subrutin-mini untuk klausa terakhir 16 iload_3 // Dorong lokal variabel 3 (the 0) 17 ireturn // Return int di atas tumpukan (the 0) // Urutan bytecode untuk klausa catch yang menangkap segala jenis pengecualian // dilempar dari dalam blok percobaan. 18 astore_1 // Pop referensi ke pengecualian terlempar,store // ke dalam variabel lokal 1 19 jsr 24 // Lompat ke subrutin-mini untuk klausa terakhir 22 aload_1 // Dorong referensi (ke pengecualian yang ditampilkan) dari // variabel lokal 1 23 setelah // Lempar kembali pengecualian yang sama / / Miniatur subrutin yang mengimplementasikan blok akhirnya. 24 astore_2 // Masukkan alamat pengirim, simpan di variabel lokal 2 25 getstatic # 8 // Dapatkan referensi ke java.lang.System.out 28 ldc # 1 // Dorong dari kumpulan konstan 30 invokevirtual # 7 // Panggil System.out.println () 33 ret 2 // Kembali ke alamat pengirim yang disimpan dalam variabel lokal 2simpan di variabel lokal 2 25 getstatic # 8 // Dapatkan referensi ke java.lang.System.out 28 ldc # 1 // Dorong dari kumpulan konstan 30 invokevirtual # 7 // Panggil System.out.println () 33 ret 2 // Kembali ke alamat pengirim yang disimpan dalam variabel lokal 2simpan di variabel lokal 2 25 getstatic # 8 // Dapatkan referensi ke java.lang.System.out 28 ldc # 1 // Dorong dari kumpulan konstan 30 invokevirtual # 7 // Panggil System.out.println () 33 ret 2 // Kembali ke alamat pengirim yang disimpan dalam variabel lokal 2
Bytecode untuk try
blok tersebut mencakup dua instruksi jsr . Instruksi jsr lain terdapat dalam catch
klausa. The catch
klausul ditambahkan oleh compiler karena jika eksepsi dilemparkan selama eksekusi dari try
blok, blok akhirnya masih harus dijalankan. Oleh karena itu, catch
klausa tersebut hanya memanggil subrutin miniatur yang mewakili finally
klausa tersebut, lalu memunculkan kembali pengecualian yang sama. Tabel pengecualian untuk giveMeThatOldFashionedBoolean()
metode tersebut, yang ditunjukkan di bawah, menunjukkan bahwa pengecualian apa pun yang dilemparkan antara dan termasuk alamat 0 dan 17 (semua bytecode yang mengimplementasikan try
blok) ditangani oleh catch
klausa yang dimulai pada alamat 18.
Tabel pengecualian: dari jenis target 0 18 18 apa saja
Bytecode dari finally
klausa dimulai dengan mengeluarkan alamat pengirim dari tumpukan dan menyimpannya ke dalam variabel lokal dua. Di akhir finally
klausa, instruksi ret mengambil alamat pengirimnya dari tempat yang tepat, variabel lokal dua.
HopAround: Simulasi mesin virtual Java
Applet di bawah ini mendemonstrasikan mesin virtual Java yang menjalankan urutan bytecode. Urutan bytecode dalam simulasi dihasilkan oleh javac
kompiler untuk hopAround()
metode kelas yang ditunjukkan di bawah ini:
kelas Clown {static int hopAround () {int i = 0; sementara (benar) {coba {coba {i = 1; } akhirnya {// yang pertama akhirnya klausa i = 2; } i = 3; kembali i; // ini tidak pernah selesai, karena lanjutkan} akhirnya {// klausa akhirnya yang kedua if (i == 3) {lanjutkan; // ini terus menimpa pernyataan return}}}}}
Bytecode yang dihasilkan oleh javac
untuk hopAround()
metode ini ditampilkan di bawah ini: