View Javadoc
1   /*
2    * Copyright 2013 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package io.netty.util;
18  
19  import io.netty.util.internal.EmptyArrays;
20  import io.netty.util.internal.PlatformDependent;
21  import io.netty.util.internal.SystemPropertyUtil;
22  import io.netty.util.internal.logging.InternalLogger;
23  import io.netty.util.internal.logging.InternalLoggerFactory;
24  
25  import java.lang.ref.WeakReference;
26  import java.lang.ref.ReferenceQueue;
27  import java.lang.reflect.Method;
28  import java.util.Arrays;
29  import java.util.Collections;
30  import java.util.HashSet;
31  import java.util.Set;
32  import java.util.concurrent.ConcurrentHashMap;
33  import java.util.concurrent.ConcurrentMap;
34  import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
35  import java.util.concurrent.atomic.AtomicReference;
36  import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
37  
38  import static io.netty.util.internal.StringUtil.EMPTY_STRING;
39  import static io.netty.util.internal.StringUtil.NEWLINE;
40  import static io.netty.util.internal.StringUtil.simpleClassName;
41  
42  public class ResourceLeakDetector<T> {
43  
44      private static final String PROP_LEVEL_OLD = "io.netty.leakDetectionLevel";
45      private static final String PROP_LEVEL = "io.netty.leakDetection.level";
46      private static final Level DEFAULT_LEVEL = Level.SIMPLE;
47  
48      private static final String PROP_TARGET_RECORDS = "io.netty.leakDetection.targetRecords";
49      private static final int DEFAULT_TARGET_RECORDS = 4;
50  
51      private static final String PROP_SAMPLING_INTERVAL = "io.netty.leakDetection.samplingInterval";
52      // There is a minor performance benefit in TLR if this is a power of 2.
53      private static final int DEFAULT_SAMPLING_INTERVAL = 128;
54  
55      private static final int TARGET_RECORDS;
56      static final int SAMPLING_INTERVAL;
57  
58      /**
59       * Represents the level of resource leak detection.
60       */
61      public enum Level {
62          /**
63           * Disables resource leak detection.
64           */
65          DISABLED,
66          /**
67           * Enables simplistic sampling resource leak detection which reports there is a leak or not,
68           * at the cost of small overhead (default).
69           */
70          SIMPLE,
71          /**
72           * Enables advanced sampling resource leak detection which reports where the leaked object was accessed
73           * recently at the cost of high overhead.
74           */
75          ADVANCED,
76          /**
77           * Enables paranoid resource leak detection which reports where the leaked object was accessed recently,
78           * at the cost of the highest possible overhead (for testing purposes only).
79           */
80          PARANOID;
81  
82          /**
83           * Returns level based on string value. Accepts also string that represents ordinal number of enum.
84           *
85           * @param levelStr - level string : DISABLED, SIMPLE, ADVANCED, PARANOID. Ignores case.
86           * @return corresponding level or SIMPLE level in case of no match.
87           */
88          static Level parseLevel(String levelStr) {
89              String trimmedLevelStr = levelStr.trim();
90              for (Level l : values()) {
91                  if (trimmedLevelStr.equalsIgnoreCase(l.name()) || trimmedLevelStr.equals(String.valueOf(l.ordinal()))) {
92                      return l;
93                  }
94              }
95              return DEFAULT_LEVEL;
96          }
97      }
98  
99      private static Level level;
100 
101     private static final InternalLogger logger = InternalLoggerFactory.getInstance(ResourceLeakDetector.class);
102 
103     static {
104         final boolean disabled;
105         if (SystemPropertyUtil.get("io.netty.noResourceLeakDetection") != null) {
106             disabled = SystemPropertyUtil.getBoolean("io.netty.noResourceLeakDetection", false);
107             logger.debug("-Dio.netty.noResourceLeakDetection: {}", disabled);
108             logger.warn(
109                     "-Dio.netty.noResourceLeakDetection is deprecated. Use '-D{}={}' instead.",
110                     PROP_LEVEL, DEFAULT_LEVEL.name().toLowerCase());
111         } else {
112             disabled = false;
113         }
114 
115         Level defaultLevel = disabled? Level.DISABLED : DEFAULT_LEVEL;
116 
117         // First read old property name
118         String levelStr = SystemPropertyUtil.get(PROP_LEVEL_OLD, defaultLevel.name());
119 
120         // If new property name is present, use it
121         levelStr = SystemPropertyUtil.get(PROP_LEVEL, levelStr);
122         Level level = Level.parseLevel(levelStr);
123 
124         TARGET_RECORDS = SystemPropertyUtil.getInt(PROP_TARGET_RECORDS, DEFAULT_TARGET_RECORDS);
125         SAMPLING_INTERVAL = SystemPropertyUtil.getInt(PROP_SAMPLING_INTERVAL, DEFAULT_SAMPLING_INTERVAL);
126 
127         ResourceLeakDetector.level = level;
128         if (logger.isDebugEnabled()) {
129             logger.debug("-D{}: {}", PROP_LEVEL, level.name().toLowerCase());
130             logger.debug("-D{}: {}", PROP_TARGET_RECORDS, TARGET_RECORDS);
131         }
132     }
133 
134     /**
135      * @deprecated Use {@link #setLevel(Level)} instead.
136      */
137     @Deprecated
138     public static void setEnabled(boolean enabled) {
139         setLevel(enabled? Level.SIMPLE : Level.DISABLED);
140     }
141 
142     /**
143      * Returns {@code true} if resource leak detection is enabled.
144      */
145     public static boolean isEnabled() {
146         return getLevel().ordinal() > Level.DISABLED.ordinal();
147     }
148 
149     /**
150      * Sets the resource leak detection level.
151      */
152     public static void setLevel(Level level) {
153         if (level == null) {
154             throw new NullPointerException("level");
155         }
156         ResourceLeakDetector.level = level;
157     }
158 
159     /**
160      * Returns the current resource leak detection level.
161      */
162     public static Level getLevel() {
163         return level;
164     }
165 
166     /** the collection of active resources */
167     private final Set<DefaultResourceLeak<?>> allLeaks =
168             Collections.newSetFromMap(new ConcurrentHashMap<DefaultResourceLeak<?>, Boolean>());
169 
170     private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
171     private final ConcurrentMap<String, Boolean> reportedLeaks = PlatformDependent.newConcurrentHashMap();
172 
173     private final String resourceType;
174     private final int samplingInterval;
175 
176     /**
177      * @deprecated use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}.
178      */
179     @Deprecated
180     public ResourceLeakDetector(Class<?> resourceType) {
181         this(simpleClassName(resourceType));
182     }
183 
184     /**
185      * @deprecated use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}.
186      */
187     @Deprecated
188     public ResourceLeakDetector(String resourceType) {
189         this(resourceType, DEFAULT_SAMPLING_INTERVAL, Long.MAX_VALUE);
190     }
191 
192     /**
193      * @deprecated Use {@link ResourceLeakDetector#ResourceLeakDetector(Class, int)}.
194      * <p>
195      * This should not be used directly by users of {@link ResourceLeakDetector}.
196      * Please use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class)}
197      * or {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}
198      *
199      * @param maxActive This is deprecated and will be ignored.
200      */
201     @Deprecated
202     public ResourceLeakDetector(Class<?> resourceType, int samplingInterval, long maxActive) {
203         this(resourceType, samplingInterval);
204     }
205 
206     /**
207      * This should not be used directly by users of {@link ResourceLeakDetector}.
208      * Please use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class)}
209      * or {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}
210      */
211     @SuppressWarnings("deprecation")
212     public ResourceLeakDetector(Class<?> resourceType, int samplingInterval) {
213         this(simpleClassName(resourceType), samplingInterval, Long.MAX_VALUE);
214     }
215 
216     /**
217      * @deprecated use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}.
218      * <p>
219      * @param maxActive This is deprecated and will be ignored.
220      */
221     @Deprecated
222     public ResourceLeakDetector(String resourceType, int samplingInterval, long maxActive) {
223         if (resourceType == null) {
224             throw new NullPointerException("resourceType");
225         }
226 
227         this.resourceType = resourceType;
228         this.samplingInterval = samplingInterval;
229     }
230 
231     /**
232      * Creates a new {@link ResourceLeak} which is expected to be closed via {@link ResourceLeak#close()} when the
233      * related resource is deallocated.
234      *
235      * @return the {@link ResourceLeak} or {@code null}
236      * @deprecated use {@link #track(Object)}
237      */
238     @Deprecated
239     public final ResourceLeak open(T obj) {
240         return track0(obj);
241     }
242 
243     /**
244      * Creates a new {@link ResourceLeakTracker} which is expected to be closed via
245      * {@link ResourceLeakTracker#close(Object)} when the related resource is deallocated.
246      *
247      * @return the {@link ResourceLeakTracker} or {@code null}
248      */
249     @SuppressWarnings("unchecked")
250     public final ResourceLeakTracker<T> track(T obj) {
251         return track0(obj);
252     }
253 
254     @SuppressWarnings("unchecked")
255     private DefaultResourceLeak track0(T obj) {
256         Level level = ResourceLeakDetector.level;
257         if (level == Level.DISABLED) {
258             return null;
259         }
260 
261         if (level.ordinal() < Level.PARANOID.ordinal()) {
262             if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) {
263                 reportLeak();
264                 return new DefaultResourceLeak(obj, refQueue, allLeaks);
265             }
266             return null;
267         }
268         reportLeak();
269         return new DefaultResourceLeak(obj, refQueue, allLeaks);
270     }
271 
272     private void clearRefQueue() {
273         for (;;) {
274             @SuppressWarnings("unchecked")
275             DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
276             if (ref == null) {
277                 break;
278             }
279             ref.dispose();
280         }
281     }
282 
283     private void reportLeak() {
284         if (!logger.isErrorEnabled()) {
285             clearRefQueue();
286             return;
287         }
288 
289         // Detect and report previous leaks.
290         for (;;) {
291             @SuppressWarnings("unchecked")
292             DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
293             if (ref == null) {
294                 break;
295             }
296 
297             if (!ref.dispose()) {
298                 continue;
299             }
300 
301             String records = ref.toString();
302             if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
303                 if (records.isEmpty()) {
304                     reportUntracedLeak(resourceType);
305                 } else {
306                     reportTracedLeak(resourceType, records);
307                 }
308             }
309         }
310     }
311 
312     /**
313      * This method is called when a traced leak is detected. It can be overridden for tracking how many times leaks
314      * have been detected.
315      */
316     protected void reportTracedLeak(String resourceType, String records) {
317         logger.error(
318                 "LEAK: {}.release() was not called before it's garbage-collected. " +
319                 "See http://netty.io/wiki/reference-counted-objects.html for more information.{}",
320                 resourceType, records);
321     }
322 
323     /**
324      * This method is called when an untraced leak is detected. It can be overridden for tracking how many times leaks
325      * have been detected.
326      */
327     protected void reportUntracedLeak(String resourceType) {
328         logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
329                 "Enable advanced leak reporting to find out where the leak occurred. " +
330                 "To enable advanced leak reporting, " +
331                 "specify the JVM option '-D{}={}' or call {}.setLevel() " +
332                 "See http://netty.io/wiki/reference-counted-objects.html for more information.",
333                 resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
334     }
335 
336     /**
337      * @deprecated This method will no longer be invoked by {@link ResourceLeakDetector}.
338      */
339     @Deprecated
340     protected void reportInstancesLeak(String resourceType) {
341     }
342 
343     @SuppressWarnings("deprecation")
344     private static final class DefaultResourceLeak<T>
345             extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
346 
347         @SuppressWarnings("unchecked") // generics and updaters do not mix.
348         private static final AtomicReferenceFieldUpdater<DefaultResourceLeak<?>, Record> headUpdater =
349                 (AtomicReferenceFieldUpdater)
350                         AtomicReferenceFieldUpdater.newUpdater(DefaultResourceLeak.class, Record.class, "head");
351 
352         @SuppressWarnings("unchecked") // generics and updaters do not mix.
353         private static final AtomicIntegerFieldUpdater<DefaultResourceLeak<?>> droppedRecordsUpdater =
354                 (AtomicIntegerFieldUpdater)
355                         AtomicIntegerFieldUpdater.newUpdater(DefaultResourceLeak.class, "droppedRecords");
356 
357         @SuppressWarnings("unused")
358         private volatile Record head;
359         @SuppressWarnings("unused")
360         private volatile int droppedRecords;
361 
362         private final Set<DefaultResourceLeak<?>> allLeaks;
363         private final int trackedHash;
364 
365         DefaultResourceLeak(
366                 Object referent,
367                 ReferenceQueue<Object> refQueue,
368                 Set<DefaultResourceLeak<?>> allLeaks) {
369             super(referent, refQueue);
370 
371             assert referent != null;
372 
373             // Store the hash of the tracked object to later assert it in the close(...) method.
374             // It's important that we not store a reference to the referent as this would disallow it from
375             // be collected via the WeakReference.
376             trackedHash = System.identityHashCode(referent);
377             allLeaks.add(this);
378             // Create a new Record so we always have the creation stacktrace included.
379             headUpdater.set(this, new Record(Record.BOTTOM));
380             this.allLeaks = allLeaks;
381         }
382 
383         @Override
384         public void record() {
385             record0(null);
386         }
387 
388         @Override
389         public void record(Object hint) {
390             record0(hint);
391         }
392 
393         /**
394          * This method works by exponentially backing off as more records are present in the stack. Each record has a
395          * 1 / 2^n chance of dropping the top most record and replacing it with itself. This has a number of convenient
396          * properties:
397          *
398          * <ol>
399          * <li>  The current record is always recorded. This is due to the compare and swap dropping the top most
400          *       record, rather than the to-be-pushed record.
401          * <li>  The very last access will always be recorded. This comes as a property of 1.
402          * <li>  It is possible to retain more records than the target, based upon the probability distribution.
403          * <li>  It is easy to keep a precise record of the number of elements in the stack, since each element has to
404          *     know how tall the stack is.
405          * </ol>
406          *
407          * In this particular implementation, there are also some advantages. A thread local random is used to decide
408          * if something should be recorded. This means that if there is a deterministic access pattern, it is now
409          * possible to see what other accesses occur, rather than always dropping them. Second, after
410          * {@link #TARGET_RECORDS} accesses, backoff occurs. This matches typical access patterns,
411          * where there are either a high number of accesses (i.e. a cached buffer), or low (an ephemeral buffer), but
412          * not many in between.
413          *
414          * The use of atomics avoids serializing a high number of accesses, when most of the records will be thrown
415          * away. High contention only happens when there are very few existing records, which is only likely when the
416          * object isn't shared! If this is a problem, the loop can be aborted and the record dropped, because another
417          * thread won the race.
418          */
419         private void record0(Object hint) {
420             // Check TARGET_RECORDS > 0 here to avoid similar check before remove from and add to lastRecords
421             if (TARGET_RECORDS > 0) {
422                 Record oldHead;
423                 Record prevHead;
424                 Record newHead;
425                 boolean dropped;
426                 do {
427                     if ((prevHead = oldHead = headUpdater.get(this)) == null) {
428                         // already closed.
429                         return;
430                     }
431                     final int numElements = oldHead.pos + 1;
432                     if (numElements >= TARGET_RECORDS) {
433                         final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
434                         if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) {
435                             prevHead = oldHead.next;
436                         }
437                     } else {
438                         dropped = false;
439                     }
440                     newHead = hint != null ? new Record(prevHead, hint) : new Record(prevHead);
441                 } while (!headUpdater.compareAndSet(this, oldHead, newHead));
442                 if (dropped) {
443                     droppedRecordsUpdater.incrementAndGet(this);
444                 }
445             }
446         }
447 
448         boolean dispose() {
449             clear();
450             return allLeaks.remove(this);
451         }
452 
453         @Override
454         public boolean close() {
455             if (allLeaks.remove(this)) {
456                 // Call clear so the reference is not even enqueued.
457                 clear();
458                 headUpdater.set(this, null);
459                 return true;
460             }
461             return false;
462         }
463 
464         @Override
465         public boolean close(T trackedObject) {
466             // Ensure that the object that was tracked is the same as the one that was passed to close(...).
467             assert trackedHash == System.identityHashCode(trackedObject);
468 
469             try {
470                 return close();
471             } finally {
472                 // This method will do `synchronized(trackedObject)` and we should be sure this will not cause deadlock.
473                 // It should not, because somewhere up the callstack should be a (successful) `trackedObject.release`,
474                 // therefore it is unreasonable that anyone else, anywhere, is holding a lock on the trackedObject.
475                 // (Unreasonable but possible, unfortunately.)
476                 reachabilityFence0(trackedObject);
477             }
478         }
479 
480          /**
481          * Ensures that the object referenced by the given reference remains
482          * <a href="package-summary.html#reachability"><em>strongly reachable</em></a>,
483          * regardless of any prior actions of the program that might otherwise cause
484          * the object to become unreachable; thus, the referenced object is not
485          * reclaimable by garbage collection at least until after the invocation of
486          * this method.
487          *
488          * <p> Recent versions of the JDK have a nasty habit of prematurely deciding objects are unreachable.
489          * see: https://stackoverflow.com/questions/26642153/finalize-called-on-strongly-reachable-object-in-java-8
490          * The Java 9 method Reference.reachabilityFence offers a solution to this problem.
491          *
492          * <p> This method is always implemented as a synchronization on {@code ref}, not as
493          * {@code Reference.reachabilityFence} for consistency across platforms and to allow building on JDK 6-8.
494          * <b>It is the caller's responsibility to ensure that this synchronization will not cause deadlock.</b>
495          *
496          * @param ref the reference. If {@code null}, this method has no effect.
497          * @see java.lang.ref.Reference#reachabilityFence
498          */
499         private static void reachabilityFence0(Object ref) {
500             if (ref != null) {
501                 // Empty synchronized is ok: https://stackoverflow.com/a/31933260/1151521
502                 synchronized (ref) { }
503             }
504         }
505 
506         @Override
507         public String toString() {
508             Record oldHead = headUpdater.getAndSet(this, null);
509             if (oldHead == null) {
510                 // Already closed
511                 return EMPTY_STRING;
512             }
513 
514             final int dropped = droppedRecordsUpdater.get(this);
515             int duped = 0;
516 
517             int present = oldHead.pos + 1;
518             // Guess about 2 kilobytes per stack trace
519             StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
520             buf.append("Recent access records: ").append(NEWLINE);
521 
522             int i = 1;
523             Set<String> seen = new HashSet<String>(present);
524             for (; oldHead != Record.BOTTOM; oldHead = oldHead.next) {
525                 String s = oldHead.toString();
526                 if (seen.add(s)) {
527                     if (oldHead.next == Record.BOTTOM) {
528                         buf.append("Created at:").append(NEWLINE).append(s);
529                     } else {
530                         buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
531                     }
532                 } else {
533                     duped++;
534                 }
535             }
536 
537             if (duped > 0) {
538                 buf.append(": ")
539                         .append(duped)
540                         .append(" leak records were discarded because they were duplicates")
541                         .append(NEWLINE);
542             }
543 
544             if (dropped > 0) {
545                 buf.append(": ")
546                    .append(dropped)
547                    .append(" leak records were discarded because the leak record count is targeted to ")
548                    .append(TARGET_RECORDS)
549                    .append(". Use system property ")
550                    .append(PROP_TARGET_RECORDS)
551                    .append(" to increase the limit.")
552                    .append(NEWLINE);
553             }
554 
555             buf.setLength(buf.length() - NEWLINE.length());
556             return buf.toString();
557         }
558     }
559 
560     private static final AtomicReference<String[]> excludedMethods =
561             new AtomicReference<String[]>(EmptyArrays.EMPTY_STRINGS);
562 
563     public static void addExclusions(Class clz, String ... methodNames) {
564         Set<String> nameSet = new HashSet<String>(Arrays.asList(methodNames));
565         // Use loop rather than lookup. This avoids knowing the parameters, and doesn't have to handle
566         // NoSuchMethodException.
567         for (Method method : clz.getDeclaredMethods()) {
568             if (nameSet.remove(method.getName()) && nameSet.isEmpty()) {
569                 break;
570             }
571         }
572         if (!nameSet.isEmpty()) {
573             throw new IllegalArgumentException("Can't find '" + nameSet + "' in " + clz.getName());
574         }
575         String[] oldMethods;
576         String[] newMethods;
577         do {
578             oldMethods = excludedMethods.get();
579             newMethods = Arrays.copyOf(oldMethods, oldMethods.length + 2 * methodNames.length);
580             for (int i = 0; i < methodNames.length; i++) {
581                 newMethods[oldMethods.length + i * 2] = clz.getName();
582                 newMethods[oldMethods.length + i * 2 + 1] = methodNames[i];
583             }
584         } while (!excludedMethods.compareAndSet(oldMethods, newMethods));
585     }
586 
587     private static final class Record extends Throwable {
588         private static final long serialVersionUID = 6065153674892850720L;
589 
590         private static final Record BOTTOM = new Record();
591 
592         private final String hintString;
593         private final Record next;
594         private final int pos;
595 
596         Record(Record next, Object hint) {
597             // This needs to be generated even if toString() is never called as it may change later on.
598             hintString = hint instanceof ResourceLeakHint ? ((ResourceLeakHint) hint).toHintString() : hint.toString();
599             this.next = next;
600             this.pos = next.pos + 1;
601         }
602 
603         Record(Record next) {
604            hintString = null;
605            this.next = next;
606            this.pos = next.pos + 1;
607         }
608 
609         // Used to terminate the stack
610         private Record() {
611             hintString = null;
612             next = null;
613             pos = -1;
614         }
615 
616         @Override
617         public String toString() {
618             StringBuilder buf = new StringBuilder(2048);
619             if (hintString != null) {
620                 buf.append("\tHint: ").append(hintString).append(NEWLINE);
621             }
622 
623             // Append the stack trace.
624             StackTraceElement[] array = getStackTrace();
625             // Skip the first three elements.
626             out: for (int i = 3; i < array.length; i++) {
627                 StackTraceElement element = array[i];
628                 // Strip the noisy stack trace elements.
629                 String[] exclusions = excludedMethods.get();
630                 for (int k = 0; k < exclusions.length; k += 2) {
631                     if (exclusions[k].equals(element.getClassName())
632                             && exclusions[k + 1].equals(element.getMethodName())) {
633                         continue out;
634                     }
635                 }
636 
637                 buf.append('\t');
638                 buf.append(element.toString());
639                 buf.append(NEWLINE);
640             }
641             return buf.toString();
642         }
643     }
644 }