skip to content

Walk through how jdeps, jmod, and jlink work together to ship a minimal runtime, and what breaks the chain.

level: seniorimportance: should knowfreq 38%

answer

  1. Pipeline: jdeps (analyze) -> jmod (package native-bearing module) -> jlink (link image)
  2. jdeps --generate-module-info bootstraps modularizing legacy JARs
  3. Breaker 1: non-modular classpath JARs aren't linkable
  4. Breaker 2: reflection/ServiceLoader invisible to static jdeps
  5. Fix: --add-modules, uses/provides + --bind-services, opens, runtime test

basics

~20 s

jdeps analyzes your code to find its dependencies and can draft a module descriptor; jmod packages a module (including native code) into a .jmod; jlink links your modules and their dependencies into a small, self-contained runtime image. The chain breaks mainly on non-modular libraries and on dependencies resolved by reflection.

solid answer

~50 s

The pipeline is analyze -> package -> link. jdeps reads bytecode to map your dependencies and, via --generate-module-info, drafts module-info files so non-modular code can become modular. jmod packages a module — including native libraries, commands, and config a JAR can't hold — into a .jmod consumed at link time. jlink takes the module path plus a root --add-modules set, resolves the transitive closure, and emits a trimmed runtime image (JVM + only needed modules). What breaks it: (1) plain classpath JARs aren't modules, so they must be modularized, treated as automatic modules, or skipped; (2) reflective access — Class.forName, ServiceLoader, frameworks — is invisible to jdeps's static analysis, so required modules can be missing from the image and fail at runtime. You compensate by adding modules explicitly (--add-modules), declaring requires/uses, and runtime-testing the image rather than trusting the static report.

go deeper

for a junior

Knows the three tools exist and roughly that one analyzes, one packages, and one builds the small runtime.

for a middle

Can describe the analyze->package->link flow and name the non-modular-JAR problem, and run jdeps then jlink for a simple modular app.

for a senior

Explains both failure modes (non-modular deps, reflection/services), applies fixes (modularize, --add-modules, uses/provides + --bind-services, opens) and insists on runtime-testing the image.

for a principal

Owns the migration/packaging strategy for a large dependency graph with native and reflective libraries, decides modularization-vs-classpath tradeoffs, and bakes static + runtime validation into the release pipeline.

## The end-to-end pipeline The three JPMS tools form a deliberate **analyze → package → link** pipeline. ### 1. jdeps — analyze `jdeps` statically reads your compiled bytecode and reports what packages/modules you depend on. Two pipeline-relevant uses: - `jdeps --jdk-internals app.jar`: confirm you aren't using soon-to-be-removed internal APIs. - `jdeps --generate-module-info out/ lib.jar`: emit a **draft `module-info.java`**, turning a legacy classpath JAR into a candidate module. You then refine it (tighten `exports`, add `requires transitive`, add `opens` for reflective access). ### 2. jmod — package For a module that needs more than classes — **native libraries** (`--libs`), **native commands** (`--cmds`), **config** (`--config`) — you use `jmod create` to produce a `.jmod`. A pure-bytecode module can stay a **modular JAR** instead. Either form is acceptable input to the next step; `.jmod` is link-time only and can't be run directly. ### 3. jlink — link `jlink --module-path <jdk-jmods + your-modules> --add-modules <roots> --output image` resolves the **transitive closure** from your root modules and assembles a self-contained runtime image: a trimmed JVM plus exactly those modules, optionally optimized with `--strip-debug`, `--compress`, `--launcher`, etc. ## Why the chain breaks — two classic failures ### Failure A: non-modular dependencies jlink links **modules**. A plain classpath JAR with no `module-info` is not a module. Options: - **Modularize it** (add a `module-info`, recompile). - Use it as an **automatic module** (a JAR on the module path gets a name derived from its filename/manifest) — but automatic modules `requires` the *entire* world and **cannot be jlinked** as-is in many setups, so this is a development convenience, not a clean link input. - Generate a descriptor with `jdeps --generate-module-info` and recompile. If you skip this, jlink can't include the dependency and resolution fails. ### Failure B: reflection and dynamic resolution jdeps sees only **static, compile-baked references**. Anything resolved at runtime is invisible: - `Class.forName("...")` and reflective instantiation. - **ServiceLoader** / the `provides`/`uses` mechanism (JDBC drivers, logging providers). - Framework-driven proxies (Spring, Hibernate) and JSON binders that reflect over your types. Consequences: a module that's only reached reflectively may be **omitted from the image** (it never showed up in jdeps), or it's present but the access fails with `InaccessibleObjectException` because the package wasn't `opens`-ed. Compensations: - Add the module explicitly with `--add-modules` at link time. - Declare `uses`/`provides` for services so `--bind-services` (or default service binding) pulls providers in. - Add `opens`/`open module` for reflective frameworks. - **Runtime-test the linked image** — a clean static analysis never proves runtime correctness. ## The mental model jdeps answers *"what do I depend on, and is any of it dangerous (internal APIs)?"*; jmod answers *"how do I package this module including its native bits?"*; jlink answers *"give me the smallest runtime that can run exactly this."* The two things that defeat the pipeline are **un-modularized code** and **dependencies the compiler never recorded** — both stem from the same root cause: static tooling can only act on what is statically present.

  • A service provider (e.g. a JDBC driver) is missing from your jlink image. Why, and how do you fix it?
    It's pulled in via ServiceLoader, which is dynamic and invisible to static resolution. Declare uses/provides in the module descriptors and enable service binding (--bind-services or add the provider with --add-modules) so jlink includes it.
  • Why are automatic modules a poor input to jlink?
    An automatic module has no real descriptor: it reads every other module and exports everything, so it can't be reliably resolved/linked. jlink generally rejects/struggles with them — modularize properly first.
  • What single root cause explains both pipeline-breaking failures?
    Static tooling can only act on what is statically present in bytecode/descriptors: un-modularized code has no descriptor to link, and reflective dependencies were never recorded at compile time.

saying these in an interview costs you the question

  • Assuming a clean jdeps run guarantees the jlink image runs correctly
  • Trying to jlink automatic modules / plain classpath JARs without modularizing
  • Forgetting that reflective/service dependencies must be added explicitly
  • Mixing up which tool does what (analysis vs packaging vs linking)

context