Ohjelmointi

Bytecode-perusteet

Tervetuloa toiseen "Under The Hood" -erään. Tämä sarake antaa Java-kehittäjille oivalluksen siitä, mitä heidän käynnissä olevien Java-ohjelmiensa alla tapahtuu. Tämän kuukauden artikkelissa tarkastellaan ensin Java-virtuaalikoneen (JVM) tavukoodikäskyjoukkoa. Artikkeli kattaa primitiiviset tyypit, joita ohjaavat tavukoodit, tavujen väliset muuntavat tavut ja pinossa toimivat tavukoodit. Myöhemmissä artikkeleissa keskustellaan muista tavukoodiperheen jäsenistä.

Tavukoodimuoto

Bytecodes ovat Java-virtuaalikoneen konekieli. Kun JVM lataa luokkatiedoston, se saa yhden tavuekoodivirran kutakin luokan menetelmää varten. Tavukoodivirrat tallennetaan JVM: n menetelmäalueelle. Menetelmän tavukoodit suoritetaan, kun kyseinen menetelmä käynnistetään ohjelman suorittamisen aikana. Ne voidaan suorittaa intepretoimalla, juuri oikeaan aikaan -kokoelmalla tai millä tahansa muulla tekniikalla, jonka tietyn JVM: n suunnittelija on valinnut.

Menetelmän tavukoodivirta on Java-virtuaalikoneen ohjeistussarja. Jokainen käsky koostuu yksitavuisesta opkoodi jota seuraa nolla tai enemmän operandit. Opkoodi ilmaisee suoritettavan toiminnan. Jos tarvitaan lisätietoja ennen kuin JVM voi ryhtyä toimenpiteeseen, nämä tiedot koodataan yhteen tai useampaan operandiin, jotka seuraavat välittömästi opkoodia.

Jokaisella opkoodityypillä on muistitikku. Tyypillisessä kokoonpanokielityylissä Java-tavukoodien virrat voidaan esittää niiden muistitiedoilla ja mitä tahansa operandiarvoja. Esimerkiksi seuraava tavukoodivirta voidaan purkaa muistiin:

// Bytecode-stream: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // Purkaminen: 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 

Tavukoodikäskyjoukko on suunniteltu kompaktiksi. Kaikki ohjeet, lukuun ottamatta kahta, jotka käsittelevät taulukon hyppäämistä, on kohdistettu tavujen rajoille. Opkoodien kokonaismäärä on riittävän pieni, jotta opkoodit vievät vain yhden tavun. Tämä auttaa minimoimaan luokkatiedostojen koon, jotka saattavat kulkea verkon yli ennen JVM: n lataamista. Se auttaa myös pitämään JVM-toteutuksen koon pienenä.

Kaikki JVM: n laskenta keskittyy pinoon. Koska JVM: llä ei ole rekistereitä virheellisten arvojen tallentamiseen, kaikki on työnnettävä pinolle, ennen kuin sitä voidaan käyttää laskelmissa. Bytecode-ohjeet toimivat siis ensisijaisesti pinossa. Esimerkiksi yllä olevassa tavukoodisekvenssissä paikallinen muuttuja kerrotaan kahdella työntämällä ensin paikallinen muuttuja pinoon iload_0 ja työnnä sitten kaksi pinoon iconst_2. Kun molemmat kokonaisluvut on työnnetty pinoon, imul käsky tuo tehokkaasti kaksi kokonaislukua pois pinosta, kertoo ne ja työntää tuloksen takaisin pinoon. Tulos ponnahtaa pinon yläosasta ja tallennetaan takaisin paikalliseen muuttujaan istore_0 ohje. JVM suunniteltiin pinopohjaiseksi koneeksi eikä rekisteripohjaiseksi koneeksi helpottamaan tehokasta käyttöönottoa rekisterirukoisissa arkkitehtuureissa, kuten Intel 486.

Primitiiviset tyypit

JVM tukee seitsemää primitiivistä tietotyyppiä. Java-ohjelmoijat voivat ilmoittaa ja käyttää näiden tietotyyppien muuttujia, ja Java-tavukoodit toimivat näillä tietotyypeillä. Seitsemän primitiivistä tyyppiä on lueteltu seuraavassa taulukossa:

TyyppiMääritelmä
tavuyksi tavu allekirjoitti kahden komplementin kokonaisluvun
lyhytkaksitavuinen allekirjoitti kahden komplementin kokonaisluvun
int4-tavuinen allekirjoitti kahden komplementin kokonaisluvun
pitkä8-tavuinen allekirjoitti kahden komplementin kokonaisluvun
kellua4-tavuinen IEEE 754 -tarkka uimuri
kaksinkertainen8-tavuinen IEEE 754-kaksoistarkka uimuri
hiiltyä2-tavuinen allekirjoittamaton Unicode-merkki

Primitiiviset tyypit esiintyvät operandeina tavukoodivirroissa. Kaikki primitiiviset tyypit, jotka vievät enemmän kuin yhden tavun, tallennetaan big-endian-järjestyksessä tavukoodivirtaan, mikä tarkoittaa, että korkeamman asteen tavut edeltävät alemman asteen tavuja. Esimerkiksi, jos haluat työntää vakioarvon 256 (hex 0100) pinoon, käytä sipush opkoodi, jota seuraa lyhyt operandi. Lyhyt näkyy tavukoodivirrassa, joka on esitetty alla, nimellä "01 00", koska JVM on big-endian. Jos JVM olisi vähän endiaa, lyhyt näyttäisi nimellä "00 01".

 // Bytecode-virta: 17 01 00 // Purkaminen: imuputki 256; // 17 01 00 

Java-opkoodit osoittavat yleensä niiden operandien tyypin. Tämä antaa operandien olla vain itseään, eikä sinun tarvitse tunnistaa tyyppiään JVM: ään. Esimerkiksi sen sijaan, että sillä olisi yksi opkoodi, joka työntää paikallisen muuttujan pinoon, JVM: llä on useita. Opcodes iload, kuormitus, kuormitusja ladata työnnä paikalliset muuttujat, tyypin int, long, float ja double, vastaavasti pinolle.

Vakioiden työntäminen pinoon

Monet opkoodit työntävät vakioita pinoon. Opkoodit osoittavat työnnettävän vakion arvon kolmella eri tavalla. Vakioarvo on joko implisiittinen itse opkoodissa, seuraa opkoodia tavukoodivirrassa operandina tai otetaan vakiopoolista.

Jotkut opkoodit itsessään ilmaisevat työnnettävän tyypin ja vakion arvon. Esimerkiksi iconst_1 opcode käskee JVM: n työntämään kokonaislukuarvo yksi. Tällaiset tavukoodit on määritelty joillekin yleisesti syötetyille erityyppisille numeroille. Nämä ohjeet vievät vain yhden tavun tavukoodivirrassa. Ne lisäävät tavukoodin suorituksen tehokkuutta ja pienentävät tavukoodivirtojen kokoa. Inkteja ja kellukkeita työntävät opcodit näkyvät seuraavassa taulukossa:

OpcodeOperandi (t)Kuvaus
iconst_m1(ei mitään)työntää int -1 pinoon
iconst_0(ei mitään)työntää int 0 pinoon
iconst_1(ei mitään)työntää int 1 pinoon
iconst_2(ei mitään)työntää int 2 pinolle
iconst_3(ei mitään)työntää int 3 pinoon
iconst_4(ei mitään)työntää int 4 pinoon
iconst_5(ei mitään)työntää int 5 pinoon
fconst_0(ei mitään)työntää kellukkeen 0 pinoon
fconst_1(ei mitään)työntää kellukkeen 1 pinoon
fconst_2(ei mitään)työntää kellukkeen 2 pinoon

Edellisessä taulukossa esitetyt opkoodit työntävät sisään ja kelluvat, jotka ovat 32-bittisiä arvoja. Jokainen Java-pinon paikka on 32 bittiä leveä. Siksi joka kerta kun int tai uimuri työnnetään pinolle, se vie yhden uran.

Seuraavassa taulukossa esitetyt opcodit työntävät pitkiä ja kaksinkertaistuvat. Pitkät ja kaksoisarvot vievät 64 bittiä. Joka kerta, kun pitkä tai kaksinkertainen työnnetään pinolle, sen arvo vie kaksi pinoa. Opkoodit, jotka osoittavat tietyn pitkän tai kaksinkertaisen työnnettävän arvon, näkyvät seuraavassa taulukossa:

OpcodeOperandi (t)Kuvaus
lconst_0(ei mitään)työntää pitkän 0 pinoon
lconst_1(ei mitään)työntää pitkän 1 pinoon
dconst_0(ei mitään)työntää kaksinkertaisen 0 pinoon
dconst_1(ei mitään)työntää kaksinkertaisen 1 pinoon

Yksi toinen opkoodi työntää implisiittisen vakion arvon pinoon. aconst_null seuraavassa taulukossa esitetty opkoodi työntää nollan objektiviitteen pinoon. Objektiviitteen muoto riippuu JVM-toteutuksesta. Objektiviittaus viittaa jotenkin Java-objektiin roskakorissa. Nolla objektiviite osoittaa, että objektiviittausmuuttuja ei tällä hetkellä viittaa mihinkään kelvolliseen objektiin. aconst_null opkoodia käytetään nollan määrittämisessä objektiviittausmuuttujalle.

OpcodeOperandi (t)Kuvaus
aconst_null(ei mitään)työntää tyhjän objektiviitteen pinoon

Kaksi opkoodia ilmaisee vakion työntää operandilla, joka seuraa välittömästi opkoodia. Näitä seuraavassa taulukossa esitettyjä opcodeja käytetään työntämään kokonaisluvuvakioita, jotka ovat kelvollisten tavujen tai lyhyiden tyyppien alueella. Opkoodia seuraava tavu tai lyhyt laajennetaan int-muotoon ennen kuin se työnnetään pinoon, koska jokainen Java-pinon paikka on 32 bittiä leveä. Pinoihin työnnetyt tavut ja shortsit suoritetaan itse asiassa niiden ekvivalenteilla.

OpcodeOperandi (t)Kuvaus
kaksisuuntainentavu 1laajentaa tavu1 (tavutyyppi) int-muotoon ja työntää sen pinoon
sipushtavu1, tavu2laajentaa tavu1, tavu2 (lyhyt tyyppi) int-muotoon ja työntää sen pinoon

Kolme opkoodia työntää vakioita vakiopoolista. Kaikki luokkaan liittyvät vakiot, kuten lopullisten muuttujien arvot, tallennetaan luokan vakiopooliin. Opkodeilla, jotka työntävät vakioita vakiopoolista, on operandit, jotka osoittavat vakion, jota painetaan määrittelemällä vakiopoolihakemisto. Java-virtuaalikone etsii indeksille annetun vakion, määrittää vakion tyypin ja työntää sen pinoon.

Vakiopoolihakemisto on allekirjoittamaton arvo, joka seuraa välittömästi opkoodia tavukoodivirrassa. Opcodes lcd1 ja lcd2 työnnä 32-bittinen esine pinolle, kuten int tai float. Ero lcd1 ja lcd2 onko tuo lcd1 voi viitata vain vakioalueiden paikkoihin yhdestä 255 asti, koska sen indeksi on vain 1 tavu. (Altaan jatkuvan sijainnin nolla on käyttämätön.) lcd2 on 2-tavuinen hakemisto, joten se voi viitata mihin tahansa vakioalueen sijaintiin. lcd2w Siinä on myös 2-tavuinen indeksi, ja sitä käytetään viittaamaan mihin tahansa vakiopoolin sijaintiin, joka sisältää pitkän tai kaksinkertaisen, joka vie 64 bittiä. Vakioita vakiopoolista työntävät opkoodit näkyvät seuraavassa taulukossa:

OpcodeOperandi (t)Kuvaus
ldc1indeksitavu 1työntää indexbyte1: n määrittelemän 32-bittisen vakio_poolin merkinnän pinoon
ldc2indexbyte1, indexbyte2työntää indexbyte1: n, indexbyte2: n määrittelemän 32-bittisen vakio_poolin merkinnän pinoon
ldc2windexbyte1, indexbyte2työntää indexbyte1: n, indexbyte2: n määrittelemän 64-bittisen vakio_poolin merkinnän pinoon

Paikallisten muuttujien työntäminen pinoon

Paikalliset muuttujat tallennetaan pinokehyksen erityiseen osaan. Pino-kehys on osa pinosta, jota parhaillaan suoritettava menetelmä käyttää. Jokainen pinokehys koostuu kolmesta osasta - paikalliset muuttujat, suoritusympäristö ja operandipino. Paikallisen muuttujan työntäminen pinoon tarkoittaa itse asiassa arvon siirtämistä pinokehyksen paikallisten muuttujien osasta operandiosaan. Parhaillaan suoritettavan menetelmän operandiosio on aina pinon yläosa, joten arvon työntäminen nykyisen pinokehyksen operandiosaan on sama kuin arvon työntäminen pinon yläosaan.

Java-pino on viimeinen, ensimmäinen-out-pino 32-bittisiä paikkoja. Koska jokainen pinon aukko vie 32 bittiä, kaikki paikalliset muuttujat vievät vähintään 32 bittiä. Pitkät ja kaksinkertaiset paikalliset muuttujat, jotka ovat 64-bittisiä määriä, vievät kaksi paikkaa pinossa. Tyyppitavuisten tai lyhyiden paikallismuuttujat tallennetaan tyypin int paikallisina muuttujina, mutta arvolla, joka on kelvollinen pienemmälle tyypille. Esimerkiksi tavutyyppiä edustava int-paikallinen muuttuja sisältää aina tavulle kelvollisen arvon (-128 <= arvo <= 127).

Jokaisella menetelmän paikallisella muuttujalla on ainutlaatuinen indeksi. Menetelmän pinokehyksen paikallinen muuttujaosa voidaan ajatella 32-bittisten aikavälien ryhmänä, joista kukin on osoitettavissa taulukkoindeksillä. Paikallisiin muuttujiin, joiden tyyppi on pitkä tai kaksinkertainen ja jotka vievät kaksi aikaväliä, viitataan alemmasta kahdesta aikavälihakemistosta. Esimerkiksi kaksoisosaan, joka vie aikavälit kaksi ja kolme, viitataan kahden indeksillä.

On olemassa useita opkoodeja, jotka työntävät int ja kelluvat paikalliset muuttujat operandipinoon. Jotkut opkoodit on määritelty, jotka implisiittisesti viittaavat yleisesti käytettyyn paikalliseen muuttujan sijaintiin. Esimerkiksi, iload_0 lataa int-paikallismuuttujan paikalle nolla. Muut paikalliset muuttujat työnnetään pinoon opkoodilla, joka ottaa paikallisen muuttujan indeksin ensimmäisestä tavusta, joka seuraa opkoodia. iload käsky on esimerkki tämän tyyppisestä opkoodista. Ensimmäinen tavu seuraa iload tulkitaan allekirjoittamattomana 8-bittisenä indeksinä, joka viittaa paikalliseen muuttujaan.

Allekirjoittamattomat 8-bittiset paikalliset muuttujahakemistot, kuten iload rajoita menetelmän paikallisten muuttujien lukumäärä 256. Erillinen käsky, nimeltään leveä, voi pidentää 8-bittistä hakemistoa vielä 8 bitillä. Tämä nostaa paikallisen muuttujan rajan 64 kilotavuun. leveä opkoodia seuraa 8-bittinen operandi. leveä opkoodi ja sen operandi voivat edeltää käskyä, kuten iload, joka vie 8-bittisen allekirjoittamattoman paikallisen muuttujahakemiston. JVM yhdistää 8-bittisen operandin leveä ohje 8-bittisellä operandilla iload käsky tuottaa 16-bittinen allekirjoittamaton paikallinen muuttujahakemisto.

Opkoodit, jotka työntävät int ja kelluvat paikalliset muuttujat pinoon, näkyvät seuraavassa taulukossa:

OpcodeOperandi (t)Kuvaus
iloadvindextyöntää int paikallisesta muuttuvasta asemasta vindex
iload_0(ei mitään)työntää int paikallisesta muuttujasta nolla
iload_1(ei mitään)työntää int paikallisesta muuttuvasta asemasta yksi
iload_2(ei mitään)työntää int paikallisesta muuttuvasta asemasta kaksi
iload_3(ei mitään)työntää int paikallisesta muuttujasta kolme
kuormitusvindextyöntää kellukkeen paikallisesta muuttuvasta sijainnista vindex
fload_0(ei mitään)työntää kellukkeen paikallisesta muuttuvasta asennosta nolla
fload_1(ei mitään)työntää kellukkeen paikallisesta muuttuvasta asennosta yksi
fload_2(ei mitään)työntää kellukkeen paikallisesta muuttuvasta asennosta kaksi
fload_3(ei mitään)työntää kellukkeen paikallisesta muuttuvasta asennosta kolme

Seuraavassa taulukossa on ohjeet, jotka työntävät paikalliset tyypin tyypit pitkät ja kaksinkertaiset pinoon. Nämä ohjeet siirtävät 64 bittiä pinokehyksen paikallisesta muuttujaosasta operandiosioon.