Klausa coba-akhirnya ditentukan dan didemonstrasikan

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 finallyklausa 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-finallyklausa:

  • menyertakan trykode yang memiliki beberapa titik keluar, dan

  • letakkan di finallyblok kode yang harus terjadi tidak peduli bagaimana tryblok 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 catchklausa yang terkait dengan trypemblokiran, Anda harus meletakkan finallyklausa setelah semua catchklausa, 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 tryblok, pengecualian dilemparkan yang ditangani oleh catchklausa yang terkait dengan tryblok, finallyklausa akan dieksekusi setelah catchklausa. Misalnya, jika Coldpengecualian dilemparkan selama eksekusi pernyataan (tidak ditampilkan) di tryblok di atas, teks berikut akan ditulis ke keluaran standar:

Tertangkap dingin! Apakah itu sesuatu untuk disemangati?

Klausa coba-akhirnya dalam bytecode

Dalam bytecode, finallyklausa bertindak sebagai subrutin miniatur dalam suatu metode. Pada setiap titik keluar di dalam tryblok dan catchklausa yang terkait , subrutin miniatur yang sesuai dengan finallyklausa dipanggil. Setelah finallyklausa selesai - selama klausa diselesaikan dengan mengeksekusi melewati pernyataan terakhir dalam finallyklausa, 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 tryblok 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 finallyklausa diringkas dalam tabel berikut:

Akhirnya klausul
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 filefinallyklausa 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, breakatau continuepernyataan. Karena kemungkinan ini, alamat pengembalian ekstra yang didorong ke tumpukan oleh jsrinstruksi harus dikeluarkan dari tumpukan segera, sehingga tidak akan tetap ada jika finallyklausul keluar dengan break, continue, return, atau pengecualian dilemparkan. Oleh karena itu, alamat pengirim disimpan ke dalam variabel lokal di awal finallysubrutin miniatur klausa mana pun .

Sebagai ilustrasi, pertimbangkan kode berikut, yang menyertakan finallyklausa 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 finallyklausa. Karena finallyklausa keluar dengan jeda, itu tidak pernah menjalankan instruksi ret . Akibatnya, JVM tidak pernah kembali untuk menyelesaikan return truepernyataan " ". Sebaliknya, itu hanya berjalan ke depan breakdan turun melewati kurung kurawal penutup whilepernyataan. Pernyataan berikutnya adalah " return false," yang persis seperti yang dilakukan JVM.

Perilaku yang ditunjukkan oleh finallyklausa yang keluar dengan a breakjuga ditunjukkan oleh finallyklausa yang keluar dengan returnatau continue, atau dengan melakukan pengecualian. Jika finallyklausa keluar karena salah satu alasan ini, instruksi ret di akhir finallyklausa 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 finallysubrutin miniatur klausa.

Untuk contoh lengkap, pertimbangkan metode berikut, yang berisi tryblok dengan dua titik keluar. Dalam contoh ini, kedua titik keluar adalah returnpernyataan:

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 tryblok tersebut mencakup dua instruksi jsr . Instruksi jsr lain terdapat dalam catchklausa. The catchklausul ditambahkan oleh compiler karena jika eksepsi dilemparkan selama eksekusi dari tryblok, blok akhirnya masih harus dijalankan. Oleh karena itu, catchklausa tersebut hanya memanggil subrutin miniatur yang mewakili finallyklausa 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 tryblok) ditangani oleh catchklausa yang dimulai pada alamat 18.

Tabel pengecualian: dari jenis target 0 18 18 apa saja 

Bytecode dari finallyklausa dimulai dengan mengeluarkan alamat pengirim dari tumpukan dan menyimpannya ke dalam variabel lokal dua. Di akhir finallyklausa, 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 javackompiler 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 javacuntuk hopAround()metode ini ditampilkan di bawah ini: