Classfile

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.

Klasse-informatie

[bewerken | brontekst bewerken]

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:

  • De access flags van de klasse, zoals "public", "abstract" of "final". Deze worden gecodeerd als een bitmask[2].
  • De naam van de klasse. Dit is een verwijzing naar een item in de constantpool dat een string met de naam van de klasse bevat.
  • De superklasse. Dit is weer een verwijzing naar een string in de constantpool.
  • Het aantal interfaces dat deze klasse implementeert.

Als de klasse één of meer interfaces implementeert volgt vervolgens een lijst met de geïmplementeerde interfaces.

Fields en methods

[bewerken | brontekst bewerken]

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.

Type descriptor

[bewerken | brontekst bewerken]

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.

Het code-attribuut

[bewerken | brontekst bewerken]

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.

Voorbeeld van een Code-attribuut

[bewerken | brontekst bewerken]

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).

Class attributen

[bewerken | brontekst bewerken]

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).

Opcodes en instructies

[bewerken | brontekst bewerken]

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.