Back to course

Modules

20 minadvancedModule 5: Linking and VersioningContinues from jvm-invokedynamic
20 min · Module 5: Linking and Versioning · Algorithms

Why This Matters

Java spent fifteen years without any way to say "this class belongs to that library and must not see anything else". The class path was one flat namespace, every class was public or private, and encapsulation stopped at the package boundary whether or not the package was meant to be an API. Then Java 9 added modules, and the mechanism they use is the strangest in this course: two constant pool tags that the JDK's own disassembler does not recognise.

Here is the finding that makes this concept worth its own lesson. javap — the reference disassembler, shipped with the JDK, used in the first concept as one of three independent readers — prints CONSTANT_Module and CONSTANT_Package as Unknown:

  $ javap -v -p module-info.class

     #6 = Unknown            #7             // "com.example.demo"
     #8 = Unknown            #9             // "java.base"
    #13 = Unknown            #14            // com/example/api
    #15 = Unknown            #16            // com/example/internal
    #17 = Unknown            #12            // "com.example.friend"
    #19 = Unknown            #20            // com/example/model

It reads them correctly and names them "Unknown". The Utf8 it resolves is right, so the tool understands the structure; it just has no name for the tag. And that is a genuinely useful thing to know about a thirty-year-old disassembler: tags 19 and 20 are so rarely used that the tool that has shipped with every JDK for three decades still has no label for them. It is a direct measurement of how unusual a feature is.

A Simple Model

Two new pool tags, and they are the simplest in the format — one name index each:

CONSTANT_Module: u1 tag = 19 u2 name_index a Utf8: a module name, dots not slashes "com.example.demo" CONSTANT_Package: u1 tag = 20 u2 name_index a Utf8: a package name, SLASHES "com/example/api"

Three bytes each, and that is the whole thing. Neither is a CONSTANT_Class, which is the design point worth noticing: a class entry's name_index points at an internal name (com/example/api/Service) because that is what the JVM needs to resolve, whereas a module's or a package's name is a human-facing dotted or slashed identifier that the resolution logic never uses. So the two new tags are names for the benefit of readers, and the format gave them their own tags rather than overloading Class to mean three things.

That is the opposite choice from the annotation's enum and class values, which were flattened into CONSTANT_Utf8 strings because they needed no identity. Modules and packages do need identity — the same module name appears in many files and a reader compares them — so each got a pool tag that can be shared. One of those decisions is right and the other is right for a different reason, and the difference is whether the name has to be found by reference or only read.

The Module attribute, which is five sections

The attribute is the one place the module's whole declaration lives, and it is five variable-length sections with no overall count:

Module: u2 module_name_index a CONSTANT_Module u2 module_flags usually 0 u2 module_version_index a CONSTANT_Utf8, or 0 u2 requires_count then that many requires entries u2 exports_count then that many exports entries u2 opens_count then that many opens entries u2 uses_count then that many class indices u2 provides_count then that many provides entries

No section ids, no tags, no end markers — the five sections follow each other in a fixed order and the counts tell you where each ends. That is the opposite design from the escape hatch, where each thing carries its own name and length. Here everything is positional, which means a reader must know the order to read it at all, and adding a sixth kind of directive would require a new major version rather than an attribute.

The Actual Detail

A real module-info.class, compiled from a nine-line source, 370 bytes, 24 constant pool entries and one 56-byte attribute:

  flags: (0x8000) ACC_MODULE

  Module:
    #6,0                                  // "com.example.demo"
    #0
    2                                     // requires
      #8,0                                // "java.base"
      #10                                 // 26.0.1
      #11,20                              // "java.logging" ACC_TRANSITIVE
      #10                                 // 26.0.1
    2                                     // exports
      #13,0                               // com/example/api
      #15,0                               // com/example/internal to ... 1
        #17                               // ... to "com.example.friend"
    1                                     // opens
      #19,0                               // com/example/model
    1                                     // uses
      #21                                 // com/example/api/Service
    1                                     // provides
      #21                                 // com/example/api/Service with ... 1
        #23                               // ... with com/example/internal/Impl

Read that against the nine lines of source that produced it, and every count has a visible cause:

  module com.example.demo  --  the nine directives below:
      requires java.base;                          -> 1 requires, no flags
      requires transitive java.logging;           -> 2nd requires, ACC_TRANSITIVE
      exports com.example.api;                    -> 1st export, no target list
      exports com.example.internal                -> 2nd export, "to" count 1
              to com.example.friend;
      opens com.example.model;                    -> 1 opens, no target list
      uses com.example.api.Service;               -> 1 uses
      provides com.example.api.Service            -> 1 provides, "with" count 1
              with com.example.internal.Impl;
  -- end of module --

Several things fall out of the decoded values, and they are the interesting content of the concept.

The version string is shared, and the flags are a second field

Both requires entries carry the same version index #10 = "26.0.1" — one Utf8 for two references, the constant pool's dictionary doing its job on a field most formats would have inlined. And the second entry's #11,20 is a CONSTANT_Module with flags 0x0020 = ACC_TRANSITIVE.

So a requires entry is three fields, not two: a module reference, a u2 of flags, and a version string. The version is not part of the module name — it is a separate optional Utf8, and a module that specifies no version has 0 there. A version number being a plain string rather than a comparable value is a deliberate and slightly surprising choice, and the reason is that Java versions are strings in the specification too: 26.0.1 and 26 are both legal and are not compared numerically.

exports and opens, and the target lists

The two exports entries differ in a field that is easy to miss: the first has 0 target count and the second has 1, naming com.example.friend. An export is public to everyone by default and restricted only by an explicit target list — the to clause narrows it. There is no "exported to nobody" form; that is what opens is for.

And the distinction between exports and opens is compile-time versus run-time access. exports makes a package's types nameable in source — other modules can import them and call their public members. opens makes a package's types reflectable — Class.forName works and setAccessible can reach non-public members, but you still cannot name the type in source. Two keywords, one attribute, and a distinction that exists because Java has compile-time access and reflective access as separate mechanisms. A framework doing dependency injection opens a package; a library that wants to be used exports one.

uses and provides: the service declaration

The last two sections are the service loader, and they are the only parts of the attribute that name classes rather than modules or packages — both use CONSTANT_Class indices, not the new tags:

  uses:      #21 -> com/example/api/Service      (a CONSTANT_Class)
  provides:  #21 -> com/example/api/Service with ... 1
                       #23 -> com/example/internal/Impl

  and note: the SAME index #21 appears in both sections, because
  both are pointing at the same Class pool entry. The service type
  is stored once.

That is the point of the pairing. A module uses a service to declare that it looks one up, and provides one to declare that it supplies an implementation. The runtime's service loader pairs them by type name, not by identity, so the two lists in two different files meet at a string. A cross-file correspondence resolved by name — and the same shape as a dynamic ELF symbol being resolved by name across objects, with a different failure mode if the names do not match.

Which gives the honest limit of the whole feature. Module boundaries are enforced by the launcher, not by the class file. A class file carries its module's declarations, but those declarations mean nothing on a plain class path, where everything sees everything. That is the same two-mode architecture as the rest of Underlayer, and it is worth naming: the attribute is a declaration of intent that a specific runtime configuration chooses to honour. Run the same class on the class path and the module system is inert — which is exactly how the migration was staged, and why every module-aware library also works without it.

A Real Example

Why this feature needed new pool tags when sealing was happy with a list of class indices — and the answer is about what the names are for.

Sealed: PermittedSubclasses -> a list of Class indices because a permitted subclass must RESOLVE to a class. A Class entry has the internal name the loader needs. Module: requires -> CONSTANT_Module exports -> CONSTANT_Package uses -> CONSTANT_Class the difference: a module name "com.example.demo" is NOT a class name. Nothing loads it. It is matched against other modules' names, in configuration files and on the command line. so the Module and Package tags mark names that are read and compared but never resolved to a Class. That is a genuinely different kind of name, and giving it a different tag keeps CONSTANT_Class meaning exactly one thing.

And the payoff of that choice is visible in this course's own tooling. The `java.lang.classfile` API added in Java 22 models these as distinct types — a ModuleEntry and a PackageEntry, distinct from ClassEntry — so a reader using the standard API cannot confuse them even by accident. A well-chosen pool tag becomes a type in every API built on top of it, and a conflated one becomes a permanent ambiguity that every consumer has to work around.

Compare ELF's e_type and PE's characteristics: both formats have a "what kind of thing is this" field, and both added values to it over time — ET_DYN and ET_CORE in ELF, the relocation-stripped flag in PE. The class file's answer was to add tags rather than flag values, which is the more expensive choice up front and the cheaper one forever, because a new tag cannot be confused with an old one and a new flag value can be silently mistaken for a combination of existing bits by a reader that does not know it. A reader from 1997 reading a tag it does not know skips it; a reader from 1997 reading an unknown flag value still renders it, and the InnerClasses0x0008 problem from Module 3 is exactly that failure.

The final point is the one that makes this concept more than a structure walk. A class file that declares modules is 370 bytes and describes an entire library's public surface — what it requires, what it exposes, what it reflects on, what it consumes and provides — in a form a machine can check. Before this, none of that was in the file; it lived in a build tool's configuration and in a convention about directory layout. The module feature did not add runtime capability so much as move a declaration from prose into bytes, and every other feature in this course has been a version of the same move: record components, permitted subclasses, enum constants, lambda targets. The format's thirty-year pattern is that each language feature eventually arrives as a structure the machine can read rather than a convention it can only hope for.

Try It Yourself

$ javap -v -p out/module-info.class
$ python3 courses/jvm/assets/samples/crosscheck.py classes/module-info.class

Check Your Understanding

Without looking back: you run javap -v on a module-info.class and see #6 = Unknown #7 // "com.example.demo". The same file's uses section shows #21 as a CONSTANT_Class. What is the difference between the two entries, why does javap label one and not the other, and what would go wrong if CONSTANT_Module were encoded as a CONSTANT_Class?

Apply It

You are building a dependency-analysis tool. It reads every class file, collects the module each one belongs to from its Module attribute, and builds a graph of module dependencies to detect cycles. On a codebase built with modules it works. On the class path — where most dependencies actually live — it reports every class as belonging to the unnamed module and then produces a single-node graph, and the team concludes the project has no cycles when it has three. What is the tool assuming, and how should it handle the two cases without making the module-aware path worse?

The general habit: a format leaves three states open — present with a value, present with a sentinel, and absent. Never collapse the last two. A default is usually the tool's invention, not the format's.

Connect

Modules are an import mechanism, and the closest comparison is one this course has already established: DWARF's unit-level model. A DWARF unit declares its own dependencies — which unit it was compiled from, its producer, its language — and a consumer that has not got the unit it needs cannot proceed. Both put a dependency declaration in the artefact so the consumer can check it, rather than leaving it to a build system's knowledge. The differences are instructive too: DWARF units are not enforced at load, and a DWARF consumer that skips a missing unit often still works.

The exports/opens distinction has a closer relative in the PE's exports directory and in an ELF symbol table: all three are a list of names with an address or a target, and all three are declarations of what a consumer may reach. Where they part company is the enforcement point — a PE export table is honoured by the loader because there is no other way to find the code, while a module's exports is honoured by a launcher that could be configured to ignore it. The Java case is the unusual one: a declaration of encapsulation that is optional to enforce, which is what made the migration possible and is also why it has been called advisory.

The positional five-section Module attribute is the format's one departure from its own philosophy, and comparing it to a WebAssembly custom section shows the cost. A custom section is name, length, contents — fully extensible, and adding a new kind costs nothing. The Module attribute is five positional sections with fixed order, so a new kind of module directive would require a new major version, because there is nowhere to put a sixth count. That is a real limitation, and it is the direct consequence of choosing positional layout for a feature that was still finding its shape. The format's own answer to that problem is the attribute mechanism, and this attribute chose not to use it.

And the uses/provides pairing is a cross-file correspondence resolved by name, which makes it the third instance in this course of that pattern: an ELF dynamic symbol matched by name across objects, a COFF weak external resolved by the linker, and now a Java service type matched across module files. Resolving by name is what lets independently-compiled things find each other, and the price is that a rename on one side silently breaks the match unless something checks both directions. Services add the uses half precisely so the consumer's intent is on the record too — which is the same two-sided declaration as the nest and the sealed hierarchy, and the same reason it exists.

One last connection, and it is the one that ties the module to the course's own architecture. The escape hatch exists so old readers survive new writers. invokedynamic exists so new writers need no new opcodes. Modules are the case where the format added something with no escape hatch at all — a positional attribute, a new class flag, and two new pool tags — which is what happens when a feature is big enough to be worth a version number. It is the right answer and it is also the only place in this course where the format's two strategies — additive and versioned — are both visible in one design, because CONSTANT_Module is additive while the attribute's layout is versioned.

Last: the version history, which is where all of this gets put in order — what each major version added, and the pattern behind thirty years of additions.