Velkommen til endnu en del af "Under The Hood." Denne kolonne giver Java-udviklere et glimt af, hvad der foregår under deres kørende Java-programmer. Denne måneds artikel tager et indledende kig på bytecode-instruktionssættet på den virtuelle Java-maskine (JVM). Artiklen dækker primitive typer, der betjenes af bytekoder, bytekoder, der konverterer mellem typer, og bytekoder, der fungerer på stakken. Efterfølgende artikler vil diskutere andre medlemmer af bytecode-familien.
Bytecode-formatet
Bytecodes er maskinsproget på den virtuelle Java-maskine. Når en JVM indlæser en klassefil, får den en strøm af bytekoder for hver metode i klassen. Bytekodestrømmene er gemt i JVM's metodeområde. Bykoderne for en metode udføres, når denne metode påberåbes i løbet af programmet. De kan udføres ved fortolkning, just-in-time kompilering eller enhver anden teknik, der blev valgt af designeren af en bestemt JVM.
En metodes bytecode-strøm er en sekvens af instruktioner til den virtuelle Java-maskine. Hver instruktion består af en byte opkode efterfulgt af nul eller mere operander. Opkoden angiver den handling, der skal udføres. Hvis der kræves flere oplysninger, før JVM kan tage handlingen, kodes disse oplysninger i en eller flere operander, der straks følger opkoden.
Hver type opkode har en memnemonic. I den typiske forsamlingssprogstil kan strømme af Java-bytecodes repræsenteres af deres mnemonics efterfulgt af eventuelle operandværdier. For eksempel kan følgende strøm af bytekoder skilles ad til mindesmærker:
// Bytecode stream: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // Adskillelse: iconst_0 // 03 istore_0 // 3b iinc 0, 1 // 84 00 01 iload_0 // 1a iconst_2 // 05 imul // 68 istore_0 // 3b goto -7 // a7 ff f9
Bytecode-instruktionssættet var designet til at være kompakt. Alle instruktioner undtagen to, der beskæftiger sig med springning af bord, er justeret efter bytegrænser. Det samlede antal opkoder er lille nok til, at opkoder kun optager en byte. Dette hjælper med at minimere størrelsen på klassefiler, der muligvis kører på tværs af netværk, før de indlæses af en JVM. Det hjælper også med at holde størrelsen på JVM-implementeringen lille.
Al beregning i JVM er centreret på stakken. Da JVM ikke har nogen registre til lagring af abitrære værdier, skal alt skubbes på stakken, før det kan bruges i en beregning. Bytecode-instruktioner fungerer derfor primært på stakken. For eksempel multipliceres en lokal variabel i ovenstående bytekodesekvens med to ved først at skubbe den lokale variabel på stakken med iload_0
instruktion og derefter skubbe to på stakken med iconst_2
. Efter at begge heltal er skubbet på stakken, vises imul
instruktion popper effektivt de to heltal fra stakken, ganger dem og skubber resultatet tilbage på stakken. Resultatet poppes fra toppen af stakken og gemmes tilbage til den lokale variabel af istore_0
instruktion. JVM blev designet som en stakbaseret maskine snarere end en registerbaseret maskine for at lette effektiv implementering på registerfattige arkitekturer såsom Intel 486.
Primitive typer
JVM understøtter syv primitive datatyper. Java-programmører kan erklære og bruge variabler af disse datatyper, og Java-bytecodes fungerer på disse datatyper. De syv primitive typer er anført i følgende tabel:
Type | Definition |
---|---|
byte | en-byte underskrevet to's komplement heltal |
kort | to-byte underskrevet to's komplement heltal |
int | 4-byte underskrevet to's komplement heltal |
lang | 8-byte underskrevet to's komplement heltal |
flyde | 4-byte IEEE 754 flydning med en enkelt præcision |
dobbelt | 8-byte IEEE 754 flyde med dobbelt præcision |
char | 2-byte usigneret Unicode-tegn |
De primitive typer vises som operander i bytecode-strømme. Alle primitive typer, der optager mere end 1 byte, lagres i stor-endian-rækkefølge i bytecode-strømmen, hvilket betyder, at højere ordensbyte går forud for lavere ordensbyte. For eksempel for at skubbe den konstante værdi 256 (hex 0100) på stakken, skal du bruge sipush
opcode efterfulgt af en kort operand. Kort vises i bytecode-strømmen, vist nedenfor, som "01 00", fordi JVM er big-endian. Hvis JVM var lille endian, ville den korte vises som "00 01".
// Bytecode stream: 17 01 00 // Adskillelse: sipush 256; // 17 01 00
Java-opkoder angiver generelt typen af deres operander. Dette gør det muligt for operander at være sig selv uden behov for at identificere deres type til JVM. For eksempel i stedet for at have en opcode, der skubber en lokal variabel på stakken, har JVM flere. Opkoder iload
, lload
, flyde
og dload
skub lokale variabler af henholdsvis typen int, long, float og double på stakken.
Skubbe konstanter på stakken
Mange opkoder skubber konstanter på stakken. Opkoder angiver den konstante værdi, der skal skubbes på tre forskellige måder. Den konstante værdi er enten implicit i selve opkoden, følger opkoden i bytekodestrømmen som en operand eller tages fra den konstante pool.
Nogle opkoder i sig selv angiver en type og en konstant værdi, der skal skubbes. F.eks iconst_1
opcode fortæller JVM at skubbe heltal værdi en. Sådanne bytekoder er defineret for nogle ofte skubbet tal af forskellige typer. Disse instruktioner optager kun 1 byte i bytekodestrømmen. De øger effektiviteten af udførelse af bytecode og reducerer størrelsen på bytecode-streams. Opkoderne, der skubber ints og floats, vises i følgende tabel:
Opkode | Operand (er) | Beskrivelse |
---|---|---|
ikonst_m1 | (ingen) | skubber int -1 på stakken |
ikonst_0 | (ingen) | skubber int 0 på stakken |
iconst_1 | (ingen) | skubber int 1 på stakken |
iconst_2 | (ingen) | skubber int 2 på stakken |
ikonst_3 | (ingen) | skubber int 3 på stakken |
ikonst_4 | (ingen) | skubber int 4 på stakken |
ikonst_5 | (ingen) | skubber int 5 på stakken |
fconst_0 | (ingen) | skubber float 0 på stakken |
fconst_1 | (ingen) | skubber svømmer 1 på stakken |
fconst_2 | (ingen) | skubber float 2 på stakken |
Opkoderne vist i den foregående tabel skubber ints og floats, som er 32-bit værdier. Hvert slot på Java-stakken er 32 bit bredt. Derfor hver gang en int eller float skubbes på stakken, optager den en slot.
Opkoderne vist i næste tabel skubber længsel og dobbelt. Lange og dobbelte værdier optager 64 bit. Hver gang en lang eller dobbelt skubbes på stakken, indtager dens værdi to slots på stakken. Opkoder, der angiver en bestemt lang eller dobbelt værdi, der skal skubbes, vises i følgende tabel:
Opkode | Operand (er) | Beskrivelse |
---|---|---|
lconst_0 | (ingen) | skubber langt 0 på stakken |
lconst_1 | (ingen) | skubber langt 1 på stakken |
dconst_0 | (ingen) | skubber dobbelt 0 på stakken |
dconst_1 | (ingen) | skubber dobbelt 1 på stakken |
En anden opcode skubber en implicit konstant værdi på stakken. Det aconst_null
opcode, vist i den følgende tabel, skubber en null-objektreference på stakken. Formatet på en objektreference afhænger af JVM-implementeringen. En objektreference vil på en eller anden måde henvise til et Java-objekt på bunken, der er indsamlet skrald. En null-objektreference angiver, at en objektreferencevariabel i øjeblikket ikke henviser til noget gyldigt objekt. Det aconst_null
opcode bruges i processen med at tildele null til en objektreferencevariabel.
Opkode | Operand (er) | Beskrivelse |
---|---|---|
aconst_null | (ingen) | skubber en nul objekthenvisning på stakken |
To opkoder angiver konstanten til at skubbe med en operand, der straks følger opkoden. Disse opkoder, vist i den følgende tabel, bruges til at skubbe heltalskonstanter, der er inden for det gyldige område for byte- eller korte typer. Byte eller kort, der følger opkoden, udvides til et int, før det skubbes på stakken, fordi hvert slot på Java-stakken er 32 bit bredt. Operationer på bytes og shorts, der er skubbet på stakken, udføres faktisk på deres int-ækvivalenter.
Opkode | Operand (er) | Beskrivelse |
---|---|---|
bipush | byte1 | udvider byte1 (en byte-type) til et int og skubber det på stakken |
sipush | byte1, byte2 | udvider byte1, byte2 (en kort type) til et int og skubber det på stakken |
Tre opkoder skubber konstanter fra den konstante pool. Alle konstanter, der er knyttet til en klasse, såsom værdier for endelige variabler, gemmes i klassens konstante pool. Opkoder, der skubber konstanter fra den konstante pool, har operander, der angiver, hvilken konstant der skal skubbes ved at angive et konstant poolindeks. Den virtuelle Java-maskine vil slå op på konstanten givet indekset, bestemme konstantens type og skubbe den på stakken.
Det konstante poolindeks er en usigneret værdi, der straks følger opkoden i bytekodestrømmen. Opkoder lcd1
og lcd2
skub et 32-bit element på stakken, såsom en int eller float. Forskellen på lcd1
og lcd2
er det lcd1
kan kun henvise til konstante puljeplaceringer en til 255, fordi dens indeks kun er 1 byte. (Konstant poolplacering nul er ubrugt.) lcd2
har et 2-byte-indeks, så det kan henvise til enhver konstant poolplacering. lcd2w
har også et 2-byte-indeks, og det bruges til at henvise til enhver konstant poolplacering indeholdende en lang eller dobbelt, som optager 64 bits. Opkoderne, der skubber konstanter fra den konstante pool, vises i følgende tabel:
Opkode | Operand (er) | Beskrivelse |
---|---|---|
ldc1 | indeksbyte1 | skubber 32-bit constant_pool-post specificeret af indexbyte1 på stakken |
ldc2 | indexbyte1, indexbyte2 | skubber 32-bit constant_pool-post angivet af indexbyte1, indexbyte2 på stakken |
ldc2w | indexbyte1, indexbyte2 | skubber 64-bit constant_pool-post angivet af indexbyte1, indexbyte2 på stakken |
Skubber lokale variabler på stakken
Lokale variabler gemmes i et specielt afsnit af stakrammen. Stakrammen er den del af stakken, der bruges ved den nuværende udførelsesmetode. Hver stakramme består af tre sektioner - de lokale variabler, udførelsesmiljøet og operandstakken. At skubbe en lokal variabel på stakken indebærer faktisk at flytte en værdi fra sektionen lokale variabler i stabelrammen til operandafsnittet. Operandafsnittet i den nuværende udførelsesmetode er altid toppen af stakken, så skubbe en værdi på operandafsnittet i den aktuelle stabelramme er det samme som at skubbe en værdi på toppen af stakken.
Java-stakken er en sidste ind-først-ud-stak med 32-bit-slots. Fordi hver slot i stakken optager 32 bit, optager alle lokale variabler mindst 32 bits. Lokale variabler af typen lang og dobbelt, som er 64-bit mængder, optager to slots på stakken. Lokale variabler af typen byte eller kort gemmes som lokale variabler af typen int, men med en værdi, der er gyldig for den mindre type. For eksempel vil en int-lokal variabel, der repræsenterer en byte-type, altid indeholde en værdi, der er gyldig for en byte (-128 <= værdi <= 127).
Hver lokale variabel i en metode har et unikt indeks. Den lokale variable sektion af en metodes stabelramme kan betragtes som en matrix med 32-bit slots, der hver adresseres af array-indekset. Lokale variabler af typen lang eller dobbelt, der optager to slots, henvises til ved det nederste af de to slotindekser. For eksempel vil en dobbelt, der indtager plads to og tre, blive henvist til med et indeks på to.
Der findes flere opkoder, der skubber int og flyder lokale variabler på operandstakken. Nogle opkoder er defineret, der implicit henviser til en almindeligt anvendt lokal variabel position. For eksempel, iload_0
indlæser den int lokale variabel i position nul. Andre lokale variabler skubbes på stakken med en opcode, der tager det lokale variabelindeks fra den første byte efter opcode. Det iload
instruktion er et eksempel på denne type opcode. Den første byte, der følger iload
fortolkes som et usigneret 8-bit indeks, der refererer til en lokal variabel.
Usignerede 8-bit lokale variable indekser, såsom den der følger iload
instruktion, begræns antallet af lokale variabler i en metode til 256. En separat instruktion, kaldet bred
, kan udvide et 8-bit indeks med yderligere 8 bit. Dette hæver den lokale variable grænse til 64 kilobyte. Det bred
opcode efterfølges af en 8-bit operand. Det bred
opcode og dets operand kan gå forud for en instruktion, såsom iload
, der tager et 8-bit usigneret lokalt variabelt indeks. JVM kombinerer 8-bit operand af bred
instruktion med 8-bit operand af iload
instruktion om at give et 16-bit usigneret lokalt variabelt indeks.
Opkoderne, der skubber int og flyder lokale variabler på stakken, vises i følgende tabel:
Opkode | Operand (er) | Beskrivelse |
---|---|---|
iload | vindex | skubber int fra lokal variabel position vindex |
iload_0 | (ingen) | skubber int fra lokal variabel position nul |
iload_1 | (ingen) | skubber int fra lokal variabel position en |
iload_2 | (ingen) | skubber int fra lokal variabel position to |
iload_3 | (ingen) | skubber int fra lokal variabel position tre |
flyde | vindex | skubber float fra lokal variabel position vindex |
fload_0 | (ingen) | skubber float fra lokal variabel position nul |
fload_1 | (ingen) | skubber float fra lokal variabel position et |
fload_2 | (ingen) | skubber flyde fra lokal variabel position to |
fload_3 | (ingen) | skubber flyde fra lokal variabel position tre |
Den næste tabel viser instruktionerne, der skubber lokale variabler af typen lang og dobbelt på stakken. Disse instruktioner flytter 64 bit fra den lokale variable sektion af stakrammen til operandafsnittet.