Een classfile bevat bytecode die uitgevoerd kan worden door een Java Virtual Machine. Classfiles kunnen inmiddels worden geproduceerd door diverse compilers (Javac, JRuby, Scala, Groovy, Kotlin, Clojure, Jython, Fantom, Gosu, en andere), wanneer deze de broncode compileert.
Een classfile bestaat uit de volgende onderdelen:
De header bevat een magic number en versieinformatie. Een magic number is een constante aan het begin van een file die aangeeft dat de file een bepaald soort data bevat. Dit geldt niet alleen voor classfiles, maar ook voor veel andere bestandsformaten, bijvoorbeeld .GIF files en .ZIP files. Het magic number waarmee iedere classfile begint is 0xCAFEBABE
.
Het magic number wordt gevolgd door 4 bytes die de versie van de gebruikte specificatie aangeven: eerst 2 bytes voor de minor version, dan 2 bytes voor de major version. Een header voor een classfile met versie 49.0 (J2SE 5.0) ziet er dan als volgt uit:
cafebabe00000031
De header wordt gevolgd door de constantpool. In de constantpool wordt een aantal constanten gedefinieerd die in de rest van de classfile gebruikt worden. De eerste 2 bytes duiden het aantal constanten in de constantpool aan.
Iedere waarde in de pool wordt voorafgegaan door een tag (1 byte) die het datatype aangeeft (en dus ook het aantal bytes dat de daaropvolgende waarde inneemt). Er zijn 11 tags, onder andere voor (UTF-8) strings, integers en verwijzingen naar methoden en fields.
Het begin van een constantpool kan er als volgt uitzien:
0003 De pool bevat 3 constanten. 01 De eerste constante is een string. 0005 De string is 5 bytes. 48656c6c6f "Hello".
In dit geval bevat de pool 3 constanten (alleen de eerste wordt hier weergegeven). De eerste constante is een UTF-8 string (aangegeven door tag 1[1]). In het geval van een string wordt de tag gevolgd door 2 bytes die de lengte van de string aangeven, in dit geval 5 bytes. Hierna volgt de string zelf: "hello", gecodeerd in hexidecimaal als 48656c6c6f
.
Na de constantpool volgt er informatie over de klasse die door deze bytecodefile gedefinieerd wordt, zoals de naam en de superclass. Dit gedeelte bestaat uit 8 bytes, verdeeld in 4 items van elk 2 bytes:
Als de klasse één of meer interfaces implementeert volgt vervolgens een lijst met de geïmplementeerde interfaces.
Deze sectie beschrijft de velden (fields, of attributen) en methoden van de klasse. De sectie begint met 2 bytes. Deze specificeren het aantal velden van de klasse. Vervolgens volgt de lijst met velden. Daarna komen er weer 2 bytes, die het aantal methoden van deze klasse aangeven. Deze worden op hun beurt weer gevolgd door de methoden.
Velden en methoden worden op dezelfde manier gecodeerd. Elk gedeelte dat een veld of een methode specificeert begint met 2 bytes die de access flags (zoals "static", "public", "abstract", enzovoort) aangeven, gecodeerd als bitmask[3]. De volgende 2 bytes bevatten de naam van de methode of het veld. Dit is weer een verwijzing naar een string in de constantpool.
Hierna volgen nog 2 bytes die het type van de methode of het veld specificeren (de zogenaamde type descriptor). Het type van een veld geeft aan welk type waarden het veld kan bevatten. Het type van een methode geeft aan hoeveel argumenten de methode verwacht, van welke datatype deze argumenten zijn en welk datatype de methode retourneert.
Deze uit 2 bytes bestaande type descriptor is weer een verwijzing naar een string in de constantpool. Bijvoorbeeld: de string in de constantpool waarnaar de type-descriptor van de main-routine wijst is altijd ([Ljava/lang/String;)V
. Dit geeft aan dat de main-routine een array (aangegeven door [
) van strings (aangegeven door Ljava/lang/String
) als argument verwacht en void (aangegeven door V
) als returntype heeft.
Na de type-descriptor volgen eventuele attributen van de methode of het veld. Een veld kan bijvoorbeeld een ConstValue
-attribuut hebben dat de beginwaarde van het veld specificeert. Meestal hebben velden echter geen attributen.
De sectie met attributen begint met 2 bytes die het aantal attributen voor deze methode of dit veld aangeven. Een attribuut wordt als volgt gecodeerd: eerst 2 bytes die weer naar een string in de constantpool verwijzen. Deze string bevat de naam van het attribuut. Vervolgens 4 bytes die de lengte van de data die bij het attribuut hoort aangeven.
De classfile specificatie maakt het mogelijk om zelf nieuwe attributen toe te voegen. De JVM die de classfile uitvoert negeert attributen die hij niet ondersteunt.
Methoden hebben in meestal in ieder geval één attribuut: een Code
-attribuut. Hierin wordt onder andere gespecificeert hoeveel items op de stack de methode (maximaal) gebruikt, en hoeveel lokale variabelen. Ook wordt in het Code-attribuut de implementatie van de methode (de body) opgenomen.
Na de body van de methode volgt nog informatie over de exceptions die de methode afhandeld[4]. En ten slotte kan het Code-attribuut op zijn beurt ook nog weer attributen hebben. Die worden dan na de informatie over exceptions gespecificeerd.
Het Code-attribuut van een methode kan er als volgt uitzien:
0003 De naam van dit attribuut is de derde string in de constantpool (in dit geval "Code"). 00000009 De data van dit attribuut is 9 bytes groot. 0002 Deze methode gebruikt 2 items op de stack. 0000 Deze methode gebruikt geen lokale variabelen. 00000001 De lengte van de code zelf (de implementatie van de methode) is 1 byte. b1 De body van de methode bestaat hier uit slechts 1 instructie, met opcode 5.
De bovenstaande methode bevat slechts één instructie: een return-statement (deze heeft opcode 5).
De classfile eindigt met (nog) een lijst attributen. Deze gelden voor de hele class en worden op dezelfde manier gecodeerd als de attributen van velden en methodes. Ook hier geeft de specificatie de mogelijkheid om nieuwe attributen te implementeren. De specificatie zelf definieerd alleen het SourceFile
-attribuut, die de naam van de file met broncode bevat waarvan de classfile afkomstig is (voor debuggers).
Zoals we al zagen worden de instructies waaruit een methode bestaat opgeslagen in een Code
-attribuut bij de betreffende methode. Omdat o.a. Java geen code buiten methodes (behalve declaraties en initialisatie-assignments) toestaat kunnen alle instructies waaruit een programma bestaat op deze manier in de classfile opgeslagen worden.
Broncode-statements en expressies worden hiervoor door de compiler vertaald naar reeksen zogenaamde bytecodes. Net als bij "echte" machinetaal bestaat een bytecode-instructie uit een opcode en een aantal argumenten, de operanden, waarbij het aantal operanden en hun lengte afhangt van de opcode.
Een serie bytecode-instructies kan er als volgt uitzien:
1201 Opcode 18: Push item op de stack. Argument: de integer 1.
1202 Zelfde, maar push nu een 2 op de stack.
60 Opcode 96: Haal 2 items van de stack, tel ze op en zet het resultaat op de stack
3601 Opcode 54: Haal item van de stack en sla deze op in lokale variabele 1.
b20508 Opcode 178: Haal een verwijzing naar een statisch veld uit de constantpool,
in dit geval "java.lang.System.out
".
1501 Opcode 21: Push inhoud van een lokale variabele op de stack.
b60609 Opcode 182: Haal een verwijzing naar een object en een argument van de stack.
Roep vervolgens de methode op index 0609 in de constantpool aan op
het object met het argument van de stack als argument.
Het bovenstaande voorbeeld wordt aanschouwelijker als we het noteren als Java assembler:
ldc 1 # 1 naar stack ldc 2 # 2 naar stack iadd # optellen, resultaat naar stack istore 1 # resultaat opslaan in lokale variabele getstatic java/lang/System/out Ljava/io/PrintStream; # ref naar System.out (type PrintStream) # op de stack iload 1 # inhoud lokale variabele naar stack invokevirtual java/io/PrintStream/println(I)V # System.out.println(1) # System.out en 1 komen van de stack
Dit stukje bytecode telt 1 en 2 bij elkaar op en print het resultaat.