Skip to content

Memory leak in mockito-inline calling method on mock with at least a mock as parameter #1614

@ttanxu

Description

@ttanxu

Summary

We found a memory leak with mockito-inline. A short example is shown as below. A more detailed example can be found at the end.

class BigClass {
  void accept(SmallClass small) {}
}

class SmallClass {
  void accept(BigClass big) {}
}

void leak() {
  BigClass a = Mockito.mock(BigClass.class);
  SmallClass b = Mockito.mock(SmallClass.class);

  // Remove any one of the 2 lines below will stop leaking
  a.accept(b);
  b.accept(a);
}

Analysis

Looking at the memory dump. Mock of BigClass and SmallClass are held as a weak ref in a map of type WeakConcurrentMap$WithInlinedExpunction used to map the mock and its invocation handler. When the mock can be reclaimed by GC the map will remove the record from it.

In the case with memory leak, the mock instance of BigClass was held as a strong reference by rawArguments and arguments in InvocationMatcher, as invocation in InterceptedInvocation, as invocationForStubbing in InvocationContainerImpl, as invocationContainer in MockHandlerImpl. That eventually leads to a value in the WeakConcurrentMap for SmallClass. Similar thing happens to the mock instance of SmallClass. That creates a ring of reference and no mock can be reclaimed by GC because they are all referenced transitively by a value in the map.

Similar things can also happen for stubbed methods, saved in stubbed in InvocationContainerImpl.

Subclass mock makers don't suffer from it because there is no map from mock to handler -- it's just a strong reference. GC can handle non-accessible rings well, but GC doesn't know the mock map purging semantic in inline mock makers.

Potential Solution

Unlike #1533 where converting spiedInstance to a weak reference may be an acceptable solution, we can't convert arguments in stubbing method calls into weak references because there are stubbing calls with an object (or a mock) that doesn't have strong ref anywhere else than arguments in Mockito.

Therefore I failed to see a solution that's transparent to callers. The possible solution below is the one that I think has the least change.

The possible solution is we can somehow reset the mock when their lives end, which clears the stubbing records. Maybe we can tie their lives to a MockitoSession. We can track all mocks created after a session is created, and reset them when the session is finished. Now we only have events for mock creation in the same thread, we may need to expand that to include other threads.

We may be able to set spiedInstance to null when their lives end, which should also be able to fix #1532 and #1533. That would be a solution with least risk.

Of course any solution that's transparent to callers is still more desirable.

A Detailed Example

See GitHub project MockitoMethodCallMemLeak. One can open it in IntelliJ and the run configuration is already configured (with mem dump at OOM and 4M java heap size).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions