Grundlagen

Maschinensprache

Unter jedem Java-Programm liegt eine Sprache aus nackten Zahlen: Lade dies, addiere das, springe dorthin. Eine kleine Maschine mit 16 Registern und 256 Speicherwörtern zeigt, wie Schleifen, Methoden und Arrays auf dieser Ebene aussehen – und wo die JVM anders denkt.

Lesestand

Maschinensprache ist die Sprache, die der Prozessor direkt ausführt: Jeder Befehl ist eine Zahl, die eine einfache Operation codiert – zwei Register addieren, ein Wort aus dem Speicher laden, zu einer Adresse springen. Ein Programmzähler zeigt auf den nächsten Befehl; die CPU holt ihn, dekodiert ihn, führt ihn aus, und alles beginnt von vorn.

Alles, was höhere Sprachen bieten, entsteht aus wenigen Mustern: Schleifen und if aus bedingten Sprüngen, Methodenaufrufe aus einem Sprung, der sich die Rücksprungadresse merkt, Arrays und Zeiger aus Registern, die Adressen enthalten. Die JVM führt Bytecode aus, der ganz ähnlich aussieht – nur als Stapelmaschine statt mit Registern.

Eine Lehrmaschine

Echte Befehlssätze wie x86-64 haben tausende Varianten. Zum Verstehen reicht eine Maschine nach dem Vorbild der TOY-Maschine von Sedgewick und Wayne: 256 Speicherwörter zu je 16 Bit (Adressen 00 bis FF, hexadezimal), 16 Register R0 bis RF, wobei R0 immer 0 enthält, und ein 8-Bit-Programmzähler. Jeder Befehl ist ein 16-Bit-Wort aus vier Hex-Ziffern:

Ziffer 1BedeutungZiffer 1Bedeutung
0anhalten8 d aaRd ← M[aa] – aus dem Speicher laden
1 d s tRd ← Rs + Rt9 d aaM[aa] ← Rd – in den Speicher schreiben
2 d s tRd ← Rs − RtA d _ tRd ← M[Rt] – indirekt laden
3 d s tRd ← Rs & RtB d _ tM[Rt] ← Rd – indirekt schreiben
4 d s tRd ← Rs ^ RtC d aawenn Rd = 0: springe nach aa
5 d s tRd ← Rs << RtD d aawenn Rd > 0: springe nach aa
6 d s tRd ← Rs >> RtE d _ _springe zur Adresse in Rd
7 d aaRd ← aa – Konstante ladenF d aaRd ← PC, springe nach aa – Aufruf

Das Wort 1CAB heißt also: RC ← RA + RB. Und C013 heißt: wenn R0 = 0, springe nach 13 – da R0 immer 0 ist, ein unbedingter Sprung. Schreibt man nach Adresse FF, erscheint der Wert in der Ausgabe.

Die Maschine zum Mitklicken

Drei Programme stehen ab Adresse 10, ihre Daten ab 00. Links der Speicher mit dem dekodierten Befehl jeder Zeile, rechts Register, Programmzähler (PC) und Befehlsregister (IR):

16 Register, 256 Wörter
Speicher
Steuerung
Register
Ausgabe (M[FF])
Programm bearbeiten

Format: Adresse: Wort in Hex, eine Zeile pro Wort; alles nach // ist Kommentar. Änderungen werden sofort geladen.

Von Java zu Maschinenbefehlen

Ein Compiler übersetzt jedes Sprachkonstrukt in ein festes Muster. Wähle eines:

Fünf Konstrukte, fünf Muster

Zwei Dinge fehlen der Lehrmaschine, die echte Prozessoren haben – und beide zeigen, was Hochsprachen leisten. Erstens ein Stack: Unser Aufruf merkt sich die Rücksprungadresse in einem Register. Ruft die Methode selbst eine andere auf, wäre die Adresse überschrieben – Rekursion geht so nur, wenn man die Adressen selbst im Speicher sichert. Echte Prozessoren legen Rücksprungadressen und lokale Variablen auf einen Stapel im Speicher; genau der läuft bei endloser Rekursion über (StackOverflowError). Zweitens Schutz: Bei uns kann ein Programm jede Adresse überschreiben, sogar seine eigenen Befehle. Programme, die sich selbst verändern, waren früher ein Trick, um Speicher zu sparen; heute verhindert das Betriebssystem, dass Speicher gleichzeitig beschreibbar und ausführbar ist.

Für Entwickler übersetzt

Die JVM hat ihre eigene Maschinensprache: Bytecode. Anders als unsere Lehrmaschine ist sie eine Stapelmaschine – Befehle nehmen ihre Operanden vom Stapel und legen das Ergebnis darauf, statt Register zu benennen. So sieht die Summe von 1 bis n aus, wenn man sie mit javap -c ansieht:

static int summe(int n) {
    int s = 0;
    for (int i = 1; i <= n; i++)
        s += i;
    return s;
}
 0: iconst_0          // 0 auf den Stapel
 1: istore_1          // s = …
 2: iconst_1
 3: istore_2          // i = 1
 4: iload_2
 5: iload_0
 6: if_icmpgt 19      // i > n? raus
 9: iload_1
10: iload_2
11: iadd              // s + i
12: istore_1
13: iinc 2, 1         // i++
16: goto 4
19: iload_1
20: ireturn

Man erkennt dasselbe Muster wie in unserem Simulator: Bedingung prüfen, bei Nichterfüllung hinausspringen, Rumpf, Sprung zurück. Der JIT-Compiler übersetzt diesen Bytecode zur Laufzeit in echte Maschinenbefehle für die CPU – dabei werden die Stapelplätze wieder auf Register verteilt.

BefehlssatzArtWo
x86-64Register, sehr viele und komplexe Befehle (CISC)die meisten PCs und Server
ARM64Register, einfache Befehle fester Länge (RISC)Smartphones, Apple-Rechner, viele Cloud-Server
RISC-VRegister, offener und lizenzfreier Standardeingebettete Systeme, Forschung, zunehmend mehr
JVM-BytecodeStapelmaschine, plattformunabhängigjede JVM – vom JIT in eine der obigen übersetzt
WebAssemblyStapelmaschine für Browser und mehrBrowser, zunehmend auch Server

Zum Anschauen: javap -c Klasse.class zeigt Bytecode, die JVM-Option -XX:+PrintAssembly (mit dem hsdis-Plugin) den Maschinencode, den der JIT daraus erzeugt.

Häufige Irrtümer

„Maschinensprache und Assembler sind dasselbe.“ – Maschinensprache sind die Zahlen, Assembler ist die lesbare Schreibweise dafür (add rcx, rax statt Bytes). Ein Assembler-Programm übersetzt eins zu eins.
„Java läuft interpretiert und ist deshalb langsam.“ – Bytecode wird anfangs interpretiert, häufig ausgeführter Code aber vom JIT in optimierten Maschinencode übersetzt – oft so schnell wie C.
„Ein Befehl dauert einen Takt.“ – Moderne CPUs zerlegen Befehle, führen mehrere gleichzeitig und außerhalb der Reihenfolge aus. Ein Speicherzugriff, der den Cache verfehlt, kostet dagegen hunderte Takte (siehe Speicherhierarchie).
„Register sind einfach schneller Speicher.“ – Sie sind Teil des Rechenwerks und haben keine Adressen im Speicher. Ihre Zahl ist im Befehlssatz festgelegt; echte CPUs haben intern noch weit mehr, auf die sie die sichtbaren Register verteilen.

Verwandte Begriffe