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    *   https://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.netty5.util;
18  
19  import io.netty5.util.internal.EmptyArrays;
20  import io.netty5.util.internal.SystemPropertyUtil;
21  import io.netty5.util.internal.logging.InternalLogger;
22  import io.netty5.util.internal.logging.InternalLoggerFactory;
23  
24  import java.lang.ref.Reference;
25  import java.lang.ref.ReferenceQueue;
26  import java.lang.ref.WeakReference;
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.ThreadLocalRandom;
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.netty5.util.internal.StringUtil.EMPTY_STRING;
39  import static io.netty5.util.internal.StringUtil.NEWLINE;
40  import static io.netty5.util.internal.StringUtil.simpleClassName;
41  import static java.util.Objects.requireNonNull;
42  
43  public class ResourceLeakDetector<T> {
44  
45      private static final String PROP_LEVEL = "io.netty5.leakDetection.level";
46      private static final Level DEFAULT_LEVEL = Level.SIMPLE;
47  
48      private static final String PROP_TARGET_RECORDS = "io.netty5.leakDetection.targetRecords";
49      private static final int DEFAULT_TARGET_RECORDS = 4;
50  
51      private static final String PROP_SAMPLING_INTERVAL = "io.netty5.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         String levelStr = SystemPropertyUtil.get(PROP_LEVEL, DEFAULT_LEVEL.name());
105         Level level = Level.parseLevel(levelStr);
106 
107         TARGET_RECORDS = SystemPropertyUtil.getInt(PROP_TARGET_RECORDS, DEFAULT_TARGET_RECORDS);
108         SAMPLING_INTERVAL = SystemPropertyUtil.getInt(PROP_SAMPLING_INTERVAL, DEFAULT_SAMPLING_INTERVAL);
109 
110         ResourceLeakDetector.level = level;
111         if (logger.isDebugEnabled()) {
112             logger.debug("-D{}: {}", PROP_LEVEL, level.name().toLowerCase());
113             logger.debug("-D{}: {}", PROP_TARGET_RECORDS, TARGET_RECORDS);
114         }
115     }
116 
117     /**
118      * Returns {@code true} if resource leak detection is enabled.
119      */
120     public static boolean isEnabled() {
121         return getLevel().ordinal() > Level.DISABLED.ordinal();
122     }
123 
124     /**
125      * Sets the resource leak detection level.
126      */
127     public static void setLevel(Level level) {
128         requireNonNull(level, "level");
129         ResourceLeakDetector.level = level;
130     }
131 
132     /**
133      * Returns the current resource leak detection level.
134      */
135     public static Level getLevel() {
136         return level;
137     }
138 
139     /** the collection of active resources */
140     private final Set<DefaultResourceLeak<?>> allLeaks = ConcurrentHashMap.newKeySet();
141 
142     private final ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
143     private final Set<String> reportedLeaks =
144             Collections.newSetFromMap(new ConcurrentHashMap<>());
145     private final String resourceType;
146     private final int samplingInterval;
147 
148     /**
149      * This should not be used directly by users of {@link ResourceLeakDetector}.
150      * Please use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class)}
151      * or {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int)}
152      */
153     public ResourceLeakDetector(Class<?> resourceType, int samplingInterval) {
154         this.resourceType = simpleClassName(resourceType);
155         this.samplingInterval = samplingInterval;
156     }
157 
158     /**
159      * Creates a new {@link ResourceLeakTracker} which is expected to be closed via
160      * {@link ResourceLeakTracker#close(Object)} when the related resource is deallocated.
161      *
162      * @return the {@link ResourceLeakTracker} or {@code null}
163      */
164     @SuppressWarnings("unchecked")
165     public final ResourceLeakTracker<T> track(T obj) {
166         return track0(obj);
167     }
168 
169     @SuppressWarnings("unchecked")
170     private DefaultResourceLeak track0(T obj) {
171         Level level = ResourceLeakDetector.level;
172         if (level == Level.DISABLED) {
173             return null;
174         }
175 
176         if (level.ordinal() < Level.PARANOID.ordinal()) {
177             if (ThreadLocalRandom.current().nextInt(samplingInterval) == 0) {
178                 reportLeak();
179                 return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
180             }
181             return null;
182         }
183         reportLeak();
184         return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
185     }
186 
187     private void clearRefQueue() {
188         for (;;) {
189             DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
190             if (ref == null) {
191                 break;
192             }
193             ref.dispose();
194         }
195     }
196 
197     /**
198      * When the return value is {@code true}, {@link #reportTracedLeak} and {@link #reportUntracedLeak}
199      * will be called once a leak is detected, otherwise not.
200      *
201      * @return {@code true} to enable leak reporting.
202      */
203     protected boolean needReport() {
204         return logger.isErrorEnabled();
205     }
206 
207     private void reportLeak() {
208         if (!needReport()) {
209             clearRefQueue();
210             return;
211         }
212 
213         // Detect and report previous leaks.
214         for (;;) {
215             DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
216             if (ref == null) {
217                 break;
218             }
219 
220             if (!ref.dispose()) {
221                 continue;
222             }
223 
224             String records = ref.getReportAndClearRecords();
225             if (reportedLeaks.add(records)) {
226                 if (records.isEmpty()) {
227                     reportUntracedLeak(resourceType);
228                 } else {
229                     reportTracedLeak(resourceType, records);
230                 }
231             }
232         }
233     }
234 
235     /**
236      * This method is called when a traced leak is detected. It can be overridden for tracking how many times leaks
237      * have been detected.
238      */
239     protected void reportTracedLeak(String resourceType, String records) {
240         logger.error(
241                 "LEAK: {}.release() was not called before it's garbage-collected. " +
242                 "See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
243                 resourceType, records);
244     }
245 
246     /**
247      * This method is called when an untraced leak is detected. It can be overridden for tracking how many times leaks
248      * have been detected.
249      */
250     protected void reportUntracedLeak(String resourceType) {
251         logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
252                 "Enable advanced leak reporting to find out where the leak occurred. " +
253                 "To enable advanced leak reporting, " +
254                 "specify the JVM option '-D{}={}' or call {}.setLevel() " +
255                 "See https://netty.io/wiki/reference-counted-objects.html for more information.",
256                 resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
257     }
258 
259     /**
260      * @deprecated This method will no longer be invoked by {@link ResourceLeakDetector}.
261      */
262     @Deprecated
263     protected void reportInstancesLeak(String resourceType) {
264     }
265 
266     /**
267      * Create a hint object to be attached to an object tracked by this record. Similar to the additional information
268      * supplied to {@link ResourceLeakTracker#record(Object)}, will be printed alongside the stack trace of the
269      * creation of the resource.
270      */
271     protected Object getInitialHint(String resourceType) {
272         return null;
273     }
274 
275     private static final class DefaultResourceLeak<T>
276             extends WeakReference<Object> implements ResourceLeakTracker<T> {
277 
278         @SuppressWarnings("unchecked") // generics and updaters do not mix.
279         private static final AtomicReferenceFieldUpdater<DefaultResourceLeak<?>, TraceRecord> headUpdater =
280                 (AtomicReferenceFieldUpdater)
281                         AtomicReferenceFieldUpdater.newUpdater(DefaultResourceLeak.class, TraceRecord.class, "head");
282 
283         @SuppressWarnings("unchecked") // generics and updaters do not mix.
284         private static final AtomicIntegerFieldUpdater<DefaultResourceLeak<?>> droppedRecordsUpdater =
285                 (AtomicIntegerFieldUpdater)
286                         AtomicIntegerFieldUpdater.newUpdater(DefaultResourceLeak.class, "droppedRecords");
287 
288         @SuppressWarnings("unused")
289         private volatile TraceRecord head;
290         @SuppressWarnings("unused")
291         private volatile int droppedRecords;
292 
293         private final Set<DefaultResourceLeak<?>> allLeaks;
294         private final int trackedHash;
295 
296         DefaultResourceLeak(
297                 Object referent,
298                 ReferenceQueue<Object> refQueue,
299                 Set<DefaultResourceLeak<?>> allLeaks,
300                 Object initialHint) {
301             super(referent, refQueue);
302 
303             assert referent != null;
304 
305             // Store the hash of the tracked object to later assert it in the close(...) method.
306             // It's important that we not store a reference to the referent as this would disallow it from
307             // be collected via the WeakReference.
308             trackedHash = System.identityHashCode(referent);
309             allLeaks.add(this);
310             // Create a new Record so we always have the creation stacktrace included.
311             headUpdater.set(this, initialHint == null ?
312                     new TraceRecord(TraceRecord.BOTTOM) : new TraceRecord(TraceRecord.BOTTOM, initialHint));
313             this.allLeaks = allLeaks;
314         }
315 
316         @Override
317         public void record() {
318             record0(null);
319         }
320 
321         @Override
322         public void record(Object hint) {
323             record0(hint);
324         }
325 
326         /**
327          * This method works by exponentially backing off as more records are present in the stack. Each record has a
328          * 1 / 2^n chance of dropping the top most record and replacing it with itself. This has a number of convenient
329          * properties:
330          *
331          * <ol>
332          * <li>  The current record is always recorded. This is due to the compare and swap dropping the top most
333          *       record, rather than the to-be-pushed record.
334          * <li>  The very last access will always be recorded. This comes as a property of 1.
335          * <li>  It is possible to retain more records than the target, based upon the probability distribution.
336          * <li>  It is easy to keep a precise record of the number of elements in the stack, since each element has to
337          *     know how tall the stack is.
338          * </ol>
339          *
340          * In this particular implementation, there are also some advantages. A thread local random is used to decide
341          * if something should be recorded. This means that if there is a deterministic access pattern, it is now
342          * possible to see what other accesses occur, rather than always dropping them. Second, after
343          * {@link #TARGET_RECORDS} accesses, backoff occurs. This matches typical access patterns,
344          * where there are either a high number of accesses (i.e. a cached buffer), or low (an ephemeral buffer), but
345          * not many in between.
346          *
347          * The use of atomics avoids serializing a high number of accesses, when most of the records will be thrown
348          * away. High contention only happens when there are very few existing records, which is only likely when the
349          * object isn't shared! If this is a problem, the loop can be aborted and the record dropped, because another
350          * thread won the race.
351          */
352         private void record0(Object hint) {
353             // Check TARGET_RECORDS > 0 here to avoid similar check before remove from and add to lastRecords
354             if (TARGET_RECORDS > 0) {
355                 TraceRecord oldHead;
356                 TraceRecord prevHead;
357                 TraceRecord newHead;
358                 boolean dropped;
359                 do {
360                     if ((prevHead = oldHead = headUpdater.get(this)) == null) {
361                         // already closed.
362                         return;
363                     }
364                     final int numElements = oldHead.pos + 1;
365                     if (numElements >= TARGET_RECORDS) {
366                         final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
367                         dropped = ThreadLocalRandom.current().nextInt(1 << backOffFactor) != 0;
368                         if (dropped) {
369                             prevHead = oldHead.next;
370                         }
371                     } else {
372                         dropped = false;
373                     }
374                     newHead = hint != null ? new TraceRecord(prevHead, hint) : new TraceRecord(prevHead);
375                 } while (!headUpdater.compareAndSet(this, oldHead, newHead));
376                 if (dropped) {
377                     droppedRecordsUpdater.incrementAndGet(this);
378                 }
379             }
380         }
381 
382         boolean dispose() {
383             clear();
384             return allLeaks.remove(this);
385         }
386 
387         @Override
388         public boolean close(T trackedObject) {
389             // Ensure that the object that was tracked is the same as the one that was passed to close(...).
390             assert trackedHash == System.identityHashCode(trackedObject);
391 
392             try {
393                 if (allLeaks.remove(this)) {
394                     // Call clear so the reference is not even enqueued.
395                     clear();
396                     headUpdater.set(this, null);
397                     return true;
398                 }
399                 return false;
400             } finally {
401                 // Ensure the tracked object remain live and strongly referenced, until close() has finished.
402                 Reference.reachabilityFence(trackedObject);
403             }
404         }
405 
406         @Override
407         public String toString() {
408             TraceRecord oldHead = headUpdater.get(this);
409             return generateReport(oldHead);
410         }
411 
412         String getReportAndClearRecords() {
413             TraceRecord oldHead = headUpdater.getAndSet(this, null);
414             return generateReport(oldHead);
415         }
416 
417         private String generateReport(TraceRecord oldHead) {
418             if (oldHead == null) {
419                 // Already closed
420                 return EMPTY_STRING;
421             }
422 
423             final int dropped = droppedRecordsUpdater.get(this);
424             int duped = 0;
425 
426             int present = oldHead.pos + 1;
427             // Guess about 2 kilobytes per stack trace
428             StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
429             buf.append("Recent access records: ").append(NEWLINE);
430 
431             int i = 1;
432             Set<String> seen = new HashSet<>(present);
433             for (; oldHead != TraceRecord.BOTTOM; oldHead = oldHead.next) {
434                 String s = oldHead.toString();
435                 if (seen.add(s)) {
436                     if (oldHead.next == TraceRecord.BOTTOM) {
437                         buf.append("Created at:").append(NEWLINE).append(s);
438                     } else {
439                         buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
440                     }
441                 } else {
442                     duped++;
443                 }
444             }
445 
446             if (duped > 0) {
447                 buf.append(": ")
448                         .append(duped)
449                         .append(" leak records were discarded because they were duplicates")
450                         .append(NEWLINE);
451             }
452 
453             if (dropped > 0) {
454                 buf.append(": ")
455                    .append(dropped)
456                    .append(" leak records were discarded because the leak record count is targeted to ")
457                    .append(TARGET_RECORDS)
458                    .append(". Use system property ")
459                    .append(PROP_TARGET_RECORDS)
460                    .append(" to increase the limit.")
461                    .append(NEWLINE);
462             }
463 
464             buf.setLength(buf.length() - NEWLINE.length());
465             return buf.toString();
466         }
467     }
468 
469     private static final AtomicReference<String[]> excludedMethods =
470             new AtomicReference<>(EmptyArrays.EMPTY_STRINGS);
471 
472     public static void addExclusions(Class<?> clz, String ... methodNames) {
473         Set<String> nameSet = new HashSet<>(Arrays.asList(methodNames));
474         // Use loop rather than lookup. This avoids knowing the parameters, and doesn't have to handle
475         // NoSuchMethodException.
476         for (Method method : clz.getDeclaredMethods()) {
477             if (nameSet.remove(method.getName()) && nameSet.isEmpty()) {
478                 break;
479             }
480         }
481         if (!nameSet.isEmpty()) {
482             throw new IllegalArgumentException("Can't find '" + nameSet + "' in " + clz.getName());
483         }
484         String[] oldMethods;
485         String[] newMethods;
486         do {
487             oldMethods = excludedMethods.get();
488             newMethods = Arrays.copyOf(oldMethods, oldMethods.length + 2 * methodNames.length);
489             for (int i = 0; i < methodNames.length; i++) {
490                 newMethods[oldMethods.length + i * 2] = clz.getName();
491                 newMethods[oldMethods.length + i * 2 + 1] = methodNames[i];
492             }
493         } while (!excludedMethods.compareAndSet(oldMethods, newMethods));
494     }
495 
496     private static class TraceRecord extends Throwable {
497         private static final long serialVersionUID = 6065153674892850720L;
498 
499         private static final TraceRecord BOTTOM = new TraceRecord();
500 
501         private final String hintString;
502         private final TraceRecord next;
503         private final int pos;
504 
505         TraceRecord(TraceRecord next, Object hint) {
506             // This needs to be generated even if toString() is never called as it may change later on.
507             hintString = hint instanceof ResourceLeakHint ? ((ResourceLeakHint) hint).toHintString() : hint.toString();
508             this.next = next;
509             pos = next.pos + 1;
510         }
511 
512         TraceRecord(TraceRecord next) {
513            hintString = null;
514            this.next = next;
515            pos = next.pos + 1;
516         }
517 
518         // Used to terminate the stack
519         private TraceRecord() {
520             super(null, null, false, false);
521             hintString = null;
522             next = null;
523             pos = -1;
524         }
525 
526         @Override
527         public String toString() {
528             StringBuilder buf = new StringBuilder(2048);
529             if (hintString != null) {
530                 buf.append("\tHint: ").append(hintString).append(NEWLINE);
531             }
532 
533             // Append the stack trace.
534             StackTraceElement[] array = getStackTrace();
535             // Skip the first three elements.
536             out: for (int i = 3; i < array.length; i++) {
537                 StackTraceElement element = array[i];
538                 // Strip the noisy stack trace elements.
539                 String[] exclusions = excludedMethods.get();
540                 for (int k = 0; k < exclusions.length; k += 2) {
541                     // Suppress a warning about out of bounds access
542                     // since the length of excludedMethods is always even, see addExclusions()
543                     if (exclusions[k].equals(element.getClassName())
544                             && exclusions[k + 1].equals(element.getMethodName())) { // lgtm[java/index-out-of-bounds]
545                         continue out;
546                     }
547                 }
548 
549                 buf.append('\t');
550                 buf.append(element.toString());
551                 buf.append(NEWLINE);
552             }
553             return buf.toString();
554         }
555     }
556 }