Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f464f7a

Browse files
committed
Fixes #1614: Add API to clean up mocks.
Due to the introduction of map from weak reference from mock instance to its invocation handler, Mockito became vunerable to memory leaks as there are multiple situations where Mockito could unintentionally hold strong references to mock instances in the map record. The strong references could be through spiedInstance for spies, and arguments used to faciliate method stubbing. Mockito could never know if the arguments passed in for method stubbing are also strongly referenced somewhere else or not, so Mockito needs to save a strong reference to these arguments to avoid premature GC. Therefore to solve cyclic strong references through arguments Mockito needs to explicitly know when mocks are out of their life, and clean up all internal strong references associated with them. This commit also fixes #1532 and fixes #1533.
1 parent f11705b commit f464f7a

File tree

9 files changed

+201
-1
lines changed

9 files changed

+201
-1
lines changed

src/main/java/org/mockito/MockitoFramework.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,14 @@ public interface MockitoFramework {
9292
*/
9393
@Incubating
9494
InvocationFactory getInvocationFactory();
95+
96+
/**
97+
* Clears up all existing mocks. This is useful when testing with {@link org.mockito.plugins.InlineMockMaker} which
98+
* may hold references to mocks and leak memory. No interaction to any mock created previously is not allowed after
99+
* calling this method.
100+
*
101+
* @since 2.24.8
102+
*/
103+
@Incubating
104+
void clearAllMocks();
95105
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (c) 2019 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
6+
package org.mockito.exceptions.misusing;
7+
8+
import org.mockito.MockitoSession;
9+
import org.mockito.exceptions.base.MockitoException;
10+
11+
/**
12+
* Reports the misuse where user tries to open more than one {@link MockitoSession} that tracks and cleans up mocks.
13+
* User needs to finish previous session that tracks and cleans up mocks before trying to open a new one. Note this
14+
* doesn't prevent user from opening sessions that doesn't track or clean up mocks.
15+
*
16+
* @since 2.24.4
17+
*/
18+
public class MultipleTrackingMockSessionException extends MockitoException {
19+
public MultipleTrackingMockSessionException(String message) {
20+
super(message);
21+
}
22+
}

src/main/java/org/mockito/internal/creation/bytebuddy/InlineByteBuddyMockMaker.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.mockito.internal.util.concurrent.WeakConcurrentMap;
1515
import org.mockito.invocation.MockHandler;
1616
import org.mockito.mock.MockCreationSettings;
17+
import org.mockito.plugins.InlineMockMaker;
1718

1819
import java.io.File;
1920
import java.io.FileOutputStream;
@@ -89,7 +90,7 @@
8990
* support this feature.
9091
*/
9192
@Incubating
92-
public class InlineByteBuddyMockMaker implements ClassCreatingMockMaker {
93+
public class InlineByteBuddyMockMaker implements ClassCreatingMockMaker, InlineMockMaker {
9394

9495
private static final Instrumentation INSTRUMENTATION;
9596

@@ -271,6 +272,15 @@ public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings
271272
}
272273
}
273274

275+
@Override
276+
public void cleanUpMock(Object mock) {
277+
if (mock == null) {
278+
mocks.clear();
279+
} else {
280+
mocks.remove(mock);
281+
}
282+
}
283+
274284
@Override
275285
public TypeMockability isTypeMockable(final Class<?> type) {
276286
return new TypeMockability() {

src/main/java/org/mockito/internal/framework/DefaultMockitoFramework.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import org.mockito.internal.util.Checks;
1111
import org.mockito.invocation.InvocationFactory;
1212
import org.mockito.listeners.MockitoListener;
13+
import org.mockito.plugins.InlineMockMaker;
14+
import org.mockito.plugins.MockMaker;
1315
import org.mockito.plugins.MockitoPlugins;
1416

1517
import static org.mockito.internal.progress.ThreadSafeMockingProgress.mockingProgress;
@@ -37,4 +39,16 @@ public MockitoPlugins getPlugins() {
3739
public InvocationFactory getInvocationFactory() {
3840
return new DefaultInvocationFactory();
3941
}
42+
43+
@Override
44+
public void clearAllMocks() {
45+
MockMaker mockMaker = Plugins.getMockMaker();
46+
47+
if (!(mockMaker instanceof InlineMockMaker)) {
48+
return;
49+
}
50+
51+
InlineMockMaker inlineMockMaker = (InlineMockMaker) mockMaker;
52+
inlineMockMaker.cleanUpMock(null);
53+
}
4054
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2019 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
6+
package org.mockito.plugins;
7+
8+
import org.mockito.Incubating;
9+
10+
/**
11+
* Extension to {@link MockMaker} for mock makers that changes inline method implementations.
12+
* @since 2.24.8
13+
*/
14+
@Incubating
15+
public interface InlineMockMaker extends MockMaker {
16+
/**
17+
* Cleans up internal state for specified {@code mock}. You may assume there won't be any interaction to this {@code mock}
18+
* after this is called.
19+
*
20+
* @param mock the mock instance whose internal state is to be cleaned, or {@code null} to clean all existing mocks.
21+
* @since 2.24.8
22+
*/
23+
@Incubating
24+
void cleanUpMock(Object mock);
25+
}

src/test/java/org/mockito/internal/creation/bytebuddy/InlineByteBuddyMockMakerTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,31 @@ public void test_parameters_retention() throws Exception {
287287
.getOnly().getParameters().getOnly().getName()).isEqualTo("bar");
288288
}
289289

290+
@Test
291+
public void test_cleanup_mock_clears_handler() {
292+
MockCreationSettings<GenericSubClass> settings = settingsFor(GenericSubClass.class);
293+
GenericSubClass proxy = mockMaker.createMock(settings, new MockHandlerImpl<GenericSubClass>(settings));
294+
assertThat(mockMaker.getHandler(proxy)).isNotNull();
295+
mockMaker.cleanUpMock(proxy);
296+
assertThat(mockMaker.getHandler(proxy)).isNull();
297+
}
298+
299+
@Test
300+
public void test_cleanup_all_mock_clears_handler() {
301+
MockCreationSettings<GenericSubClass> settings = settingsFor(GenericSubClass.class);
302+
GenericSubClass proxy1 = mockMaker.createMock(settings, new MockHandlerImpl<GenericSubClass>(settings));
303+
assertThat(mockMaker.getHandler(proxy1)).isNotNull();
304+
305+
settings = settingsFor(GenericSubClass.class);
306+
GenericSubClass proxy2 = mockMaker.createMock(settings, new MockHandlerImpl<GenericSubClass>(settings));
307+
assertThat(mockMaker.getHandler(proxy1)).isNotNull();
308+
309+
mockMaker.cleanUpMock(null);
310+
311+
assertThat(mockMaker.getHandler(proxy1)).isNull();
312+
assertThat(mockMaker.getHandler(proxy2)).isNull();
313+
}
314+
290315
private static <T> MockCreationSettings<T> settingsFor(Class<T> type, Class<?>... extraInterfaces) {
291316
MockSettingsImpl<T> mockSettings = new MockSettingsImpl<T>();
292317
mockSettings.setTypeToMock(type);

src/test/java/org/mockito/internal/framework/DefaultMockitoFrameworkTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,18 @@
1010
import org.mockito.MockSettings;
1111
import org.mockito.StateMaster;
1212
import org.mockito.exceptions.misusing.RedundantListenerException;
13+
import org.mockito.internal.configuration.plugins.Plugins;
1314
import org.mockito.listeners.MockCreationListener;
1415
import org.mockito.listeners.MockitoListener;
1516
import org.mockito.mock.MockCreationSettings;
17+
import org.mockito.plugins.InlineMockMaker;
1618
import org.mockitoutil.TestBase;
1719

1820
import java.util.List;
1921
import java.util.Set;
2022

23+
import static org.junit.Assert.assertFalse;
24+
import static org.junit.Assert.assertTrue;
2125
import static org.mockito.Mockito.*;
2226
import static org.mockitoutil.ThrowableAssert.assertThat;
2327

@@ -112,5 +116,17 @@ public void run() {
112116
"For more information, see the javadoc for RedundantListenerException class.");
113117
}
114118

119+
@Test
120+
public void clears_all_mocks() {
121+
List list = mock(List.class);
122+
assertTrue(mockingDetails(list).isMock());
123+
124+
framework.clearAllMocks();
125+
126+
if (Plugins.getMockMaker() instanceof InlineMockMaker) {
127+
assertFalse(mockingDetails(list).isMock());
128+
}
129+
}
130+
115131
private static class MyListener implements MockitoListener {}
116132
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2019 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
6+
package org.mockitoinline.bugs;
7+
8+
import org.junit.Test;
9+
import org.mockito.MockitoSession;
10+
11+
import static org.mockito.Mockito.framework;
12+
import static org.mockito.Mockito.mock;
13+
import static org.mockito.Mockito.mockitoSession;
14+
15+
public class CyclicMockMethodArgumentMemoryLeakTest {
16+
private static final int ARRAY_LENGTH = 1 << 20; // 4 MB
17+
18+
@Test
19+
public void no_memory_leak_when_cyclically_calling_method_with_mocks() {
20+
for (int i = 0; i < 100; ++i) {
21+
final A a = mock(A.class);
22+
a.largeArray = new int[ARRAY_LENGTH];
23+
final B b = mock(B.class);
24+
25+
a.accept(b);
26+
b.accept(a);
27+
28+
framework().clearAllMocks();
29+
}
30+
}
31+
32+
private static class A {
33+
private int[] largeArray;
34+
35+
void accept(B b) {}
36+
}
37+
38+
private static class B {
39+
void accept(A a) {}
40+
}
41+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2019 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
6+
package org.mockitoinline.bugs;
7+
8+
import org.junit.Test;
9+
import org.mockito.MockitoSession;
10+
11+
import static org.mockito.Mockito.framework;
12+
import static org.mockito.Mockito.mockitoSession;
13+
import static org.mockito.Mockito.spy;
14+
15+
public class SelfSpyReferenceMemoryLeakTest {
16+
private static final int ARRAY_LENGTH = 1 << 20; // 4 MB
17+
18+
@Test
19+
public void no_memory_leak_when_spy_holds_reference_to_self() {
20+
for (int i = 0; i < 100; ++i) {
21+
final DeepRefSelfClass instance = spy(new DeepRefSelfClass());
22+
instance.refInstance(instance);
23+
24+
framework().clearAllMocks();
25+
}
26+
}
27+
28+
private static class DeepRefSelfClass {
29+
private final DeepRefSelfClass[] array = new DeepRefSelfClass[1];
30+
31+
private final int[] largeArray = new int[ARRAY_LENGTH];
32+
33+
private void refInstance(DeepRefSelfClass instance) {
34+
array[0] = instance;
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)