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:
Tyyppi | Määritelmä |
---|---|
tavu | yksi tavu allekirjoitti kahden komplementin kokonaisluvun |
lyhyt | kaksitavuinen allekirjoitti kahden komplementin kokonaisluvun |
int | 4-tavuinen allekirjoitti kahden komplementin kokonaisluvun |
pitkä | 8-tavuinen allekirjoitti kahden komplementin kokonaisluvun |
kellua | 4-tavuinen IEEE 754 -tarkka uimuri |
kaksinkertainen | 8-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
, kuormitus
ja 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:
Opcode | Operandi (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:
Opcode | Operandi (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.
Opcode | Operandi (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.
Opcode | Operandi (t) | Kuvaus |
---|---|---|
kaksisuuntainen | tavu 1 | laajentaa tavu1 (tavutyyppi) int-muotoon ja työntää sen pinoon |
sipush | tavu1, tavu2 | laajentaa 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:
Opcode | Operandi (t) | Kuvaus |
---|---|---|
ldc1 | indeksitavu 1 | työntää indexbyte1: n määrittelemän 32-bittisen vakio_poolin merkinnän pinoon |
ldc2 | indexbyte1, indexbyte2 | työntää indexbyte1: n, indexbyte2: n määrittelemän 32-bittisen vakio_poolin merkinnän pinoon |
ldc2w | indexbyte1, indexbyte2 | työ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:
Opcode | Operandi (t) | Kuvaus |
---|---|---|
iload | vindex | työ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 |
kuormitus | vindex | työ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.