Language Runtimes · Virtual Machines

Constant Pools and Operand Encoding in Bytecode

Why bytecode instructions reference large constants indirectly through a pool, how the pool is laid out, and what the fixed-width index fields cost.

An instruction field is a fixed number of bits. A string literal, a double, or a reference to another class cannot be squeezed into eight bits, and putting a full pointer into the instruction stream would make the bytecode position-dependent and unshareable. The constant pool is the mechanism that resolves this: the instruction carries a small index, and the index selects an entry in a per-unit table that holds the actual value. Every bytecode format has some version of this table, and its layout determines what the compiler can emit directly, what requires an extra indirection, and how much work loading a class costs.

Index width sets the ceiling

The JVM’s constant pool is indexed by a 16-bit unsigned value, and the class file stores constant_pool_count, which is one greater than the number of entries, with index 0 reserved as invalid. The highest usable index is therefore 65,534. That number is not arbitrary; it is what a u2 can express. The pool is stored in the class file as a count followed by a variable-length array of entries, and each entry begins with a one-byte tag naming its kind.

The tags partition into two groups. Literal entries hold a value directly: CONSTANT_Integer, CONSTANT_Float, CONSTANT_Long, CONSTANT_Double, and CONSTANT_Utf8, which holds modified UTF-8 bytes of a string. Symbolic entries hold structure: CONSTANT_Class names a class by pointing at a UTF-8 entry, CONSTANT_Fieldref and CONSTANT_Methodref point at a class entry and a name-and-type entry. The indirection through UTF-8 entries is deliberate: identical names are shared, so a pool with many references to the same method name stores that name once.

enum { CONSTANT_Utf8 = 1, CONSTANT_Integer = 3, CONSTANT_Float = 4,
       CONSTANT_Long = 5, CONSTANT_Double = 6, CONSTANT_Class = 7 };

/* A constant pool entry in a typical class-file reader. */
typedef struct {
    uint8_t  tag;
    union {
        int32_t  i;
        float    f;
        uint16_t name_index;          /* Utf8, Class, String */
        struct { uint16_t class_index, nat_index; } ref;  /* Field/Method */
        struct { uint16_t name, desc; } nat;              /* NameAndType */
    } u;
} CpEntry;

/* Long and double occupy two slots; the second is unusable. */
static size_t cp_span(const CpEntry *e)
{
    return (e->tag == CONSTANT_Long || e->tag == CONSTANT_Double) ? 2 : 1;
}

The two-slot rule exists because the pool is read as a flat array and the ldc2_w instruction’s index must point at the first slot of a wide constant. Reserving the second slot keeps the indexing scheme uniform at the cost of one wasted slot per wide constant.

Why the instruction cannot always reach the pool

The narrow ldc instruction carries an unsigned byte index. It can therefore address only the first 256 pool entries, and only for the entry kinds that fit in one slot. When a compiler needs a constant at index 300, it must emit ldc_w, which carries a u2 index and is three bytes long instead of two. The assembler’s job is to choose between them, and because inserting an instruction changes offsets of everything after it, the choice can be circular: widening one instruction can push a branch target out of range, requiring another widening.

This is the bytecode equivalent of branch relaxation in an assembler, and the same fixup-iteration approach applies. A compiler emits the narrow form everywhere and records the offsets that must be patched; after layout it walks the fixup list and widens any instruction whose index exceeds 255, which shifts subsequent code and invalidates other recorded offsets unless the fixups are expressed relative to instructions rather than absolute addresses. The JVM’s own format avoids the iteration in the common case by offering both widths directly: a compiler that can already tell an index will not fit emits the wide form at once, and the branch instructions have wide counterparts for the same reason, so no relaxation pass is needed for code the compiler generates. A tool that inserts instructions into an already-emitted method faces the original problem.

Other formats choose differently. Lua’s instruction word is a fixed 32 bits, split into an opcode field and narrow operand fields, and an arithmetic instruction’s operand can be either a register number or a constant index, distinguished by a flag bit in the same field. Constants the narrow field cannot reach are loaded by a separate wide load instruction whose index field spans two operand fields. That is a deliberate trade: fixed width makes decoding a shift and a mask over one memory word, at the cost of a second instruction whenever a constant does not fit the narrow form.

The pool as a lazily resolved table

In a class file the pool holds symbolic names, not addresses. On first use, the runtime resolves an entry: it loads the referenced class, finds the named field or method, and records the resulting direct pointer in the pool slot or in a separate resolved-reference cache. The JVM specification calls this linking and gives implementations wide latitude in when it happens, requiring only that resolution errors surface at a defined point relative to the instruction that triggered them.

The design reason is that a class file must be self-contained and relocatable. A bytecode file may be loaded at any address, by any process, possibly on a different machine from the one that produced it. Symbolic references let the loader bind names to whatever the current runtime provides, which is what makes separate compilation possible at all. Direct pointers would require a relocation pass and would break the format’s portability.

The cost is a per-entry state machine. Each unresolved entry needs a place to store the resolved target and a way to distinguish “not yet resolved”, “resolved”, and “resolution failed”. Implementations typically use a side table indexed by pool slot rather than adding fields to the on-disk entry, because the on-disk layout is fixed by the specification and cannot grow. That side table is a per-class allocation proportional to the pool size, so a class with a large pool pays for it even if most entries are never used.

Failed resolution must be sticky. If resolving a field reference fails with NoSuchFieldError, later instructions referencing the same entry must fail the same way rather than retrying and possibly observing a different class state. Implementations therefore cache the error, which means the side table must be able to hold either a pointer or an error marker.

Sharing, deduplication, and the size trade

Because entries are shared by value, pool construction is a deduplication problem. Two occurrences of the same int constant become one entry; two references to the same method become one entry. Doing this well shrinks the pool, and a smaller pool means more indices fit in the narrow instruction forms.

The deduplication is not free of subtlety. CONSTANT_Utf8 entries hold modified UTF-8, which encodes the null character as two bytes rather than one so that a string can contain a null without terminating the pool’s C representation. Two strings that are equal as Java strings therefore have identical encodings, and one entry serves both. But a string literal used as a String object and the same bytes used as a class name both point at one CONSTANT_Utf8 entry while being distinct pool entries of different kinds, because the kinds carry different semantics at resolution time.

The pool also carries information the code needs but does not reference directly. A class file’s pool must contain the entry for the class itself, and it holds the descriptors of every method, which the verifier reads. That makes the pool a partial index of the class’s interface, and tools that read class files without running them rely on it being complete.

Alternatives and their consequences

Formats that target a single runtime can skip symbolic resolution. A bytecode embedded in a running image can hold direct pointers or interned object references, which removes the resolution step entirely. The cost is that the code is no longer a portable artifact; it must be regenerated or relocated for each image.

Formats that target size over speed can move constants out of the pool and into the code stream as immediate operands. A load-immediate instruction with a 32-bit payload costs five bytes but needs no pool entry, no index, and no resolution. This is common for small integers and is why many formats have dedicated one-byte push-constant opcodes for values in a small range, with a general immediate form for the rest.

Formats that target a very large number of constants widen the index field. WebAssembly’s i32.const carries a signed 32-bit immediate encoded in LEB128, so a constant needs no pool at all; the price is variable-length decoding, which costs a few extra instructions in the interpreter per constant load. That trade, variable-length code for a smaller and simpler loader, appears throughout the format: WebAssembly has no constant pool, and its module structure plays the role of one at a coarser granularity, holding the function signatures and global definitions that code indexes into.

The pool’s interaction with the loader shows up again when code is verified before execution, since the verifier must read descriptors out of the pool and reject instructions whose operand kinds do not match. That check is the subject of bytecode verification and stack map frames. The place where a resolved pool entry finally lands, as an argument or a receiver on the operand stack, is covered in calling conventions on the operand stack.

References

  1. The Java Virtual Machine Specification, Java SE 8 Edition — Oracle
  2. The Implementation of Lua 5.0 — Ierusalimschy, de Figueiredo & Celes, Journal of Universal Computer Science
  3. WebAssembly Core Specification — W3C