Memasak bytecode di dapur JVM

Nama saya Alexander Kotsyuruba, saya memimpin pengembangan layanan internal di DomKlik. Banyak pengembang Java yang berpengalaman memahami struktur internal JVM. Untuk memfasilitasi perjalanan Java Samurai ini, saya memutuskan untuk meletakkan dasar-dasar Java Virtual Machine (JVM) dan bekerja dengan bytecode dalam bahasa yang sederhana.

Apa itu bytecode misterius dan di mana ia tinggal?

Saya akan mencoba menjawab pertanyaan ini menggunakan contoh pengawetan.



Mengapa saya membutuhkan JVM dan bytecode?


JVM berasal dari slogan Write Once Run Anywhere (WORA) di Sun Microsystems. Tidak seperti Write Setelah Kompilasi Anywhere (WOCA) konsep , Wora menyiratkan mesin virtual untuk setiap OS yang mengeksekusi sekali- kode dikompilasi (bytecode).


Write Once Run Anywhere (WORA)


Write Once Compile Anywhere (WOCA)

JVM dan bytecode adalah dasar dari konsep WORA dan menyelamatkan kita dari nuansa dan kebutuhan untuk mengkompilasi untuk setiap OS.

Bytecode


Untuk memahami apa itu bytecode, mari kita lihat sebuah contoh. Tentu saja, kode ini tidak melakukan sesuatu yang bermanfaat, itu hanya akan berfungsi untuk analisis lebih lanjut.

Sumber:

class Solenya(val jarForPickles: Any? = Any(), var ingredientsCount: Int = 0) {


    /**
     *   
     *  @param ingredient -  
     */
    fun add(ingredient: Any) {
        ingredientsCount = ingredientsCount.inc()
        //- 
    }

    /**
     *   
     *  @param duration -   
     */
    fun warmUp(duration: Int) {
        for (x in 1..duration)
            println("Warming")
    }

    init {
        //   
        val jarForPickles = takeJarForPickles()
        // 
        val pickles = Any()
        // 
        val water = Any()

        //
        add(pickles)
        add(water)

        //
        warmUp(10)
    }

    /**
     *   
     */
    private fun takeJarForPickles(): Any = openLocker()

    /**
     *   
     */
    private fun openLocker(): Any = takeKeyForLocker()

    /**
     *     
     */
    private fun takeKeyForLocker(): Any = {}
}

Dengan menggunakan alat Intellij IDEA bawaan ( Alat Bantu -> Kotlin -> Tampilkan Kotlin Bytecode ) kami mendapatkan bytecode yang dibongkar (hanya sebagian yang diperlihatkan dalam contoh):

...
   INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L5
   L6
    LINENUMBER 12 L6
    RETURN
   L7
    LOCALVARIABLE this Lcom/company/Solenya; L0 L7 0
    LOCALVARIABLE ingredient Ljava/lang/Object; L0 L7 1
    LOCALVARIABLE $i$f$add I L1 L7 2
    MAXSTACK = 2
    MAXLOCALS = 5

  // access flags 0x11
  public final warmUp(I)V
    // annotable parameter count: 1 (visible)
    // annotable parameter count: 1 (invisible)
   L0
    LINENUMBER 19 L0
    ICONST_1
    ISTORE 2
...

Sekilas - seperangkat instruksi yang tidak bisa dipahami. Untuk memahami bagaimana dan dengan apa mereka bekerja, Anda perlu masuk ke dapur bagian dalam JVM.

JVM Kitchen


Mari kita lihat memori runtime JVM:



Kita dapat mengatakan bahwa JVM adalah dapur kita. Selanjutnya, pertimbangkan peserta yang tersisa:

Area metode - Cookbook



Area Metode menyimpan kode yang dikompilasi untuk setiap fungsi. Ketika utas mulai melakukan fungsi, secara umum, ia menerima instruksi dari area ini. Bahkan, itu adalah buku resep kuliner yang merinci cara memasak semuanya dari telur orak hingga Catalan zarzuela.

Thread 1..N - Cook Tim



Streaming dengan ketat mengikuti instruksi yang ditentukan oleh mereka (area metode), untuk ini mereka memiliki Daftar PC dan JVM Stack. Anda dapat membandingkan setiap aliran dengan juru masak yang melakukan tugas yang diberikan kepadanya, persis mengikuti resep dari buku masak.

Daftar PC - Catatan Lapangan



Program Counter Register - penghitung perintah dari aliran kami. Ini menyimpan alamat instruksi yang dieksekusi. Di dapur, ini akan menjadi beberapa catatan di halaman buku resep kita sekarang.

Tumpukan JVM


Tumpukan bingkai. Bingkai dialokasikan untuk setiap fungsi, di mana utas saat ini bekerja dengan variabel dan operan. Sebagai bagian dari analogi dengan persiapan acar kami, ini bisa menjadi serangkaian operasi bersarang:

-> -> -> ...

Bingkai - Desktop



Bingkai bertindak sebagai desktop juru masak, di mana terletak papan pemotong dan wadah yang ditandatangani.

Variabel lokal - Wadah yang ditandatangani



Ini adalah array variabel lokal (tabel variabel lokal), yang, sesuai namanya, menyimpan nilai, jenis, dan cakupan variabel lokal. Ini mirip dengan wadah bertanda tangan, di mana Anda dapat menambahkan hasil sementara dari aktivitas profesional.

Operand tumpukan - talenan



Operand stack menyimpan argumen untuk instruksi JVM. Misalnya, nilai integer untuk operasi penjumlahan, referensi ke objek tumpukan, dll

. Contoh terdekat yang dapat saya berikan adalah talenan di mana tomat dan mentimun berubah menjadi salad pada satu titik. Tidak seperti variabel lokal, kami hanya menerapkan apa yang akan kami lakukan dengan instruksi selanjutnya.

Heap - Tabel Distribusi



Sebagai bagian dari bekerja dengan frame, kami beroperasi pada tautan ke objek, objek itu sendiri disimpan dalam tumpukan. Perbedaan penting adalah bahwa bingkai hanya dimiliki oleh satu utas, dan variabel lokal "hidup" saat bingkai masih hidup (fungsi dijalankan). Dan heap dapat diakses oleh aliran lain, dan hidup sampai pengumpul sampah dihidupkan. Dengan analogi dengan dapur, kita bisa memberikan contoh dengan tabel distribusi, yang saja sudah umum. Dan itu dibersihkan oleh tim pembersih yang terpisah.

Dapur JVM. Tampak dari dalam. Bekerja dengan Frame


Mari kita mulai dengan fungsinya warmUp:

    /**
     *   
     *  @param duration -   
     */
    fun warmUp(duration: Int) {
        for (x in 1..duration)
            println("Warming...")
    }

Fungsi bytecode dibongkar:

  public final warmUp(I)V
    // annotable parameter count: 1 (visible)
    // annotable parameter count: 1 (invisible)
   L0
    LINENUMBER 19 L0
    ICONST_1
    ISTORE 2
    ILOAD 1
    ISTORE 3
    ILOAD 2
    ILOAD 3
    IF_ICMPGT L1
   L2
    LINENUMBER 20 L2
    LDC "Warming..."
    ASTORE 4
   L3
    ICONST_0
    ISTORE 5
   L4
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 4
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L5
   L6
    LINENUMBER 19 L6
    ILOAD 2
    ILOAD 3
    IF_ICMPEQ L1
    IINC 2 1
   L7
    GOTO L2
   L1
    LINENUMBER 21 L1
    RETURN
   L8
    LOCALVARIABLE x I L2 L7 2
    LOCALVARIABLE this Lcom/company/Solenya; L0 L8 0
    LOCALVARIABLE duration I L0 L8 1
    MAXSTACK = 2
    MAXLOCALS = 6

Frame Inisialisasi - Persiapan Tempat Kerja


Untuk menjalankan fungsi ini, sebuah frame akan dibuat dalam aliran tumpukan JVM. Biarkan saya mengingatkan Anda bahwa tumpukan terdiri dari array variabel lokal dan tumpukan operan.

  1. Agar kita dapat memahami berapa banyak memori yang dialokasikan untuk frame ini, kompiler menyediakan meta-informasi tentang fungsi ini (penjelasan dalam komentar kode):

        MAXSTACK = 2 //    2*32bit
        MAXLOCALS = 6 //    6*32bit
    
  2. Kami juga memiliki informasi tentang beberapa elemen array variabel lokal:

        LOCALVARIABLE x I L2 L7 2 //  x  Int(I),      L2-L7   2
        LOCALVARIABLE this Lcom/company/Solenya; L0 L8 0
        LOCALVARIABLE duration I L0 L8 1
    
  3. Argumen fungsi ketika menginisialisasi frame jatuh ke dalam variabel lokal. Dalam contoh ini, nilai durasi akan ditulis ke array dengan indeks 1.

Jadi, awalnya bingkai akan terlihat seperti ini:


Mulai jalankan instruksi


Untuk memahami cara kerja frame, cukup lengkapi diri Anda dengan daftar instruksi JVM ( daftar instruksi bytecode Java ) dan lewati labelnya L0:

   L0
    LINENUMBER 19 L0 //     
    ICONST_1
    ISTORE 2
    ILOAD 1
    ISTORE 3
    ILOAD 2
    ILOAD 3
    IF_ICMPGT L1

ICONST_1 - add 1(Int) di tumpukan operan:



iStore 2 - nilai tarik (jenis Int) dari stack operan dan menulis ke variabel lokal dengan indeks 2:



dua operasi ini dapat diartikan di Jawa-kode: int x = 1.

ILOAD 1 - memuat nilai variabel lokal dengan indeks 1 di stack operan:



iStore 3 - nilai tarik (jenis Int) dari stack operan dan menulis ke variabel lokal dengan indeks 3:



dua operasi ini dapat diartikan di Jawa-kode: int var3 = duration.

ILOAD 2 - memuat nilai dari variabel lokal dengan indeks 2 di tumpukan operan.

ILOAD 3 - memuat nilai dari variabel lokal dengan indeks 3 di tumpukan operan:



IF_ICMPGT L1- instruksi untuk membandingkan dua nilai integer dari stack. Jika nilai "lebih rendah" lebih besar dari "atas", buka label L1. Setelah menjalankan instruksi ini, tumpukan akan menjadi kosong.

Inilah tampilan garis bytecode Java ini:

      int x = 1;
      int var3 = duration;
      if (x > var3) {
         ....L1...

Kami mendekompilasi kode menggunakan Intellij IDEA di sepanjang Kotlin -> path Java :

   public final void warmUp(int duration) {
      int x = 1;
      int var3 = duration;
      if (x <= duration) {
         while(true) {
            String var4 = "Warming";
            boolean var5 = false;
            System.out.println(var4);
            if (x == var3) {
               break;
            }
            ++x;
         }
      }
   }

Di sini Anda dapat melihat variabel yang tidak digunakan ( var5) dan tidak adanya panggilan fungsi println(). Jangan khawatir, ini karena spesifikasi fungsi inline kompilasi ( println()) dan ekspresi lambda. Praktis tidak ada overhead untuk pelaksanaan instruksi ini, apalagi, kode mati akan dihapus berkat JIT. Ini adalah topik yang menarik, yang harus dikhususkan untuk artikel terpisah.

Menggambar analogi dengan dapur, fungsi ini dapat digambarkan sebagai tugas juru masak untuk "merebus air selama 10 menit". Selanjutnya, profesional kami di bidangnya:

  1. membuka buku resep (area metode);
  2. menemukan instruksi tentang cara merebus air ( warmUp());
  3. menyiapkan tempat kerja, mengalokasikan hot plate (operan stack) dan wadah (variabel lokal) untuk penyimpanan sementara produk.

Dapur JVM. Tampak dari dalam. Bekerja dengan Heap


Pertimbangkan kodenya:

val pickles = Any()

Bytecode dibongkar:

    NEW java/lang/Object
    DUP
    INVOKESPECIAL java/lang/Object.<init> ()V
    ASTORE 3

Java / lang / Object BARU - alokasi memori untuk objek kelas Objectdari heap. Objek itu sendiri tidak akan ditempatkan di tumpukan, tetapi tautan ke sana di tumpukan:


DUP - duplikasi elemen "atas" dari tumpukan. Satu tautan diperlukan untuk menginisialisasi objek, yang kedua untuk menyimpannya dalam variabel lokal:


INVOKESPECIAL java / lang / Object. <init> () V - inisialisasi objek dari kelas yang sesuai ( Object) dengan tautan dari stack:


ASTORE 3 adalah langkah terakhir, menyimpan referensi ke objek dalam variabel lokal dengan indeks 3.

Menggambar analogi dengan dapur, saya akan membandingkan penciptaan objek kelas dengan memasak di atas meja bersama (heap). Untuk melakukan ini, Anda perlu mengalokasikan ruang yang cukup untuk diri Anda sendiri pada tabel distribusi, kembali ke tempat kerja dan melemparkan catatan dengan alamat (referensi) dalam wadah yang sesuai (variabel lokal). Dan hanya setelah itu mulai membuat objek kelas.

Dapur JVM. Tampak dari dalam. Multithreading


Sekarang perhatikan contoh ini:

    fun add(ingredient: Any) {
        ingredientsCount = ingredientsCount.inc()
        //- 
    }

Ini adalah contoh klasik dari masalah threading. Kami memiliki jumlah bahan ingredientsCount. Fungsi add, selain menambahkan bahan, melakukan penambahan ingredientsCount.

Bytecode yang dibongkar terlihat seperti ini:

    ALOAD 0
    ALOAD 0
    GETFIELD com/company/Solenya.ingredientsCount : I
    ICONST_1
    IADD
    PUTFIELD com/company/Solenya.ingredientsCount : I

Status tumpukan operan kami sebagai instruksi yang dijalankan:


Saat bekerja dalam satu utas, semuanya akan dieksekusi dengan benar. Jika ada beberapa utas, maka masalah berikut mungkin terjadi. Bayangkan bahwa kedua utas secara bersamaan mendapatkan nilai bidang ingredientsCountdan menulisnya ke tumpukan. Maka keadaan tumpukan operan dan bidang ingredientsCountmungkin terlihat seperti ini:


Fungsi dieksekusi dua kali (sekali oleh setiap utas) dan nilainya ingredientsCountharus sama dengan 2. Namun pada kenyataannya, salah satu utas bekerja dengan nilai yang kedaluwarsa ingredientsCount, dan oleh karena itu hasil aktualnya adalah 1 (masalah Pembaruan Hilang).

Situasinya mirip dengan pekerjaan paralel dari tim koki yang menambahkan rempah-rempah ke hidangan. Membayangkan:

  1. Ada tabel distribusi tempat piring (Heap) berada.
  2. Ada dua koki di dapur (Thread * 2).
  3. Setiap juru masak memiliki meja potong sendiri, di mana mereka menyiapkan campuran rempah-rempah (JVM Stack * 2).
  4. Tugas: tambahkan dua porsi bumbu ke piring.
  5. Di atas meja distribusi terdapat selembar kertas yang mereka baca dan di mana mereka menulis bagian mana yang ditambahkan ( ingredientsCount). Dan untuk menyimpan rempah-rempah:
    • Sebelum memulai persiapan rempah-rempah, juru masak harus membaca di selembar kertas bahwa jumlah rempah yang ditambahkan tidak cukup;
    • setelah menambahkan rempah-rempah, juru masak dapat menulis berapa banyak, menurut pendapatnya, rempah-rempah ditambahkan ke piring.

Dalam kondisi seperti itu, sebuah situasi dapat muncul:

  1. Masak # 1 membaca bahwa 3 porsi bumbu ditambahkan.
  2. Masak # 2 membaca bahwa 3 porsi bumbu ditambahkan.
  3. Keduanya pergi ke meja masing-masing dan menyiapkan campuran rempah-rempah.
  4. Kedua koki menambahkan rempah-rempah (3 + 2) ke piring.
  5. Cook # 1 menulis bahwa 4 porsi bumbu telah ditambahkan.
  6. Cook # 2 menulis bahwa 4 porsi bumbu telah ditambahkan.

Intinya: produknya hilang, hidangannya pedas, dll.

Untuk menghindari situasi seperti itu, ada berbagai alat seperti kunci, fungsi pengaman benang, dll.

Untuk meringkas


Sangat jarang bagi pengembang untuk perlu merangkak ke bytecode, kecuali ini khusus untuk pekerjaannya. Pada saat yang sama, memahami karya bytecode membantu untuk lebih memahami multithreading dan keuntungan dari bahasa tertentu, dan juga membantu untuk tumbuh secara profesional.

Perlu dicatat bahwa ini jauh dari semua bagian JVM. Ada banyak "hal" yang lebih menarik, misalnya, kumpulan konstan, bytecode verifier, JIT, cache kode, dll. Tetapi agar tidak membebani artikel itu, saya hanya fokus pada elemen-elemen yang diperlukan untuk pemahaman bersama.

Tautan yang bermanfaat:


All Articles