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             headUpdater.set(this, Record.BOTTOM);
373             this.allLeaks = allLeaks;
374         }
375 
376         @Override
377         public void record() {
378             record0(null);
379         }
380 
381         @Override
382         public void record(Object hint) {
383             record0(hint);
384         }
385 
386         /**
387          * This method works by exponentially backing off as more records are present in the stack. Each record has a
388          * 1 / 2^n chance of dropping the top most record and replacing it with itself. This has a number of convenient
389          * properties:
390          *
391          * <ol>
392          * <li>  The current record is always recorded. This is due to the compare and swap dropping the top most
393          *       record, rather than the to-be-pushed record.
394          * <li>  The very last access will always be recorded. This comes as a property of 1.
395          * <li>  It is possible to retain more records than the target, based upon the probability distribution.
396          * <li>  It is easy to keep a precise record of the number of elements in the stack, since each element has to
397          *     know how tall the stack is.
398          * </ol>
399          *
400          * In this particular implementation, there are also some advantages. A thread local random is used to decide
401          * if something should be recorded. This means that if there is a deterministic access pattern, it is now
402          * possible to see what other accesses occur, rather than always dropping them. Second, after
403          * {@link #TARGET_RECORDS} accesses, backoff occurs. This matches typical access patterns,
404          * where there are either a high number of accesses (i.e. a cached buffer), or low (an ephemeral buffer), but
405          * not many in between.
406          *
407          * The use of atomics avoids serializing a high number of accesses, when most of the records will be thrown
408          * away. High contention only happens when there are very few existing records, which is only likely when the
409          * object isn't shared! If this is a problem, the loop can be aborted and the record dropped, because another
410          * thread won the race.
411          */
412         private void record0(Object hint) {
413             // Check TARGET_RECORDS > 0 here to avoid similar check before remove from and add to lastRecords
414             if (TARGET_RECORDS > 0) {
415                 Record oldHead;
416                 Record prevHead;
417                 Record newHead;
418                 boolean dropped;
419                 do {
420                     if ((prevHead = oldHead = headUpdater.get(this)) == null) {
421                         // already closed.
422                         return;
423                     }
424                     final int numElements = oldHead.pos + 1;
425                     if (numElements >= TARGET_RECORDS) {
426                         final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
427                         if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) {
428                             prevHead = oldHead.next;
429                         }
430                     } else {
431                         dropped = false;
432                     }
433                     newHead = hint != null ? new Record(prevHead, hint) : new Record(prevHead);
434                 } while (!headUpdater.compareAndSet(this, oldHead, newHead));
435                 if (dropped) {
436                     droppedRecordsUpdater.incrementAndGet(this);
437                 }
438             }
439         }
440 
441         boolean dispose() {
442             clear();
443             return allLeaks.remove(this, LeakEntry.INSTANCE);
444         }
445 
446         @Override
447         public boolean close() {
448             // Use the ConcurrentMap remove method, which avoids allocating an iterator.
449             if (allLeaks.remove(this, LeakEntry.INSTANCE)) {
450                 // Call clear so the reference is not even enqueued.
451                 clear();
452                 headUpdater.set(this, null);
453                 return true;
454             }
455             return false;
456         }
457 
458         @Override
459         public boolean close(T trackedObject) {
460             // Ensure that the object that was tracked is the same as the one that was passed to close(...).
461             assert trackedHash == System.identityHashCode(trackedObject);
462 
463             // We need to actually do the null check of the trackedObject after we close the leak because otherwise
464             // we may get false-positives reported by the ResourceLeakDetector. This can happen as the JIT / GC may
465             // be able to figure out that we do not need the trackedObject anymore and so already enqueue it for
466             // collection before we actually get a chance to close the enclosing ResourceLeak.
467             return close() && trackedObject != null;
468         }
469 
470         @Override
471         public String toString() {
472             Record oldHead = headUpdater.getAndSet(this, null);
473             if (oldHead == null) {
474                 // Already closed
475                 return EMPTY_STRING;
476             }
477 
478             final int dropped = droppedRecordsUpdater.get(this);
479             int duped = 0;
480 
481             int present = oldHead.pos + 1;
482             // Guess about 2 kilobytes per stack trace
483             StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
484             buf.append("Recent access records: ").append(NEWLINE);
485 
486             int i = 1;
487             Set<String> seen = new HashSet<String>(present);
488             for (; oldHead != Record.BOTTOM; oldHead = oldHead.next) {
489                 String s = oldHead.toString();
490                 if (seen.add(s)) {
491                     if (oldHead.next == Record.BOTTOM) {
492                         buf.append("Created at:").append(NEWLINE).append(s);
493                     } else {
494                         buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
495                     }
496                 } else {
497                     duped++;
498                 }
499             }
500 
501             if (duped > 0) {
502                 buf.append(": ")
503                         .append(dropped)
504                         .append(" leak records were discarded because they were duplicates")
505                         .append(NEWLINE);
506             }
507 
508             if (dropped > 0) {
509                 buf.append(": ")
510                    .append(dropped)
511                    .append(" leak records were discarded because the leak record count is targeted to ")
512                    .append(TARGET_RECORDS)
513                    .append(". Use system property ")
514                    .append(PROP_TARGET_RECORDS)
515                    .append(" to increase the limit.")
516                    .append(NEWLINE);
517             }
518 
519             buf.setLength(buf.length() - NEWLINE.length());
520             return buf.toString();
521         }
522     }
523 
524     private static final AtomicReference<String[]> excludedMethods =
525             new AtomicReference<String[]>(EmptyArrays.EMPTY_STRINGS);
526 
527     public static void addExclusions(Class clz, String ... methodNames) {
528         Set<String> nameSet = new HashSet<String>(Arrays.asList(methodNames));
529         // Use loop rather than lookup. This avoids knowing the parameters, and doesn't have to handle
530         // NoSuchMethodException.
531         for (Method method : clz.getDeclaredMethods()) {
532             if (nameSet.remove(method.getName()) && nameSet.isEmpty()) {
533                 break;
534             }
535         }
536         if (!nameSet.isEmpty()) {
537             throw new IllegalArgumentException("Can't find '" + nameSet + "' in " + clz.getName());
538         }
539         String[] oldMethods;
540         String[] newMethods;
541         do {
542             oldMethods = excludedMethods.get();
543             newMethods = Arrays.copyOf(oldMethods, oldMethods.length + 2 * methodNames.length);
544             for (int i = 0; i < methodNames.length; i++) {
545                 newMethods[oldMethods.length + i * 2] = clz.getName();
546                 newMethods[oldMethods.length + i * 2 + 1] = methodNames[i];
547             }
548         } while (!excludedMethods.compareAndSet(oldMethods, newMethods));
549     }
550 
551     private static final class Record extends Throwable {
552         private static final long serialVersionUID = 6065153674892850720L;
553 
554         private static final Record BOTTOM = new Record();
555 
556         private final String hintString;
557         private final Record next;
558         private final int pos;
559 
560         Record(Record next, Object hint) {
561             // This needs to be generated even if toString() is never called as it may change later on.
562             hintString = hint instanceof ResourceLeakHint ? ((ResourceLeakHint) hint).toHintString() : hint.toString();
563             this.next = next;
564             this.pos = next.pos + 1;
565         }
566 
567         Record(Record next) {
568            hintString = null;
569            this.next = next;
570            this.pos = next.pos + 1;
571         }
572 
573         // Used to terminate the stack
574         private Record() {
575             hintString = null;
576             next = null;
577             pos = -1;
578         }
579 
580         @Override
581         public String toString() {
582             StringBuilder buf = new StringBuilder(2048);
583             if (hintString != null) {
584                 buf.append("\tHint: ").append(hintString).append(NEWLINE);
585             }
586 
587             // Append the stack trace.
588             StackTraceElement[] array = getStackTrace();
589             // Skip the first three elements.
590             out: for (int i = 3; i < array.length; i++) {
591                 StackTraceElement element = array[i];
592                 // Strip the noisy stack trace elements.
593                 String[] exclusions = excludedMethods.get();
594                 for (int k = 0; k < exclusions.length; k += 2) {
595                     if (exclusions[k].equals(element.getClassName())
596                             && exclusions[k + 1].equals(element.getMethodName())) {
597                         continue out;
598                     }
599                 }
600 
601                 buf.append('\t');
602                 buf.append(element.toString());
603                 buf.append(NEWLINE);
604             }
605             return buf.toString();
606         }
607     }
608 
609     private static final class LeakEntry {
610         static final LeakEntry INSTANCE = new LeakEntry();
611         private static final int HASH = System.identityHashCode(INSTANCE);
612 
613         private LeakEntry() {
614         }
615 
616         @Override
617         public int hashCode() {
618             return HASH;
619         }
620 
621         @Override
622         public boolean equals(Object obj) {
623             return obj == this;
624         }
625     }
626 }