Skip to content

Commit

Permalink
[ci maven-central-release] Merge pull request #2034 from mockito/expl…
Browse files Browse the repository at this point in the history
…icit-escape-during-dispatch

Escape mock during method dispatch on mock to avoid premature garbage collection.
  • Loading branch information
raphw committed Sep 3, 2020
2 parents fcd788c + 093d527 commit 893e2f4
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 8 deletions.
Expand Up @@ -6,6 +6,8 @@

import static org.mockito.internal.invocation.DefaultInvocationFactory.createInvocation;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.Method;
Expand Down Expand Up @@ -36,12 +38,19 @@ public class MockMethodInterceptor implements Serializable {

private final ByteBuddyCrossClassLoaderSerializationSupport serializationSupport;

private transient ThreadLocal<Object> weakReferenceHatch = new ThreadLocal<>();

public MockMethodInterceptor(MockHandler handler, MockCreationSettings mockCreationSettings) {
this.handler = handler;
this.mockCreationSettings = mockCreationSettings;
serializationSupport = new ByteBuddyCrossClassLoaderSerializationSupport();
}

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
weakReferenceHatch = new ThreadLocal<>();
}

Object doIntercept(Object mock, Method invokedMethod, Object[] arguments, RealMethod realMethod)
throws Throwable {
return doIntercept(mock, invokedMethod, arguments, realMethod, new LocationImpl());
Expand All @@ -54,14 +63,33 @@ Object doIntercept(
RealMethod realMethod,
Location location)
throws Throwable {
return handler.handle(
createInvocation(
mock,
invokedMethod,
arguments,
realMethod,
mockCreationSettings,
location));
// If the currently dispatched method is used in a hot path, typically a tight loop and if
// the mock is not used after the currently dispatched method, the JVM might attempt a
// garbage collection of the mock instance even before the execution of the current
// method is completed. Since we only reference the mock weakly from hereon after to avoid
// leaking the instance, it might therefore be garbage collected before the
// handler.handle(...) method completes. Since the handler method expects the mock to be
// present while a method call onto the mock is dispatched, this can lead to the problem
// described in GitHub #1802.
//
// To avoid this problem, we distract the JVM JIT by escaping the mock instance to a thread
// local field for the duration of the handler's dispatch.
//
// When dropping support for Java 8, instead of this hatch we should use an explicit fence
// https://docs.oracle.com/javase/9/docs/api/java/lang/ref/Reference.html#reachabilityFence-java.lang.Object-
weakReferenceHatch.set(mock);
try {
return handler.handle(
createInvocation(
mock,
invokedMethod,
arguments,
realMethod,
mockCreationSettings,
location));
} finally {
weakReferenceHatch.remove();
}
}

public MockHandler getMockHandler() {
Expand Down
43 changes: 43 additions & 0 deletions src/test/java/org/mockito/PrematureGarbageCollectionTest.java
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2020 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.mockito;

import org.junit.Test;

public class PrematureGarbageCollectionTest {

@Test
public void provoke_premature_garbage_collection() {
for (int i = 0; i < 500; i++) {
populateNodeList();
}
}

private static void populateNodeList() {
Node node = nodes();
while (node != null) {
Node next = node.next;
node.object.run();
node = next;
}
}

private static Node nodes() {
Node node = null;
for (int i = 0; i < 1_000; ++i) {
Node next = new Node();
next.next = node;
node = next;
}
return node;
}

private static class Node {

private Node next;

private final Runnable object = Mockito.mock(Runnable.class);
}
}

0 comments on commit 893e2f4

Please sign in to comment.