Remove usage of ObjectCleaner (#8064)

Motivation:

ObjectCleaner does start a Thread to handle the cleaning of resources which leaks into the users application. We should not use it in netty itself to make things more predictable.

Modifications:

- Remove usage of ObjectCleaner and use finalize as a replacement when possible.
- Clarify javadocs for FastThreadLocal.onRemoval(...) to ensure its clear that remove() is not guaranteed to be called when the Thread completees and so this method is not enough to guarantee cleanup for this case.

Result:

Fixes https://github.com/netty/netty/issues/8017.
This commit is contained in:
Norman Maurer 2018-06-28 08:15:27 +02:00 committed by GitHub
parent 2818730092
commit 5b1fe611a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 40 additions and 22 deletions

View File

@ -219,6 +219,16 @@ final class PoolThreadCache {
} }
} }
/// TODO: In the future when we move to Java9+ we should use java.lang.ref.Cleaner.
@Override
protected void finalize() throws Throwable {
try {
super.finalize();
} finally {
free();
}
}
/** /**
* Should be called if the Thread that uses this cache is about to exist to release resources out of the cache * Should be called if the Thread that uses this cache is about to exist to release resources out of the cache
*/ */

View File

@ -17,7 +17,6 @@
package io.netty.util; package io.netty.util;
import io.netty.util.concurrent.FastThreadLocal; import io.netty.util.concurrent.FastThreadLocal;
import io.netty.util.internal.ObjectCleaner;
import io.netty.util.internal.SystemPropertyUtil; import io.netty.util.internal.SystemPropertyUtil;
import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory; import io.netty.util.internal.logging.InternalLoggerFactory;
@ -244,10 +243,9 @@ public abstract class Recycler<T> {
Link next; Link next;
} }
// This act as a place holder for the head Link but also will be used by the ObjectCleaner // This act as a place holder for the head Link but also will reclaim space once finalized.
// to return space that was before reserved. Its important this does not hold any reference to // Its important this does not hold any reference to either Stack or WeakOrderQueue.
// either Stack or WeakOrderQueue. static final class Head {
static final class Head implements Runnable {
private final AtomicInteger availableSharedCapacity; private final AtomicInteger availableSharedCapacity;
Link link; Link link;
@ -256,12 +254,21 @@ public abstract class Recycler<T> {
this.availableSharedCapacity = availableSharedCapacity; this.availableSharedCapacity = availableSharedCapacity;
} }
/// TODO: In the future when we move to Java9+ we should use java.lang.ref.Cleaner.
@Override @Override
public void run() { protected void finalize() throws Throwable {
try {
super.finalize();
} finally {
Link head = link; Link head = link;
link = null;
while (head != null) { while (head != null) {
reclaimSpace(LINK_CAPACITY); reclaimSpace(LINK_CAPACITY);
head = head.next; Link next = head.next;
// Unlink to help GC and guard against GC nepotism.
head.next = null;
head = next;
}
} }
} }
@ -318,12 +325,6 @@ public abstract class Recycler<T> {
// may be accessed while its still constructed. // may be accessed while its still constructed.
stack.setHead(queue); stack.setHead(queue);
// We need to reclaim all space that was reserved by this WeakOrderQueue so we not run out of space in
// the stack. This is needed as we not have a good life-time control over the queue as it is used in a
// WeakHashMap which will drop it at any time.
final Head head = queue.head;
ObjectCleaner.register(queue, head);
return queue; return queue;
} }

View File

@ -16,7 +16,6 @@
package io.netty.util.concurrent; package io.netty.util.concurrent;
import io.netty.util.internal.InternalThreadLocalMap; import io.netty.util.internal.InternalThreadLocalMap;
import io.netty.util.internal.ObjectCleaner;
import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.PlatformDependent;
import java.util.Collections; import java.util.Collections;
@ -153,6 +152,8 @@ public class FastThreadLocal<V> {
threadLocalMap.setCleanerFlag(index); threadLocalMap.setCleanerFlag(index);
// TODO: We need to find a better way to handle this.
/*
// We will need to ensure we will trigger remove(InternalThreadLocalMap) so everything will be released // We will need to ensure we will trigger remove(InternalThreadLocalMap) so everything will be released
// and FastThreadLocal.onRemoval(...) will be called. // and FastThreadLocal.onRemoval(...) will be called.
ObjectCleaner.register(current, new Runnable() { ObjectCleaner.register(current, new Runnable() {
@ -164,6 +165,7 @@ public class FastThreadLocal<V> {
// the Thread is collected by GC. In this case the ThreadLocal will be gone away already. // the Thread is collected by GC. In this case the ThreadLocal will be gone away already.
} }
}); });
*/
} }
/** /**
@ -281,7 +283,9 @@ public class FastThreadLocal<V> {
} }
/** /**
* Invoked when this thread local variable is removed by {@link #remove()}. * Invoked when this thread local variable is removed by {@link #remove()}. Be aware that {@link #remove()}
* is not guaranteed to be called when the `Thread` completes which means you can not depend on this for
* cleanup of the resources in the case of `Thread` completion.
*/ */
protected void onRemoval(@SuppressWarnings("UnusedParameters") V value) throws Exception { } protected void onRemoval(@SuppressWarnings("UnusedParameters") V value) throws Exception { }
} }

View File

@ -18,6 +18,7 @@ package io.netty.util.concurrent;
import io.netty.util.internal.ObjectCleaner; import io.netty.util.internal.ObjectCleaner;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -96,13 +97,13 @@ public class FastThreadLocalTest {
thread.start(); thread.start();
thread.join(); thread.join();
assertEquals(1, ObjectCleaner.getLiveSetCount() - sizeWhenStart); assertEquals(0, ObjectCleaner.getLiveSetCount() - sizeWhenStart);
Thread thread2 = new Thread(runnable); Thread thread2 = new Thread(runnable);
thread2.start(); thread2.start();
thread2.join(); thread2.join();
assertEquals(2, ObjectCleaner.getLiveSetCount() - sizeWhenStart); assertEquals(0, ObjectCleaner.getLiveSetCount() - sizeWhenStart);
} }
@Test @Test
@ -128,13 +129,13 @@ public class FastThreadLocalTest {
thread.start(); thread.start();
thread.join(); thread.join();
assertEquals(2, ObjectCleaner.getLiveSetCount() - sizeWhenStart); assertEquals(0, ObjectCleaner.getLiveSetCount() - sizeWhenStart);
Thread thread2 = new Thread(runnable); Thread thread2 = new Thread(runnable);
thread2.start(); thread2.start();
thread2.join(); thread2.join();
assertEquals(4, ObjectCleaner.getLiveSetCount() - sizeWhenStart); assertEquals(0, ObjectCleaner.getLiveSetCount() - sizeWhenStart);
} }
@Test(timeout = 4000) @Test(timeout = 4000)
@ -142,6 +143,7 @@ public class FastThreadLocalTest {
testOnRemoveCalled(true, true); testOnRemoveCalled(true, true);
} }
@Ignore("onRemoval(...) not called with non FastThreadLocal")
@Test(timeout = 4000) @Test(timeout = 4000)
public void testOnRemoveCalledForNonFastThreadLocalGet() throws Exception { public void testOnRemoveCalledForNonFastThreadLocalGet() throws Exception {
testOnRemoveCalled(false, true); testOnRemoveCalled(false, true);
@ -152,6 +154,7 @@ public class FastThreadLocalTest {
testOnRemoveCalled(true, false); testOnRemoveCalled(true, false);
} }
@Ignore("onRemoval(...) not called with non FastThreadLocal")
@Test(timeout = 4000) @Test(timeout = 4000)
public void testOnRemoveCalledForNonFastThreadLocalSet() throws Exception { public void testOnRemoveCalledForNonFastThreadLocalSet() throws Exception {
testOnRemoveCalled(false, false); testOnRemoveCalled(false, false);