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).
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.
Analysis
Looking at the memory dump. Mock of
BigClassandSmallClassare held as a weak ref in a map of typeWeakConcurrentMap$WithInlinedExpunctionused 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
BigClasswas held as a strong reference byrawArgumentsandargumentsinInvocationMatcher, asinvocationinInterceptedInvocation, asinvocationForStubbinginInvocationContainerImpl, asinvocationContainerinMockHandlerImpl. That eventually leads to a value in theWeakConcurrentMapforSmallClass. Similar thing happens to the mock instance ofSmallClass. 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
stubbedinInvocationContainerImpl.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
spiedInstanceto 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
spiedInstancetonullwhen 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).