Bagaimana polimorfisme diimplementasikan di dalam JVM

Terjemahan artikel ini telah disiapkan khusus untuk siswa di kursus Java Developer.





Dalam artikel saya sebelumnya Semuanya Tentang Metode Overloading vs Metode Overriding , kami melihat aturan dan perbedaan metode overloading dan overriding. Pada artikel ini, kita akan melihat bagaimana metode overloading dan overriding ditangani di dalam JVM.

Sebagai contoh, ambil kelas dari artikel sebelumnya: induk Mammal (mamalia) dan anak Human (manusia).

 public class OverridingInternalExample { private static class Mammal { public void speak() { System.out.println("ohlllalalalalalaoaoaoa"); } } private static class Human extends Mammal { @Override public void speak() { System.out.println("Hello"); } //   speak() public void speak(String language) { if (language.equals("Hindi")) System.out.println("Namaste"); else System.out.println("Hello"); } @Override public String toString() { return "Human Class"; } } //           public static void main(String[] args) { Mammal anyMammal = new Mammal(); anyMammal.speak(); // Output - ohlllalalalalalaoaoaoa // 10: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V Mammal humanMammal = new Human(); humanMammal.speak(); // Output - Hello // 23: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V Human human = new Human(); human.speak(); // Output - Hello // 36: invokevirtual #7 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:()V human.speak("Hindi"); // Output - Namaste // 42: invokevirtual #9 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:(Ljava/lang/String;)V } } 

Kita dapat melihat pertanyaan polimorfisme dari dua sisi: dari "logis" dan "fisik". Pertama mari kita lihat sisi logis dari masalah ini.

Sudut pandang logis


Dari sudut pandang logis, pada tahap kompilasi, metode yang dipanggil dianggap terkait dengan jenis referensi. Tetapi pada saat dijalankan, metode objek yang dirujuk akan dipanggil.

Misalnya, di baris humanMammal.speak(); kompiler berpikir bahwa Mammal.speak() akan dipanggil, karena humanMammal dinyatakan sebagai Mammal . Tetapi pada saat dijalankan, JVM akan tahu bahwa humanMammal berisi objek Human dan benar-benar akan memanggil metode Human.speak() .

Semuanya cukup sederhana selama kita tetap pada level konseptual. Tetapi bagaimana JVM menangani ini semua secara internal? Bagaimana JVM menghitung metode mana yang harus dipanggil?

Kita juga tahu bahwa metode kelebihan beban tidak disebut polimorfik dan diselesaikan pada waktu kompilasi. Meskipun kadang-kadang metode overloading disebut kompilasi-waktu polimorfisme atau pengikatan awal / statis .

Metode yang diganti (override) diselesaikan pada saat runtime karena kompiler tidak tahu jika ada metode yang ditimpa dalam objek yang ditugaskan ke tautan.

Sudut pandang fisik


Pada bagian ini, kami akan mencoba menemukan bukti β€œfisik” untuk semua pernyataan di atas. Untuk melakukan ini, lihat bytecode yang bisa kita dapatkan dengan menjalankan javap -verbose OverridingInternalExample . Parameter -verbose akan memungkinkan kita untuk mendapatkan bytecode yang lebih intuitif sesuai dengan program java kita.

Perintah di atas akan menampilkan dua bagian bytecode.

1. Kelompok konstanta . Ini berisi hampir semua yang diperlukan untuk menjalankan program. Misalnya, referensi metode ( #Methodref ), kelas ( #Class ), string literal ( #String ).



2. Bytecode program. Instruksi kode byte yang dapat dieksekusi.



Mengapa metode overloading disebut pengikatan statis


Dalam contoh di atas, kompiler berpikir bahwa metode humanMammal.speak() akan dipanggil dari kelas Mammal , meskipun pada saat run time akan dipanggil dari objek yang dirujuk dalam humanMammal - itu akan menjadi objek dari kelas Human .

Melihat kode kita dan hasil javap , kita melihat bahwa bytecode yang berbeda digunakan untuk memanggil metode humanMammal.speak() , human.speak() dan human.speak("Hindi") , karena kompiler dapat membedakannya berdasarkan referensi kelas .

Dengan demikian, dalam hal terjadi kelebihan metode, kompiler dapat mengidentifikasi instruksi bytecode dan alamat metode pada waktu kompilasi. Itulah sebabnya ini disebut hubungan statis atau compile-time polymorphism.

Mengapa metode overriding disebut pengikatan dinamis


Untuk memanggil metode anyMammal.speak() dan humanMammal.speak() , bytecode adalah sama, karena dari sudut pandang kompiler kedua metode dipanggil untuk kelas Mammal :

 invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V 

Jadi sekarang pertanyaannya adalah, jika kedua panggilan memiliki bytecode yang sama, bagaimana JVM tahu metode mana yang harus dihubungi?

Jawabannya tersembunyi di bytecode itu sendiri dan dalam instruksi invokevirtual . Menurut spesifikasi JVM (catatan penerjemah: referensi ke JVM spec 2.11.8 ) :
Instruksi invokevirtual memanggil metode instance melalui pengiriman jenis objek (virtual). Ini adalah pengiriman metode normal dalam bahasa pemrograman Java.
JVM menggunakan invokevirtual untuk memanggil metode Java yang setara dengan metode virtual C ++. Di C ++, untuk mengganti metode di kelas lain, metode harus dinyatakan sebagai virtual. Tetapi di Java, secara default, semua metode adalah virtual (kecuali untuk metode final dan statis), jadi di kelas anak kita dapat mengganti metode apa pun.

Instruksi invokevirtual mengambil pointer ke metode yang akan dipanggil (# 4 adalah indeks dalam kumpulan konstan).

 invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V 

Tetapi referensi # 4 lebih lanjut merujuk pada metode dan Kelas lain.

 #4 = Methodref #2.#27 // org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V #2 = Class #25 // org/programming/mitra/exercises/OverridingInternalExample$Mammal #25 = Utf8 org/programming/mitra/exercises/OverridingInternalExample$Mammal #27 = NameAndType #35:#17 // speak:()V #35 = Utf8 speak #17 = Utf8 ()V 

Semua tautan ini digunakan bersama untuk mendapatkan referensi ke metode dan kelas di mana metode yang diinginkan berada. Ini juga disebutkan dalam spesifikasi JVM ( catatan penerjemah: referensi ke JVM spec 2.7 ):
Java Virtual Machine tidak memerlukan struktur internal objek tertentu.
Dalam beberapa implementasi Java Virtual Machine oleh Oracle, referensi ke instance kelas adalah referensi ke handler, yang dengan sendirinya terdiri dari sepasang tautan: satu menunjuk ke tabel metode objek dan penunjuk ke objek Kelas yang mewakili jenis objek, dan yang lainnya ke area data pada heap yang berisi data objek.

Ini berarti bahwa setiap variabel referensi berisi dua pointer tersembunyi:

  1. Penunjuk ke tabel yang berisi metode objek dan penunjuk ke objek Class , misalnya, [speak(), speak(String) Class object]
  2. Pointer ke memori pada heap yang dialokasikan untuk data objek, seperti nilai-nilai bidang objek.

Tetapi sekali lagi muncul pertanyaan: bagaimana invokevirtual bekerja dengan ini? Sayangnya, tidak ada yang bisa menjawab pertanyaan ini, karena semuanya tergantung pada implementasi JVM dan bervariasi dari JVM ke JVM.

Dari alasan di atas, kita dapat menyimpulkan bahwa referensi ke objek secara tidak langsung berisi tautan / penunjuk ke tabel yang berisi semua referensi ke metode objek ini. Java meminjam konsep ini dari C ++. Tabel ini dikenal dengan berbagai nama, seperti tabel metode virtual (VMT), tabel fungsi virtual (vftable), tabel virtual (vtable), tabel pengiriman .

Kami tidak dapat memastikan bagaimana vtable diimplementasikan di Java, karena itu tergantung pada JVM tertentu. Tetapi kita bisa berharap bahwa strategi akan hampir sama dengan di C ++, di mana vtable adalah struktur mirip array yang berisi nama metode dan referensi mereka. Setiap kali JVM mencoba mengeksekusi metode virtual, ia meminta alamatnya di vtable.

Untuk setiap kelas, hanya ada satu vtable, yang berarti bahwa tabel tersebut unik dan sama untuk semua objek kelas, mirip dengan objek Kelas. Objek kelas dibahas secara lebih rinci dalam artikel Mengapa kelas Java luar tidak bisa statis dan Mengapa Java adalah Bahasa Berorientasi Objek Murni Atau Mengapa Tidak .

Dengan demikian, hanya ada satu vtable untuk kelas Object , yang berisi semua 11 metode (jika registerNatives tidak diperhitungkan) dan tautan yang sesuai dengan implementasinya.



Ketika JVM memuat kelas Mammal ke dalam memori, ia membuat objek Class untuknya dan membuat vtable yang berisi semua metode dari vtable kelas Object dengan referensi yang sama (karena Mammal tidak menimpa metode dari Object ) dan menambahkan entri baru untuk metode speak() .



Kemudian kelas kelas Human masuk, dan JVM menyalin semua entri dari tabel kelas Mammal ke tabel kelas Human dan menambahkan entri baru untuk versi speak(String) .

JVM tahu bahwa kelas Human telah menimpa dua metode: toString() dari Object dan speak() dari Mammal . Sekarang untuk metode ini, alih-alih membuat catatan baru dengan tautan yang diperbarui, JVM akan mengubah tautan ke metode yang ada dalam indeks yang sama dengan yang sebelumnya disajikan, dan mempertahankan nama metode yang sama.



Instruksi invokevirtual menyebabkan JVM untuk memproses nilai dalam referensi ke metode # 4 bukan sebagai alamat, tetapi sebagai nama metode yang dicari dalam tabel untuk objek saat ini.
Saya harap sekarang lebih jelas bagaimana JVM menggunakan pool konstan dan tabel metode virtual untuk menentukan metode mana yang akan dipanggil.
Anda dapat menemukan kode sampel di repositori Github .

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


All Articles