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