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