---
title: 'Source, Bytecode, and the Virtual Machine'
source: 'https://academia.sh/en/courses/java-fundamentals/source-bytecode-and-virtual-machine'
course: 'Java Fundamentals'
language: en
updated: '2026-08-17T18:09:39+00:00'
license: 'CC BY-SA 4.0'
---

# Source, Bytecode, and the Virtual Machine

The source written in Java is not the thing that runs: eight of twelve methods carry a step in the class file that is absent from the source, 13 added steps counted in total; a concatenation sitting in the source, by contrast, is entirely absent from the class file.

The Python curriculum closed with five courses, and its closing course defined a project's
number with a single question: how many distinct outcomes does the same input give. Something
was shared across everything measured through that curriculum — the source written stayed
close to what the interpreter saw. When asked which method got called under a syntax form, the
answer was searched for **at runtime**, because the distance between the source text and the
thing running was short.

In Java, that distance is not short. Between the source and the thing that runs sits a
**compiler** and the **class file** it produces; the **Java virtual machine** reads not the
source but that file. The difference in between is not a difference in form: the class file
holds things not written in the source. This lesson's question, and the whole course's, comes
from here — the line written turns into **how many steps, and which ones**, in the class file?

## The Chain and This Lesson's Share

The How Computers Work course established the compiler, interpreter, and virtual machine
trio, the concept of **bytecode**, and **just-in-time compilation**; it also covered
compilation stages in a separate lesson. **These are not repeated here.** What was established
there was the **concept**; what is measured here is the **file** a compiler actually produces
for this source.

The chain holds three stops. Source text is what the human writes. The compiler turns it into
a class file; this file carries bytecode, that is, a sequence of instructions written in the
virtual machine's instruction set. The virtual machine loads the file and executes the
instructions. Only the first of the two passes is this lesson's subject: **the difference
between the source and the class file.**

The difference being measurable comes from a convenience. The class file's format is defined,
and the standard library carries both an interface reading that file and a compiler callable
from within a program. No external tool is needed for the measurement: we hand over a text,
compile it, read the file that comes out. Since we write the source ourselves, we know what we
wrote; whatever extra we find in the file is what the compiler put there.

## The Measurement Core

Every measurement in this course passes through a single helper class. `Gauge` compiles a
given source text, reads the class file it produces, and returns two things for every method:
**how many instructions** were produced, and **which step not written in the source** was
added. An added step is one of five kinds — a call, a dynamic call, an object or array
creation, a type check, a numeric conversion.

The measurement's assumptions:

- **LR1** — The oracle is the rig itself: since we wrote the source ourselves, we know what we
  wrote, so whatever extra is found in the class file is not an inference, it is a dump.
- **LR2** — The compiler and the class-file reader are part of the standard library and are
  called from within the program; no external tool is used.
- **LR3** — The counted unit is the **instruction**. The numbers come from one compiler run and
  are deterministic within that run; the lesson's thesis does not rest on a single instruction
  count, it rests on the **direction** between the numbers.
- **LR4** — The measurement reads no environment-dependent data: duration, memory address,
  identity, and path are not written.
- **LR5** — The measured source carries twelve methods, and these methods cover the syntax
  forms the course will pay off lesson by lesson; it is not the language's whole syntax.

```java
// Gauge.java — the measurement core: reads what the compiler adds to the class file
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Gauge {
    record Method(String name, int instructions, List<String> added) {}

    static ClassModel compile(String source, String className) throws Exception {
        Path d = Files.createTempDirectory("gauge");
        Path f = d.resolve(className + ".java");
        Files.writeString(f, source);
        PrintWriter sink = new PrintWriter(Writer.nullWriter());
        if (ToolProvider.findFirst("javac").orElseThrow()
                .run(sink, sink, "-d", d.toString(), f.toString()) != 0)
            throw new IllegalStateException("did not compile: " + className);
        return ClassFile.of().parse(d.resolve(className + ".class"));
    }

    static List<Method> read(String source, String className) throws Exception {
        List<Method> methods = new ArrayList<>();
        for (MethodModel m : compile(source, className).methods()) {
            if (m.code().isEmpty()) continue;
            List<String> added = new ArrayList<>();
            int instructions = 0;
            for (CodeElement e : m.code().get()) {
                if (e instanceof Instruction) instructions++;
                if (e instanceof InvokeInstruction iv)
                    added.add(shortName(iv.owner().asInternalName()) + "." + iv.name().stringValue());
                else if (e instanceof InvokeDynamicInstruction id)
                    added.add("dynamic:" + id.name().stringValue());
                else if (e instanceof NewObjectInstruction n)
                    added.add("new " + shortName(n.className().asInternalName()));
                else if (e instanceof NewPrimitiveArrayInstruction a)
                    added.add("new array " + a.typeKind().name().toLowerCase(Locale.ROOT));
                else if (e instanceof NewReferenceArrayInstruction a)
                    added.add("new array " + shortName(a.componentType().asInternalName()));
                else if (e instanceof TypeCheckInstruction t && t.opcode() == Opcode.CHECKCAST)
                    added.add("type check " + shortName(t.type().asInternalName()));
                else if (e instanceof ConvertInstruction c)
                    added.add("convert " + c.opcode().name().toLowerCase(Locale.ROOT));
            }
            methods.add(new Method(m.methodName().stringValue(), instructions, added));
        }
        return methods;
    }

    static String shortName(String internal) { return internal.substring(internal.lastIndexOf('/') + 1); }
}
```

The measured source also sits in a separate file, because this lesson will read it twice.

```java
// Source.java — the measured source text: twelve methods, twelve syntax forms
class Source {
    static final String SAMPLE = """
        import java.util.List;
        class Sample {
            static int add(int a, int b) { return a + b; }
            static Object box(int n) { Integer k = n; return k; }
            static int unbox(Integer k) { int n = k; return n; }
            static String concat(String a, int n) { return a + n; }
            static String constantConcat() { return "sim" + "ple"; }
            static String loopConcat(String[] d) {
                String s = "";
                for (String x : d) s += x;
                return s;
            }
            static int arraySum(int[] d) { int t = 0; for (int x : d) t += x; return t; }
            static int listSum(List<Integer> l) { int t = 0; for (int x : l) t += x; return t; }
            static long widen(byte b, long u) { return b + u; }
            static byte narrow(byte b) { b += 300; return b; }
            static int variadic(int... numbers) { return numbers.length; }
            static int variadicCall() { return variadic(1, 2, 3); }
        }
        """;
}
```

## Method by Method: How Many Instructions, Which Step

```java
// Main.java — instruction count and step not in source, for every method
public class Main {
    public static void main(String[] args) throws Exception {
        System.out.printf("%-18s %6s  %s%n", "method", "instrs", "step not in source");
        int written = 0, withAdded = 0, total = 0;
        for (Gauge.Method y : Gauge.read(Source.SAMPLE, "Sample")) {
            System.out.printf("%-18s %6d  %s%n", y.name(), y.instructions(),
                    y.added().isEmpty() ? "-" : String.join(", ", y.added()));
            if (y.name().equals("<init>")) continue;
            written++;
            if (!y.added().isEmpty()) withAdded++;
            total += y.added().size();
        }
        System.out.println("written methods: " + written
                + " | carrying an added step: " + withAdded
                + " | total added steps: " + total);
    }
}
```

```
method             instrs  step not in source
<init>                  3  Object.<init>
add                     4  -
box                     5  Integer.valueOf
unbox                   5  Integer.intValue
concat                  4  dynamic:makeConcatWithConstants
constantConcat          2  -
loopConcat             24  dynamic:makeConcatWithConstants
arraySum               24  -
listSum                20  List.iterator, Iterator.hasNext, Iterator.next, type check Integer, Integer.intValue
widen                   5  convert i2l
narrow                  7  convert i2b
variadic                3  -
variadicCall           16  new array int, Sample.variadic
written methods: 12 | carrying an added step: 8 | total added steps: 13
```

## Reading the Numbers

**Eight** of the twelve written methods carry a step in the class file not present in the
source; **13 added steps** in total. This is the measure the course will carry: **an added
step is the rule, not the exception.** In every row of the table, the method we wrote sits on
the left, what the compiler put there sits on the right.

Reading the rows one by one shows where the addition comes from. `Integer k = n` is an
assignment in the source, and an `Integer.valueOf` call in the class file. `a + n` is written
like an operator, and is a **dynamic call** in the class file. On the `b + u` line, a
conversion instruction is added so a `byte` value can be added to a `long`. `variadic(1, 2, 3)`
stands like a three-argument call; in the class file, an array is created first, then a
single-argument call is made — this is why a three-instruction method's call costs **16
instructions**.

The **four** methods carrying no added step give just as much information: `add`,
`constantConcat`, `arraySum`, and `variadic`. What they share is this — none of them asks
another party a question at runtime. Adding two integers, reading an array's element, asking
an array's length: all of these are met by the virtual machine's direct instructions, and no
call steps in between. This is where the added step's condition is read: the compiler puts a
step in between when it **cannot find** a match for the written form in the instruction set.

This fourth method also shows where an added step lands. `variadic` is three instructions and
carries no step; its **call**, though, costs 16 instructions and carries an array creation. The
cost of a method taking a variable number of arguments does not sit in its definition, it sits
**where it is called**. Looking only at a form's definition, when searching for its cost, is
misleading.

The table also has a method **never written in the source** at all: `<init>`, a
three-instruction constructor. No constructor is written for the `Sample` class in the source;
the class file has one, and its only job is to call the base class's constructor. The next
lesson measures when this line disappears.

The sharpest reading comes from comparing two rows. `arraySum` and `listSum` carry the
**same** loop form in the source — both are the enhanced `for` loop. On an array, this loop
produces **24 instructions** and **zero** added steps; on a list, **20 instructions** and
**five** added steps. That is, **fewer instructions, more added steps**: instruction count and
added-step count do not move in the same direction. On the array, the virtual machine's direct
instructions are enough; on the list, an iterator chain is set up — `iterator`, `hasNext`,
`next`, a type check, and an `intValue`. The two measurements say separate things, and one
measurement cannot be compared to another without saying what it counts.

The same observation can be built the other way too. `loopConcat` and `arraySum` carry
**equal** instruction counts — both 24 — but one carries a dynamic call, the other carries no
step at all. Equal instruction count does not mean equal work. Counting instructions is one
measure; counting added steps is another, and neither substitutes for the other.

## A Bounding Measurement: What Sits in Source, Missing from the Class File

Every row up to here showed an **addition**. The claim's boundary sits in the same table too:
`constantConcat`'s source writes a concatenation, and its class file carries **2 instructions**
and **zero** added steps. Given the same `+` operator turns into a dynamic call in `concat`,
what happened here? The class file's instruction names give the answer.

```java
// Trace.java — is the concatenation sitting in the source present in the class file
import java.lang.classfile.*;
import java.lang.classfile.constantpool.*;
import java.util.*;

public class Trace {
    static List<String> opcodes(ClassModel cm, String method) {
        List<String> a = new ArrayList<>();
        for (MethodModel m : cm.methods())
            if (m.methodName().equalsString(method) && m.code().isPresent())
                for (CodeElement e : m.code().get())
                    if (e instanceof Instruction i) a.add(i.opcode().name().toLowerCase(Locale.ROOT));
        return a;
    }

    public static void main(String[] args) throws Exception {
        ClassModel cm = Gauge.compile(Source.SAMPLE, "Sample");
        System.out.println("constantConcat -> " + opcodes(cm, "constantConcat"));
        System.out.println("concat         -> " + opcodes(cm, "concat"));
        Set<String> strings = new HashSet<>();
        for (PoolEntry pe : cm.constantPool())
            if (pe instanceof StringEntry se) strings.add(se.stringValue());
        for (String s : List.of("simple", "sim", "ple"))
            System.out.printf("class file contains string literal \"%s\": %s%n", s, strings.contains(s));
    }
}
```

```
constantConcat -> [ldc, areturn]
concat         -> [aload_0, iload_1, invokedynamic, areturn]
class file contains string literal "simple": true
class file contains string literal "sim": false
class file contains string literal "ple": false
```

`constantConcat` consists of two instructions: load a constant, return. There is no
concatenation in there. Furthermore, the concatenation's **operands** are not there either: the
class file carries the string `"simple"`; `"sim"` and `"ple"` are not found in it. Two
constants have been folded into a single constant during compilation — this is **constant
folding**, which the How Computers Work course established as a concept, and here it appears
not as a concept but as a measured absence.

This is where the course's second claim is born: **an added step is not always an addition;
sometimes it is a subtraction.** The same `+` operator gives two separate results in two
methods — zero steps in one, a dynamic call in the other. What creates the distinction is not
the operator itself, it is **whether its operands are known at compile time**. Looking at the
written form cannot tell us what will happen in the class file.

## Which Machine Was the Translation Written For

The instruction names say a second thing too. `aload_0` and `iload_1` refer to a **local
variable slot** by number, not a processor register. `ldc` pushes a value onto the **stack**,
`areturn` takes one off the stack and returns. This instruction set belongs to a stack machine,
and nowhere in it does a real hardware name appear.

The same holds for names. In the table, the `listSum` row writes `List.iterator` and
`Iterator.hasNext`: the class file carries the method it will call not by address, but **by
name**. What that name corresponds to in code is resolved not at compile time, but while the
file is being loaded.

This is portability's measure: the compiler wrote not to a machine with hardware, but to **a
machine with a specification**. The class file is not translated to a real processor; that
translation is taken on by the virtual machine itself, while running — this is the job the How
Computers Work course established under the heading of just-in-time compilation. What this
course measures is not that second pass, it is the first.

## Summary

- In Java, a compiler and a class file sit between the source and the thing that runs; the
  virtual machine reads not the source, but the bytecode in the class file.
- The measurement core `Gauge` compiles a source text and, by reading the class file, dumps
  the instruction count and the steps not written in the source for every method.
- **Eight** of the twelve written methods carry an added step, **13 added steps** are counted
  in total; there is even a three-instruction constructor never written in the source at all.
- The same enhanced `for` loop produces **24 instructions** and **zero** added steps on an
  array, **20 instructions** and **five** added steps on a list: instruction count and added
  steps do not move in the same direction.
- A concatenation sitting in the source is entirely absent from the class file; two constants
  fold into one, and the operands are not found in the file. An added step is sometimes a
  subtraction.
- The class file's instructions refer to a slot number and a stack operation, and carry the
  method they will call by name: what got translated was written for a machine with a
  specification, not a machine with hardware.

## Next Step

This lesson measured **what** the class file is, but never looked at **when** the instructions
inside it run. The table held a constructor never written in the source, and when it gets
called was never said; where a class's own setup work sits was never asked either. The next
lesson measures the program's life cycle: which signature does the virtual machine count as
the **entry point**, in what order do the static initializer and the constructor run, and how
many members does a class with two names written in the source carry on its class-file
surface?
