Programmering

Grundlæggende om Bytecode

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:

TypeDefinition
byteen-byte underskrevet to's komplement heltal
kortto-byte underskrevet to's komplement heltal
int4-byte underskrevet to's komplement heltal
lang8-byte underskrevet to's komplement heltal
flyde4-byte IEEE 754 flydning med en enkelt præcision
dobbelt8-byte IEEE 754 flyde med dobbelt præcision
char2-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, flydeog 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:

OpkodeOperand (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:

OpkodeOperand (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.

OpkodeOperand (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.

OpkodeOperand (er)Beskrivelse
bipushbyte1udvider byte1 (en byte-type) til et int og skubber det på stakken
sipushbyte1, byte2udvider 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:

OpkodeOperand (er)Beskrivelse
ldc1indeksbyte1skubber 32-bit constant_pool-post specificeret af indexbyte1 på stakken
ldc2indexbyte1, indexbyte2skubber 32-bit constant_pool-post angivet af indexbyte1, indexbyte2 på stakken
ldc2windexbyte1, indexbyte2skubber 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:

OpkodeOperand (er)Beskrivelse
iloadvindexskubber 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
flydevindexskubber 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.