/* * Copyright 2013 The Netty Project * * The Netty Project licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package io.netty.util; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.SystemPropertyUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; public final class ResourceLeakDetector { private static final boolean DISABLED = SystemPropertyUtil.getBoolean("io.netty.noResourceLeakDetection", false); private static final InternalLogger logger = InternalLoggerFactory.getInstance(ResourceLeakDetector.class); static { logger.debug("io.netty.noResourceLeakDetection: {}", DISABLED); } private static final int DEFAULT_SAMPLING_INTERVAL = 113; private static final ResourceLeak NOOP = new ResourceLeak() { @Override public boolean close() { return false; } }; /** the linked list of active resources */ private final DefaultResourceLeak head = new DefaultResourceLeak(null); private final DefaultResourceLeak tail = new DefaultResourceLeak(null); private final ReferenceQueue refQueue = new ReferenceQueue(); private final ConcurrentMap reportedLeaks = PlatformDependent.newConcurrentHashMap(); private final String resourceType; private final int samplingInterval; private final long maxActive; private long active; private final AtomicBoolean loggedTooManyActive = new AtomicBoolean(); private long leakCheckCnt; public ResourceLeakDetector(Class resourceType) { this(resourceType.getSimpleName()); } public ResourceLeakDetector(String resourceType) { this(resourceType, DEFAULT_SAMPLING_INTERVAL, Long.MAX_VALUE); } public ResourceLeakDetector(Class resourceType, int samplingInterval, long maxActive) { this(resourceType.getSimpleName(), samplingInterval, maxActive); } public ResourceLeakDetector(String resourceType, int samplingInterval, long maxActive) { if (resourceType == null) { throw new NullPointerException("resourceType"); } if (samplingInterval <= 0) { throw new IllegalArgumentException("samplingInterval: " + samplingInterval + " (expected: 1+)"); } if (maxActive <= 0) { throw new IllegalArgumentException("maxActive: " + maxActive + " (expected: 1+)"); } this.resourceType = resourceType; this.samplingInterval = samplingInterval; this.maxActive = maxActive; head.next = tail; tail.prev = head; } public ResourceLeak open(T obj) { if (DISABLED || leakCheckCnt ++ % samplingInterval != 0) { return NOOP; } reportLeak(); return new DefaultResourceLeak(obj); } private void reportLeak() { if (!logger.isWarnEnabled()) { for (;;) { @SuppressWarnings("unchecked") DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll(); if (ref == null) { break; } ref.close(); } return; } // Report too many instances. if (active * samplingInterval > maxActive && loggedTooManyActive.compareAndSet(false, true)) { logger.warn( "LEAK: You are creating too many " + resourceType + " instances. " + resourceType + " is a shared resource that must be reused across the JVM," + "so that only a few instances are created."); } // Detect and report previous leaks. for (;;) { @SuppressWarnings("unchecked") DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll(); if (ref == null) { break; } ref.clear(); if (!ref.close()) { continue; } if (reportedLeaks.putIfAbsent(ref.exception, Boolean.TRUE) == null) { logger.warn("LEAK: " + resourceType + " was GC'd before being released correctly.", ref.exception); } } } private final class DefaultResourceLeak extends PhantomReference implements ResourceLeak { private final ResourceLeakException exception; private final AtomicBoolean freed; private DefaultResourceLeak prev; private DefaultResourceLeak next; public DefaultResourceLeak(Object referent) { super(referent, referent != null? refQueue : null); if (referent != null) { exception = new ResourceLeakException( referent.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(referent))); // TODO: Use CAS to update the list. synchronized (head) { prev = head; next = head.next; head.next.prev = this; head.next = this; active ++; } freed = new AtomicBoolean(); } else { exception = null; freed = new AtomicBoolean(true); } } @Override public boolean close() { if (freed.compareAndSet(false, true)) { synchronized (head) { active --; prev.next = next; next.prev = prev; prev = null; next = null; } return true; } return false; } } }