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