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.netty.util;
18  
19  import io.netty.util.internal.EmptyArrays;
20  import io.netty.util.internal.ObjectUtil;
21  import io.netty.util.internal.PlatformDependent;
22  import io.netty.util.internal.SystemPropertyUtil;
23  import io.netty.util.internal.logging.InternalLogger;
24  import io.netty.util.internal.logging.InternalLoggerFactory;
25  
26  import java.lang.ref.ReferenceQueue;
27  import java.lang.ref.WeakReference;
28  import java.lang.reflect.Method;
29  import java.util.Arrays;
30  import java.util.Collections;
31  import java.util.HashSet;
32  import java.util.Set;
33  import java.util.concurrent.ConcurrentHashMap;
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, Level.DISABLED.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         ResourceLeakDetector.level = ObjectUtil.checkNotNull(level, "level");
154     }
155 
156     /**
157      * Returns the current resource leak detection level.
158      */
159     public static Level getLevel() {
160         return level;
161     }
162 
163     /** the collection of active resources */
164     private final Set<DefaultResourceLeak<?>> allLeaks =
165             Collections.newSetFromMap(new ConcurrentHashMap<DefaultResourceLeak<?>, Boolean>());
166 
167     private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
168     private final Set<String> reportedLeaks =
169             Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
170 
171     private final String resourceType;
172     private final int samplingInterval;
173 
174     /**
175      * Will be notified once a leak is detected.
176      */
177     private volatile LeakListener leakListener;
178 
179     /**
180      * @deprecated use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}.
181      */
182     @Deprecated
183     public ResourceLeakDetector(Class<?> resourceType) {
184         this(simpleClassName(resourceType));
185     }
186 
187     /**
188      * @deprecated use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}.
189      */
190     @Deprecated
191     public ResourceLeakDetector(String resourceType) {
192         this(resourceType, DEFAULT_SAMPLING_INTERVAL, Long.MAX_VALUE);
193     }
194 
195     /**
196      * @deprecated Use {@link ResourceLeakDetector#ResourceLeakDetector(Class, int)}.
197      * <p>
198      * This should not be used directly by users of {@link ResourceLeakDetector}.
199      * Please use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class)}
200      * or {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}
201      *
202      * @param maxActive This is deprecated and will be ignored.
203      */
204     @Deprecated
205     public ResourceLeakDetector(Class<?> resourceType, int samplingInterval, long maxActive) {
206         this(resourceType, samplingInterval);
207     }
208 
209     /**
210      * This should not be used directly by users of {@link ResourceLeakDetector}.
211      * Please use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class)}
212      * or {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}
213      */
214     @SuppressWarnings("deprecation")
215     public ResourceLeakDetector(Class<?> resourceType, int samplingInterval) {
216         this(simpleClassName(resourceType), samplingInterval, Long.MAX_VALUE);
217     }
218 
219     /**
220      * @deprecated use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}.
221      * <p>
222      * @param maxActive This is deprecated and will be ignored.
223      */
224     @Deprecated
225     public ResourceLeakDetector(String resourceType, int samplingInterval, long maxActive) {
226         this.resourceType = ObjectUtil.checkNotNull(resourceType, "resourceType");
227         this.samplingInterval = samplingInterval;
228     }
229 
230     /**
231      * Creates a new {@link ResourceLeak} which is expected to be closed via {@link ResourceLeak#close()} when the
232      * related resource is deallocated.
233      *
234      * @return the {@link ResourceLeak} or {@code null}
235      * @deprecated use {@link #track(Object)}
236      */
237     @Deprecated
238     public final ResourceLeak open(T obj) {
239         return track0(obj, false);
240     }
241 
242     /**
243      * Creates a new {@link ResourceLeakTracker} which is expected to be closed via
244      * {@link ResourceLeakTracker#close(Object)} when the related resource is deallocated.
245      *
246      * @return the {@link ResourceLeakTracker} or {@code null}
247      */
248     @SuppressWarnings("unchecked")
249     public final ResourceLeakTracker<T> track(T obj) {
250         return track0(obj, false);
251     }
252 
253     /**
254      * Creates a new {@link ResourceLeakTracker} which is expected to be closed via
255      * {@link ResourceLeakTracker#close(Object)} when the related resource is deallocated.
256      *
257      * Unlike {@link #track(Object)}, this method always returns a tracker, regardless
258      * of the detection settings.
259      *
260      * @return the {@link ResourceLeakTracker}
261      */
262     @SuppressWarnings("unchecked")
263     public ResourceLeakTracker<T> trackForcibly(T obj) {
264         return track0(obj, true);
265     }
266 
267     @SuppressWarnings("unchecked")
268     private DefaultResourceLeak track0(T obj, boolean force) {
269         Level level = ResourceLeakDetector.level;
270         if (force ||
271                 level == Level.PARANOID ||
272                 (level != Level.DISABLED && PlatformDependent.threadLocalRandom().nextInt(samplingInterval) == 0)) {
273             reportLeak();
274             return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
275         }
276         return null;
277     }
278 
279     private void clearRefQueue() {
280         for (;;) {
281             DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
282             if (ref == null) {
283                 break;
284             }
285             ref.dispose();
286         }
287     }
288 
289     /**
290      * When the return value is {@code true}, {@link #reportTracedLeak} and {@link #reportUntracedLeak}
291      * will be called once a leak is detected, otherwise not.
292      *
293      * @return {@code true} to enable leak reporting.
294      */
295     protected boolean needReport() {
296         return logger.isErrorEnabled();
297     }
298 
299     private void reportLeak() {
300         if (!needReport()) {
301             clearRefQueue();
302             return;
303         }
304 
305         // Detect and report previous leaks.
306         for (;;) {
307             DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
308             if (ref == null) {
309                 break;
310             }
311 
312             if (!ref.dispose()) {
313                 continue;
314             }
315 
316             String records = ref.getReportAndClearRecords();
317             if (reportedLeaks.add(records)) {
318                 if (records.isEmpty()) {
319                     reportUntracedLeak(resourceType);
320                 } else {
321                     reportTracedLeak(resourceType, records);
322                 }
323 
324                 LeakListener listener = leakListener;
325                 if (listener != null) {
326                     listener.onLeak(resourceType, records);
327                 }
328             }
329         }
330     }
331 
332     /**
333      * This method is called when a traced leak is detected. It can be overridden for tracking how many times leaks
334      * have been detected.
335      */
336     protected void reportTracedLeak(String resourceType, String records) {
337         logger.error(
338                 "LEAK: {}.release() was not called before it's garbage-collected. " +
339                 "See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
340                 resourceType, records);
341     }
342 
343     /**
344      * This method is called when an untraced leak is detected. It can be overridden for tracking how many times leaks
345      * have been detected.
346      */
347     protected void reportUntracedLeak(String resourceType) {
348         logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
349                 "Enable advanced leak reporting to find out where the leak occurred. " +
350                 "To enable advanced leak reporting, " +
351                 "specify the JVM option '-D{}={}' or call {}.setLevel() " +
352                 "See https://netty.io/wiki/reference-counted-objects.html for more information.",
353                 resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
354     }
355 
356     /**
357      * @deprecated This method will no longer be invoked by {@link ResourceLeakDetector}.
358      */
359     @Deprecated
360     protected void reportInstancesLeak(String resourceType) {
361     }
362 
363     /**
364      * Create a hint object to be attached to an object tracked by this record. Similar to the additional information
365      * supplied to {@link ResourceLeakTracker#record(Object)}, will be printed alongside the stack trace of the
366      * creation of the resource.
367      */
368     protected Object getInitialHint(String resourceType) {
369         return null;
370     }
371 
372     /**
373      * Set leak listener. Previous listener will be replaced.
374      */
375     public void setLeakListener(LeakListener leakListener) {
376         this.leakListener = leakListener;
377     }
378 
379     public interface LeakListener {
380 
381         /**
382          * Will be called once a leak is detected.
383          */
384         void onLeak(String resourceType, String records);
385     }
386 
387     @SuppressWarnings("deprecation")
388     private static final class DefaultResourceLeak<T>
389             extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
390 
391         @SuppressWarnings("unchecked") // generics and updaters do not mix.
392         private static final AtomicReferenceFieldUpdater<DefaultResourceLeak<?>, TraceRecord> headUpdater =
393                 (AtomicReferenceFieldUpdater)
394                         AtomicReferenceFieldUpdater.newUpdater(DefaultResourceLeak.class, TraceRecord.class, "head");
395 
396         @SuppressWarnings("unchecked") // generics and updaters do not mix.
397         private static final AtomicIntegerFieldUpdater<DefaultResourceLeak<?>> droppedRecordsUpdater =
398                 (AtomicIntegerFieldUpdater)
399                         AtomicIntegerFieldUpdater.newUpdater(DefaultResourceLeak.class, "droppedRecords");
400 
401         @SuppressWarnings("unused")
402         private volatile TraceRecord head;
403         @SuppressWarnings("unused")
404         private volatile int droppedRecords;
405 
406         private final Set<DefaultResourceLeak<?>> allLeaks;
407         private final int trackedHash;
408 
409         DefaultResourceLeak(
410                 Object referent,
411                 ReferenceQueue<Object> refQueue,
412                 Set<DefaultResourceLeak<?>> allLeaks,
413                 Object initialHint) {
414             super(referent, refQueue);
415 
416             assert referent != null;
417 
418             this.allLeaks = allLeaks;
419 
420             // Store the hash of the tracked object to later assert it in the close(...) method.
421             // It's important that we not store a reference to the referent as this would disallow it from
422             // be collected via the WeakReference.
423             trackedHash = System.identityHashCode(referent);
424             allLeaks.add(this);
425             // Create a new Record so we always have the creation stacktrace included.
426             headUpdater.set(this, initialHint == null ?
427                     new TraceRecord(TraceRecord.BOTTOM) : new TraceRecord(TraceRecord.BOTTOM, initialHint));
428         }
429 
430         @Override
431         public void record() {
432             record0(null);
433         }
434 
435         @Override
436         public void record(Object hint) {
437             record0(hint);
438         }
439 
440         /**
441          * This method works by exponentially backing off as more records are present in the stack. Each record has a
442          * 1 / 2^n chance of dropping the top most record and replacing it with itself. This has a number of convenient
443          * properties:
444          *
445          * <ol>
446          * <li>  The current record is always recorded. This is due to the compare and swap dropping the top most
447          *       record, rather than the to-be-pushed record.
448          * <li>  The very last access will always be recorded. This comes as a property of 1.
449          * <li>  It is possible to retain more records than the target, based upon the probability distribution.
450          * <li>  It is easy to keep a precise record of the number of elements in the stack, since each element has to
451          *     know how tall the stack is.
452          * </ol>
453          *
454          * In this particular implementation, there are also some advantages. A thread local random is used to decide
455          * if something should be recorded. This means that if there is a deterministic access pattern, it is now
456          * possible to see what other accesses occur, rather than always dropping them. Second, after
457          * {@link #TARGET_RECORDS} accesses, backoff occurs. This matches typical access patterns,
458          * where there are either a high number of accesses (i.e. a cached buffer), or low (an ephemeral buffer), but
459          * not many in between.
460          *
461          * The use of atomics avoids serializing a high number of accesses, when most of the records will be thrown
462          * away. High contention only happens when there are very few existing records, which is only likely when the
463          * object isn't shared! If this is a problem, the loop can be aborted and the record dropped, because another
464          * thread won the race.
465          */
466         private void record0(Object hint) {
467             // Check TARGET_RECORDS > 0 here to avoid similar check before remove from and add to lastRecords
468             if (TARGET_RECORDS > 0) {
469                 TraceRecord oldHead;
470                 TraceRecord prevHead;
471                 TraceRecord newHead;
472                 boolean dropped;
473                 do {
474                     if ((prevHead = oldHead = headUpdater.get(this)) == null) {
475                         // already closed.
476                         return;
477                     }
478                     final int numElements = oldHead.pos + 1;
479                     if (numElements >= TARGET_RECORDS) {
480                         final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
481                         if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) {
482                             prevHead = oldHead.next;
483                         }
484                     } else {
485                         dropped = false;
486                     }
487                     newHead = hint != null ? new TraceRecord(prevHead, hint) : new TraceRecord(prevHead);
488                 } while (!headUpdater.compareAndSet(this, oldHead, newHead));
489                 if (dropped) {
490                     droppedRecordsUpdater.incrementAndGet(this);
491                 }
492             }
493         }
494 
495         boolean dispose() {
496             clear();
497             return allLeaks.remove(this);
498         }
499 
500         @Override
501         public boolean close() {
502             if (allLeaks.remove(this)) {
503                 // Call clear so the reference is not even enqueued.
504                 clear();
505                 headUpdater.set(this, null);
506                 return true;
507             }
508             return false;
509         }
510 
511         @Override
512         public boolean close(T trackedObject) {
513             // Ensure that the object that was tracked is the same as the one that was passed to close(...).
514             assert trackedHash == System.identityHashCode(trackedObject);
515 
516             try {
517                 return close();
518             } finally {
519                 // This method will do `synchronized(trackedObject)` and we should be sure this will not cause deadlock.
520                 // It should not, because somewhere up the callstack should be a (successful) `trackedObject.release`,
521                 // therefore it is unreasonable that anyone else, anywhere, is holding a lock on the trackedObject.
522                 // (Unreasonable but possible, unfortunately.)
523                 reachabilityFence0(trackedObject);
524             }
525         }
526 
527          /**
528          * Ensures that the object referenced by the given reference remains
529          * <a href="package-summary.html#reachability"><em>strongly reachable</em></a>,
530          * regardless of any prior actions of the program that might otherwise cause
531          * the object to become unreachable; thus, the referenced object is not
532          * reclaimable by garbage collection at least until after the invocation of
533          * this method.
534          *
535          * <p> Recent versions of the JDK have a nasty habit of prematurely deciding objects are unreachable.
536          * see: https://stackoverflow.com/questions/26642153/finalize-called-on-strongly-reachable-object-in-java-8
537          * The Java 9 method Reference.reachabilityFence offers a solution to this problem.
538          *
539          * <p> This method is always implemented as a synchronization on {@code ref}, not as
540          * {@code Reference.reachabilityFence} for consistency across platforms and to allow building on JDK 6-8.
541          * <b>It is the caller's responsibility to ensure that this synchronization will not cause deadlock.</b>
542          *
543          * @param ref the reference. If {@code null}, this method has no effect.
544          * @see java.lang.ref.Reference#reachabilityFence
545          */
546         private static void reachabilityFence0(Object ref) {
547             if (ref != null) {
548                 synchronized (ref) {
549                     // Empty synchronized is ok: https://stackoverflow.com/a/31933260/1151521
550                 }
551             }
552         }
553 
554         @Override
555         public String toString() {
556             TraceRecord oldHead = headUpdater.get(this);
557             return generateReport(oldHead);
558         }
559 
560         String getReportAndClearRecords() {
561             TraceRecord oldHead = headUpdater.getAndSet(this, null);
562             return generateReport(oldHead);
563         }
564 
565         private String generateReport(TraceRecord oldHead) {
566             if (oldHead == null) {
567                 // Already closed
568                 return EMPTY_STRING;
569             }
570 
571             final int dropped = droppedRecordsUpdater.get(this);
572             int duped = 0;
573 
574             int present = oldHead.pos + 1;
575             // Guess about 2 kilobytes per stack trace
576             StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
577             buf.append("Recent access records: ").append(NEWLINE);
578 
579             int i = 1;
580             Set<String> seen = new HashSet<String>(present);
581             for (; oldHead != TraceRecord.BOTTOM; oldHead = oldHead.next) {
582                 String s = oldHead.toString();
583                 if (seen.add(s)) {
584                     if (oldHead.next == TraceRecord.BOTTOM) {
585                         buf.append("Created at:").append(NEWLINE).append(s);
586                     } else {
587                         buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
588                     }
589                 } else {
590                     duped++;
591                 }
592             }
593 
594             if (duped > 0) {
595                 buf.append(": ")
596                         .append(duped)
597                         .append(" leak records were discarded because they were duplicates")
598                         .append(NEWLINE);
599             }
600 
601             if (dropped > 0) {
602                 buf.append(": ")
603                    .append(dropped)
604                    .append(" leak records were discarded because the leak record count is targeted to ")
605                    .append(TARGET_RECORDS)
606                    .append(". Use system property ")
607                    .append(PROP_TARGET_RECORDS)
608                    .append(" to increase the limit.")
609                    .append(NEWLINE);
610             }
611 
612             buf.setLength(buf.length() - NEWLINE.length());
613             return buf.toString();
614         }
615     }
616 
617     private static final AtomicReference<String[]> excludedMethods =
618             new AtomicReference<String[]>(EmptyArrays.EMPTY_STRINGS);
619 
620     public static void addExclusions(Class clz, String ... methodNames) {
621         Set<String> nameSet = new HashSet<String>(Arrays.asList(methodNames));
622         // Use loop rather than lookup. This avoids knowing the parameters, and doesn't have to handle
623         // NoSuchMethodException.
624         for (Method method : clz.getDeclaredMethods()) {
625             if (nameSet.remove(method.getName()) && nameSet.isEmpty()) {
626                 break;
627             }
628         }
629         if (!nameSet.isEmpty()) {
630             throw new IllegalArgumentException("Can't find '" + nameSet + "' in " + clz.getName());
631         }
632         String[] oldMethods;
633         String[] newMethods;
634         do {
635             oldMethods = excludedMethods.get();
636             newMethods = Arrays.copyOf(oldMethods, oldMethods.length + 2 * methodNames.length);
637             for (int i = 0; i < methodNames.length; i++) {
638                 newMethods[oldMethods.length + i * 2] = clz.getName();
639                 newMethods[oldMethods.length + i * 2 + 1] = methodNames[i];
640             }
641         } while (!excludedMethods.compareAndSet(oldMethods, newMethods));
642     }
643 
644     private static class TraceRecord extends Throwable {
645         private static final long serialVersionUID = 6065153674892850720L;
646 
647         private static final TraceRecord BOTTOM = new TraceRecord() {
648             private static final long serialVersionUID = 7396077602074694571L;
649 
650             // Override fillInStackTrace() so we not populate the backtrace via a native call and so leak the
651             // Classloader.
652             // See https://github.com/netty/netty/pull/10691
653             @Override
654             public Throwable fillInStackTrace() {
655                 return this;
656             }
657         };
658 
659         private final String hintString;
660         private final TraceRecord next;
661         private final int pos;
662 
663         TraceRecord(TraceRecord next, Object hint) {
664             // This needs to be generated even if toString() is never called as it may change later on.
665             hintString = hint instanceof ResourceLeakHint ? ((ResourceLeakHint) hint).toHintString() : hint.toString();
666             this.next = next;
667             this.pos = next.pos + 1;
668         }
669 
670         TraceRecord(TraceRecord next) {
671            hintString = null;
672            this.next = next;
673            this.pos = next.pos + 1;
674         }
675 
676         // Used to terminate the stack
677         private TraceRecord() {
678             hintString = null;
679             next = null;
680             pos = -1;
681         }
682 
683         @Override
684         public String toString() {
685             StringBuilder buf = new StringBuilder(2048);
686             if (hintString != null) {
687                 buf.append("\tHint: ").append(hintString).append(NEWLINE);
688             }
689 
690             // Append the stack trace.
691             StackTraceElement[] array = getStackTrace();
692             // Skip the first three elements.
693             out: for (int i = 3; i < array.length; i++) {
694                 StackTraceElement element = array[i];
695                 // Strip the noisy stack trace elements.
696                 String[] exclusions = excludedMethods.get();
697                 for (int k = 0; k < exclusions.length; k += 2) {
698                     // Suppress a warning about out of bounds access
699                     // since the length of excludedMethods is always even, see addExclusions()
700                     if (exclusions[k].equals(element.getClassName())
701                             && exclusions[k + 1].equals(element.getMethodName())) {
702                         continue out;
703                     }
704                 }
705 
706                 buf.append('\t');
707                 buf.append(element.toString());
708                 buf.append(NEWLINE);
709             }
710             return buf.toString();
711         }
712     }
713 }