Running a Mockito 5 test suite on JDK 21 or newer prints a warning that a Java agent has been loaded dynamically and that this will be disallowed by default in a future release. Where does that come from and how do you resolve it?
answer
- inline maker needs Instrumentation → agent
- ByteBuddyAgent self-attach at runtime
- JDK 21 / JEP 451 warning
- fix: -javaagent:byte-buddy-agent.jar at startup
- silence: -XX:+EnableDynamicAgentLoading
basics
~20 sMockito's inline mock maker attaches ByteBuddy's agent to the running JVM at startup. JDK 21 warns about dynamic agent attachment because it is being phased out. Fix it by starting tests with -javaagent pointing at the byte-buddy-agent jar, or silence it with -XX:+EnableDynamicAgentLoading.
solid answer
~50 sThe inline mock maker needs the instrumentation API, so at first use ByteBuddy attaches its agent to the *already running* JVM through the attach API. Starting with JDK 21 (JEP 451, "Prepare to Disallow the Dynamic Loading of Agents"), the JVM prints a warning for that: agents loaded after startup can silently alter any code, which conflicts with integrity by default, and the JDK team has signalled it will eventually require explicit opt-in. Two resolutions: - **Proper fix:** load the agent at startup — add `-javaagent:<path-to-byte-buddy-agent.jar>` to the test JVM args. In Gradle you resolve the `net.bytebuddy:byte-buddy-agent` artifact into a configuration and add `jvmArgs` for the `Test` task; in Maven the same jar is passed via Surefire's `argLine`. - **Suppress:** `-XX:+EnableDynamicAgentLoading` silences the warning without changing the mechanism. Mocks work either way today; the warning is a forward-compatibility signal, not a failure.
code
java · 12 lines// build.gradle.kts
val mockitoAgent: Configuration by configurations.creating
dependencies {
testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
mockitoAgent("net.bytebuddy:byte-buddy-agent:1.15.11")
}
tasks.test {
useJUnitPlatform()
jvmArgs("-javaagent:${mockitoAgent.asPath}")
}go deeper
Recognise the warning as coming from Mockito's agent attaching at runtime, and that tests still pass.
Name the instrumentation requirement and the two flags, and know which jar supplies the agent.
Wire -javaagent into the Gradle or Maven test task and diagnose self-attach failures across environments.
Plan for disallow-by-default: audit which suites depend on instrumentation, and decide where subclass mocking or design change is the durable answer.
## Why an agent is involved at all The inline mock maker intercepts calls by *retransforming loaded classes* — injecting a dispatcher preamble into method bodies. Retransformation is only available through `java.lang.instrument.Instrumentation`, and you can only obtain an `Instrumentation` instance from a Java agent. Mockito does not ask you to launch the JVM with an agent; instead ByteBuddy's `ByteBuddyAgent.install()` uses the attach API to load `byte-buddy-agent.jar` into the JVM that is already running — self-attachment. That is invisible when it works, which is exactly what the JDK now objects to. ## What JDK 21 changed JEP 451, delivered in JDK 21, is titled *Prepare to Disallow the Dynamic Loading of Agents*. Its reasoning is the "integrity by default" line of work: an agent loaded into a live JVM can rewrite arbitrary code and break invariants the runtime and libraries rely on, and it can be attached by any process with sufficient privileges without the application's knowledge. Rather than break tooling immediately, JDK 21 emits a warning on each dynamic attach: ``` WARNING: A Java agent has been loaded dynamically (.../byte-buddy-agent-x.y.z.jar) WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning WARNING: Dynamic loading of agents will be disallowed by default in a future release ``` Starting the JVM with `-XX:+EnableDynamicAgentLoading` opts in explicitly and removes the warning; the eventual plan is for the default to become disallowed, at which point suites relying on self-attachment would fail rather than warn. Treat the warning as a deprecation notice with an unspecified deadline. ## The clean fix: load the agent at startup Mockito's own guidance is to pass the agent on the command line so nothing is attached dynamically. The jar is `net.bytebuddy:byte-buddy-agent`, which Mockito already brings transitively; the trick is getting its resolved path. Gradle: ```kotlin val mockitoAgent = configurations.create("mockitoAgent") dependencies { mockitoAgent("net.bytebuddy:byte-buddy-agent:1.15.11") } tasks.test { jvmArgs("-javaagent:${mockitoAgent.asPath}") } ``` Maven passes the same thing through Surefire's `argLine`, typically combined with `maven-dependency-plugin`'s `properties` goal so the local-repository path of the agent jar is available as a property. Either way the agent is present from process start, no attach happens, and the warning disappears for the right reason rather than being muted. ## Diagnosing related failures Several neighbouring symptoms come from the same area: - **"Could not self-attach to current VM"** — self-attachment blocked outright. Common on locked-down JVMs, some containers, and JDKs where the attach mechanism is restricted; also seen when the JDK (not just a JRE) tooling is unavailable. The startup `-javaagent` approach fixes it properly. - **Warnings appearing only in CI** — CI often runs a different JDK than developer machines; the JDK version, not the build, decides. - **Suites that must run without any agent** — Android and some GraalVM native-image test setups cannot instrument at all. There the answer is `mockito-subclass` (or the `mock-maker-subclass` resource) and accepting that final types are not mockable. ## How to talk about it The answer that lands is layered: *what* the mechanism is (instrumentation requires an agent; Mockito self-attaches), *why* the JDK complains (JEP 451, integrity by default, dynamic attach to be disallowed), *the correct fix* (`-javaagent` at startup, wired through the test task), and *the escape hatch* (`-XX:+EnableDynamicAgentLoading` to silence today, subclass maker where instrumentation is impossible). Saying "it's only a warning, ignore it" is the weak answer — it is a warning that names its own future as an error.
- Why is passing -javaagent at startup better than -XX:+EnableDynamicAgentLoading?The flag only suppresses the warning; the JVM still loads an agent dynamically, so the build stays exposed to the day that becomes disallowed. Passing -javaagent means the agent is present from process start, which is the mechanism the JDK intends to keep supporting. It is a real fix rather than muting the notice.
- A CI job fails with 'Could not self-attach to current VM' while the same tests pass locally. What do you check?The CI JVM and its attach permissions: some hardened runtimes, containers and JDK configurations block dynamic attachment entirely, and the runner's JDK may differ from the developer's. Wiring the byte-buddy agent into the test task with -javaagent removes the dependency on attachment altogether. If instrumentation is unavailable in that environment at all, the subclass mock maker is the fallback.
- Which Mockito features stop working if the agent cannot be loaded at all?Everything that depends on retransforming real classes: mocking final classes and final methods, static method mocking, and constructor mocking. Ordinary interface and non-final class mocking still works through the subclass maker. In practice you switch that environment to mockito-subclass and restructure any tests that relied on the instrumentation-only features.
saying these in an interview costs you the question
- Calling the warning a Mockito bug rather than a JDK policy change
- Claiming mocking is already broken on JDK 21 (it still works, with a warning)
- Treating -XX:+EnableDynamicAgentLoading as the recommended fix
- Not knowing which jar the agent comes from (byte-buddy-agent)
- Assuming the warning appears only for static mocking