skip to content

A large Java test suite slows down and its heap usage climbs steadily after Mockito's instrumentation-based mock maker becomes the default. What costs does that mock maker introduce, and what would you do about them?

level: seniorimportance: nice to knowfreq 24%

answer

  1. retransform once per class per JVM
  2. forking discards the instrumentation cache
  3. invocation records hold arguments strongly
  4. Mockito.framework().clearInlineMocks()
  5. MockMakers.SUBCLASS for hot simple types

basics

~20 s

Instrumenting each newly mocked class costs time on first use, and Mockito keeps per-mock state including recorded invocations and their arguments, so long-running suites accumulate memory. Clear it with Mockito.framework().clearInlineMocks() after each test, null out fields, and use the subclass maker for hot simple types.

solid answer

~50 s

Two distinct costs. **Time.** The inline maker retransforms a class the first time it is mocked. Retransformation is comparatively expensive but happens once per class per JVM; after that mock creation is cheap. Suites that mock hundreds of distinct types in a short JVM feel it, and forking a JVM per test class multiplies the cost because the instrumentation cache dies with the process. **Memory.** Mockito keeps state for every mock — its stubbings and its recorded invocations, which hold **strong references to the arguments**. Mock instances themselves are tracked weakly, but a mock kept alive by a test-class field (JUnit keeps instances until the class finishes, and `@Mock` fields are not nulled) retains everything it ever received. Mitigations: `Mockito.framework().clearInlineMocks()` in an `@AfterEach`/`@AfterAll`; avoid `static` mock fields; keep `forkEvery` reasonable; and select `MockMakers.SUBCLASS` for hot, simple, non-final types.

code

java · 16 lines
java
abstract class MockitoMemoryHygiene {

    @AfterEach
    void clearMockitoState() {
        Mockito.framework().clearInlineMocks();
    }
}

@ExtendWith(MockitoExtension.class)
class OrderServiceTest extends MockitoMemoryHygiene {

    @Mock(mockMaker = MockMakers.SUBCLASS)   // hot, simple, non-final type
    Clock clock;

    // ...
}

go deeper

for a junior

Know that mocks retain the calls made on them and that clearing them between tests helps.

for a middle

Separate the one-off instrumentation cost per class from the ongoing retention of invocation data.

for a senior

Diagnose from GC logs or a heap dump, apply clearInlineMocks and lifetime hygiene, and use per-mock maker selection where profiling justifies it.

for a principal

Decide suite-wide policy — fork strategy, shared cleanup extension, Spring context caching — and weigh instrumentation cost against the capability it buys.

## Where the time goes The inline mock maker works by retransforming loaded classes so each method body starts with a dispatcher check. That retransformation happens **once per class per JVM**, on the first mock of that type. It involves the JVM's retransform machinery and ByteBuddy's bytecode work, which is heavier than the subclass maker's "generate a subclass" step for a single simple interface — but unlike the subclass maker it is amortised: subsequent mocks of the same type are cheap, whereas the subclass maker may generate a new class per distinct mock-settings combination. So the shape of the cost matters more than its size: - suites that mock **many distinct types** pay per type; - suites that mock **the same few types thousands of times** amortise well; - **JVM forking** (`forkEvery` in Gradle/Surefire, or one JVM per module) throws the instrumentation cache away repeatedly and re-pays everything. ## Where the memory goes Every Mockito mock has an invocation container: the list of calls made on it, each holding the actual argument objects, plus its stubbings and their answers. Those references are strong. So a mock retains every argument ever passed to it for as long as the mock is reachable. Under the inline maker there is additionally a global registry associating instrumented classes and mock state; mock instances are held weakly, but reachability is decided by *your* references. The classic leak shapes: - `@Mock` fields on a test class — JUnit 5 creates one instance per test method by default and discards it, which is fine, but `@Mock` fields on a `@TestInstance(PER_CLASS)` class, or mocks stored in `static` fields, live for the whole run; - mocks captured by long-lived caches or by a Spring test context (`@MockBean` mocks live as long as the cached context); - arguments that are themselves large object graphs, retained through the invocation record. The symptom is a heap that climbs across the suite and OOMs in the longest module, with a heap dump dominated by Mockito's internal maps and by your own DTOs held from invocation records. ## What to do **Clear inline mocks.** `Mockito.framework().clearInlineMocks()` drops the inline maker's retained mock state; there is also `clearInlineMock(mock)` for a single one. Called from an `@AfterEach` or `@AfterAll` (or a JUnit extension applied globally) it bounds the growth. Note it is only meaningful for the inline maker. **Let mocks die.** Prefer method-scoped mocks and per-method test instances over static or class-level ones; with Spring, be conscious that `@MockBean` mocks live with the context and that each distinct context configuration adds another cached context. **Reduce fork churn.** A fresh JVM per test class re-instruments everything; unless isolation demands it, fewer forks is faster. **Choose the maker per mock.** `mock(Simple.class, withSettings().mockMaker(MockMakers.SUBCLASS))` or `@Mock(mockMaker = MockMakers.SUBCLASS)` for non-final types mocked in extreme volume, keeping the inline maker as the classpath default for everything that needs it. **Measure before acting.** Turn on GC logging or take a heap dump at the end of the suite; confirm the retention path actually runs through Mockito before restructuring tests. Suite slowdowns are just as often caused by Spring context churn, and the fix for that is context-configuration hygiene, not the mock maker. ## The judgment part None of this argues against the inline maker as a default — it is the default precisely because "mocking final types works" is worth more to most projects than a few seconds of instrumentation. The senior answer is: know that the cost exists, know its two shapes, bound it with `clearInlineMocks` and mock lifetimes, and reach for per-mock maker selection only where profiling justifies it.

  • Why do recorded invocations, rather than the mocks themselves, dominate the retained heap?
    Each invocation record keeps the actual argument objects so that verification and argument captors can inspect them later, and those references are strong. If the arguments are large domain objects or collections, one long-lived mock can retain a substantial graph per call. The mock object is small; what it remembers is not.
  • When is Mockito.framework().clearInlineMocks() a no-op?
    When the active mock maker is not the inline one — the call only clears state held by the inline maker's registry. It also does nothing for objects that are still strongly referenced elsewhere in a way that keeps their own data alive outside Mockito. It is a bounding measure for suite-level growth, not a general memory fix.
  • How does JVM forking interact with instrumentation cost?
    Instrumentation results are cached per JVM, so a forked process starts cold and re-instruments every class it mocks. Configuring a fork per test class multiplies the one-off cost by the number of classes, which can dominate a large suite's runtime. Unless isolation genuinely requires it, keeping forks low is the cheapest available win.

saying these in an interview costs you the question

  • Claiming the inline maker instruments every class on every mock creation
  • Saying Mockito mocks are always garbage collected promptly regardless of test structure
  • Treating clearInlineMocks as a general memory fix, including under the subclass maker
  • Blaming the mock maker without profiling (often it is Spring context churn)
  • Recommending a switch back to the subclass maker for the whole build as the first response

context