1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 import org.jetbrains.annotations.Nullable;
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.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
53 private static final int DEFAULT_SAMPLING_INTERVAL = 128;
54
55 private static final String PROP_TRACK_CLOSE = "io.netty.leakDetection.trackClose";
56 private static final boolean DEFAULT_TRACK_CLOSE = true;
57
58 private static final int TARGET_RECORDS;
59 static final int SAMPLING_INTERVAL;
60 private static final boolean TRACK_CLOSE;
61
62
63
64
65 public enum Level {
66
67
68
69 DISABLED,
70
71
72
73
74 SIMPLE,
75
76
77
78
79 ADVANCED,
80
81
82
83
84 PARANOID;
85
86
87
88
89
90
91
92 static Level parseLevel(String levelStr) {
93 String trimmedLevelStr = levelStr.trim();
94 for (Level l : values()) {
95 if (trimmedLevelStr.equalsIgnoreCase(l.name()) || trimmedLevelStr.equals(String.valueOf(l.ordinal()))) {
96 return l;
97 }
98 }
99 return DEFAULT_LEVEL;
100 }
101 }
102
103 private static Level level;
104
105 private static final InternalLogger logger = InternalLoggerFactory.getInstance(ResourceLeakDetector.class);
106
107 static {
108 final boolean disabled;
109 if (SystemPropertyUtil.get("io.netty.noResourceLeakDetection") != null) {
110 disabled = SystemPropertyUtil.getBoolean("io.netty.noResourceLeakDetection", false);
111 logger.debug("-Dio.netty.noResourceLeakDetection: {}", disabled);
112 logger.warn(
113 "-Dio.netty.noResourceLeakDetection is deprecated. Use '-D{}={}' instead.",
114 PROP_LEVEL, Level.DISABLED.name().toLowerCase());
115 } else {
116 disabled = false;
117 }
118
119 Level defaultLevel = disabled? Level.DISABLED : DEFAULT_LEVEL;
120
121
122 String levelStr = SystemPropertyUtil.get(PROP_LEVEL_OLD, defaultLevel.name());
123
124
125 levelStr = SystemPropertyUtil.get(PROP_LEVEL, levelStr);
126 Level level = Level.parseLevel(levelStr);
127
128 TARGET_RECORDS = SystemPropertyUtil.getInt(PROP_TARGET_RECORDS, DEFAULT_TARGET_RECORDS);
129 SAMPLING_INTERVAL = SystemPropertyUtil.getInt(PROP_SAMPLING_INTERVAL, DEFAULT_SAMPLING_INTERVAL);
130 TRACK_CLOSE = SystemPropertyUtil.getBoolean(PROP_TRACK_CLOSE, DEFAULT_TRACK_CLOSE);
131
132 ResourceLeakDetector.level = level;
133 if (logger.isDebugEnabled()) {
134 logger.debug("-D{}: {}", PROP_LEVEL, level.name().toLowerCase());
135 logger.debug("-D{}: {}", PROP_TARGET_RECORDS, TARGET_RECORDS);
136 }
137 }
138
139
140
141
142 @Deprecated
143 public static void setEnabled(boolean enabled) {
144 setLevel(enabled? Level.SIMPLE : Level.DISABLED);
145 }
146
147
148
149
150 public static boolean isEnabled() {
151 return getLevel().ordinal() > Level.DISABLED.ordinal();
152 }
153
154
155
156
157 public static void setLevel(Level level) {
158 ResourceLeakDetector.level = ObjectUtil.checkNotNull(level, "level");
159 }
160
161
162
163
164 public static Level getLevel() {
165 return level;
166 }
167
168
169 private final Set<DefaultResourceLeak<?>> allLeaks = ConcurrentHashMap.newKeySet();
170
171 private final ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
172 private final Set<String> reportedLeaks = ConcurrentHashMap.newKeySet();
173
174 private final String resourceType;
175 private final int samplingInterval;
176
177
178
179
180 private volatile LeakListener leakListener;
181
182
183
184
185 @Deprecated
186 public ResourceLeakDetector(Class<?> resourceType) {
187 this(simpleClassName(resourceType));
188 }
189
190
191
192
193 @Deprecated
194 public ResourceLeakDetector(String resourceType) {
195 this(resourceType, DEFAULT_SAMPLING_INTERVAL, Long.MAX_VALUE);
196 }
197
198
199
200
201
202
203
204
205
206
207 @Deprecated
208 public ResourceLeakDetector(
209 Class<?> resourceType, int samplingInterval, @SuppressWarnings("unused") long maxActive) {
210 this(resourceType, samplingInterval);
211 }
212
213
214
215
216
217
218 @SuppressWarnings("deprecation")
219 public ResourceLeakDetector(Class<?> resourceType, int samplingInterval) {
220 this(simpleClassName(resourceType), samplingInterval, Long.MAX_VALUE);
221 }
222
223
224
225
226
227
228 @Deprecated
229 public ResourceLeakDetector(
230 String resourceType, int samplingInterval, @SuppressWarnings("unused") long maxActive) {
231 this.resourceType = ObjectUtil.checkNotNull(resourceType, "resourceType");
232 this.samplingInterval = samplingInterval;
233 }
234
235
236
237
238
239
240
241
242 @Deprecated
243 public final ResourceLeak open(T obj) {
244 return track0(obj, false);
245 }
246
247
248
249
250
251
252
253 public ResourceLeakTracker<T> track(T obj) {
254 return track0(obj, false);
255 }
256
257
258
259
260
261
262
263
264
265
266 public ResourceLeakTracker<T> trackForcibly(T obj) {
267 return track0(obj, true);
268 }
269
270
271
272
273
274
275 public boolean isRecordEnabled() {
276 Level level = getLevel();
277 return (level == Level.ADVANCED || level == Level.PARANOID) && TARGET_RECORDS > 0;
278 }
279
280 private DefaultResourceLeak<T> track0(T obj, boolean force) {
281 Level level = ResourceLeakDetector.level;
282 if (force ||
283 level == Level.PARANOID ||
284 (level != Level.DISABLED && ThreadLocalRandom.current().nextInt(samplingInterval) == 0)) {
285 reportLeak();
286 return new DefaultResourceLeak<>(obj, refQueue, allLeaks, getInitialHint(resourceType));
287 }
288 return null;
289 }
290
291 private void clearRefQueue() {
292 for (;;) {
293 DefaultResourceLeak<?> ref = (DefaultResourceLeak<?>) refQueue.poll();
294 if (ref == null) {
295 break;
296 }
297 ref.dispose();
298 }
299 }
300
301
302
303
304
305
306
307 protected boolean needReport() {
308 return logger.isErrorEnabled();
309 }
310
311 private void reportLeak() {
312 if (!needReport()) {
313 clearRefQueue();
314 return;
315 }
316
317
318 for (;;) {
319 DefaultResourceLeak<?> ref = (DefaultResourceLeak<?>) refQueue.poll();
320 if (ref == null) {
321 break;
322 }
323
324 if (!ref.dispose()) {
325 continue;
326 }
327
328 String records = ref.getReportAndClearRecords();
329 if (reportedLeaks.add(records)) {
330 if (records.isEmpty()) {
331 reportUntracedLeak(resourceType);
332 } else {
333 reportTracedLeak(resourceType, records);
334 }
335
336 LeakListener listener = leakListener;
337 if (listener != null) {
338 listener.onLeak(resourceType, records);
339 }
340 }
341 }
342 }
343
344
345
346
347
348 protected void reportTracedLeak(String resourceType, String records) {
349 logger.error(
350 "LEAK: {}.release() was not called before it's garbage-collected. " +
351 "See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
352 resourceType, records);
353 }
354
355
356
357
358
359 protected void reportUntracedLeak(String resourceType) {
360 logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
361 "Enable advanced leak reporting to find out where the leak occurred. " +
362 "To enable advanced leak reporting, " +
363 "specify the JVM option '-D{}={}' or call {}.setLevel() " +
364 "See https://netty.io/wiki/reference-counted-objects.html for more information.",
365 resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
366 }
367
368
369
370
371 @Deprecated
372 protected void reportInstancesLeak(String resourceType) {
373 }
374
375
376
377
378
379
380 protected Object getInitialHint(String resourceType) {
381 return null;
382 }
383
384
385
386
387 public void setLeakListener(LeakListener leakListener) {
388 this.leakListener = leakListener;
389 }
390
391 public interface LeakListener {
392
393
394
395
396 void onLeak(String resourceType, String records);
397 }
398
399 @SuppressWarnings("deprecation")
400 private static final class DefaultResourceLeak<T>
401 extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
402
403 @SuppressWarnings({"unchecked", "rawtypes"})
404 private static final AtomicReferenceFieldUpdater<DefaultResourceLeak<?>, TraceRecord> headUpdater =
405 (AtomicReferenceFieldUpdater)
406 AtomicReferenceFieldUpdater.newUpdater(DefaultResourceLeak.class, TraceRecord.class, "head");
407
408 @SuppressWarnings({"unchecked", "rawtypes"})
409 private static final AtomicIntegerFieldUpdater<DefaultResourceLeak<?>> droppedRecordsUpdater =
410 (AtomicIntegerFieldUpdater)
411 AtomicIntegerFieldUpdater.newUpdater(DefaultResourceLeak.class, "droppedRecords");
412
413 @SuppressWarnings("unused")
414 private volatile TraceRecord head;
415 @SuppressWarnings("unused")
416 private volatile int droppedRecords;
417
418 private final Set<DefaultResourceLeak<?>> allLeaks;
419 private final int trackedHash;
420
421 DefaultResourceLeak(
422 Object referent,
423 ReferenceQueue<Object> refQueue,
424 Set<DefaultResourceLeak<?>> allLeaks,
425 Object initialHint) {
426 super(referent, refQueue);
427
428 assert referent != null;
429
430 this.allLeaks = allLeaks;
431
432
433
434
435 trackedHash = System.identityHashCode(referent);
436 allLeaks.add(this);
437
438 headUpdater.set(this, initialHint == null ?
439 new TraceRecord(TraceRecord.BOTTOM) : new TraceRecord(TraceRecord.BOTTOM, initialHint));
440 }
441
442 @Override
443 public void record() {
444 record0(null);
445 }
446
447 @Override
448 public void record(Object hint) {
449 record0(hint);
450 }
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478 private void record0(Object hint) {
479
480 if (TARGET_RECORDS > 0) {
481 TraceRecord oldHead;
482 TraceRecord prevHead;
483 TraceRecord newHead;
484 boolean dropped;
485 do {
486 if ((prevHead = oldHead = headUpdater.get(this)) == null ||
487 oldHead.pos == TraceRecord.CLOSE_MARK_POS) {
488
489 return;
490 }
491 final int numElements = oldHead.pos + 1;
492 if (numElements >= TARGET_RECORDS) {
493 final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
494 dropped = ThreadLocalRandom.current().nextInt(1 << backOffFactor) != 0;
495 if (dropped) {
496 prevHead = oldHead.next;
497 }
498 } else {
499 dropped = false;
500 }
501 newHead = hint != null ? new TraceRecord(prevHead, hint) : new TraceRecord(prevHead);
502 } while (!headUpdater.compareAndSet(this, oldHead, newHead));
503 if (dropped) {
504 droppedRecordsUpdater.incrementAndGet(this);
505 }
506 }
507 }
508
509 boolean dispose() {
510 clear();
511 return allLeaks.remove(this);
512 }
513
514 @Override
515 public boolean close() {
516 if (allLeaks.remove(this)) {
517
518 clear();
519 headUpdater.set(this, TRACK_CLOSE ? new TraceRecord(true) : null);
520 return true;
521 }
522 return false;
523 }
524
525 @Override
526 public boolean close(T trackedObject) {
527
528 assert trackedHash == System.identityHashCode(trackedObject);
529
530 try {
531 return close();
532 } finally {
533
534
535
536
537 reachabilityFence0(trackedObject);
538 }
539 }
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560 @SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "EmptySynchronizedStatement"})
561 private static void reachabilityFence0(Object ref) {
562 if (ref != null) {
563 synchronized (ref) {
564
565 }
566 }
567 }
568
569 @Override
570 public @Nullable Throwable getCloseStackTraceIfAny() {
571 TraceRecord head = headUpdater.get(this);
572 if (head != null && head.pos == TraceRecord.CLOSE_MARK_POS) {
573 return head;
574 }
575 return null;
576 }
577
578 @Override
579 public String toString() {
580 TraceRecord oldHead = headUpdater.get(this);
581 return generateReport(oldHead);
582 }
583
584 String getReportAndClearRecords() {
585 TraceRecord oldHead = headUpdater.getAndSet(this, null);
586 return generateReport(oldHead);
587 }
588
589 private String generateReport(TraceRecord oldHead) {
590 if (oldHead == null) {
591
592 return EMPTY_STRING;
593 }
594
595 final int dropped = droppedRecordsUpdater.get(this);
596 int duped = 0;
597
598 int present = oldHead.pos + 1;
599
600 StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
601 buf.append("Recent access records: ").append(NEWLINE);
602
603 int i = 1;
604 Set<String> seen = new HashSet<>(present);
605 for (; oldHead != TraceRecord.BOTTOM; oldHead = oldHead.next) {
606 String s = oldHead.toString();
607 if (seen.add(s)) {
608 if (oldHead.next == TraceRecord.BOTTOM) {
609 buf.append("Created at:").append(NEWLINE).append(s);
610 } else {
611 buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
612 }
613 } else {
614 duped++;
615 }
616 }
617
618 if (duped > 0) {
619 buf.append(": ")
620 .append(duped)
621 .append(" leak records were discarded because they were duplicates")
622 .append(NEWLINE);
623 }
624
625 if (dropped > 0) {
626 buf.append(": ")
627 .append(dropped)
628 .append(" leak records were discarded because the leak record count is targeted to ")
629 .append(TARGET_RECORDS)
630 .append(". Use system property ")
631 .append(PROP_TARGET_RECORDS)
632 .append(" to increase the limit.")
633 .append(NEWLINE);
634 }
635
636 buf.setLength(buf.length() - NEWLINE.length());
637 return buf.toString();
638 }
639 }
640
641 private static final AtomicReference<String[]> excludedMethods =
642 new AtomicReference<>(EmptyArrays.EMPTY_STRINGS);
643
644 public static void addExclusions(@SuppressWarnings("rawtypes") Class clz, String ... methodNames) {
645 Set<String> nameSet = new HashSet<>(Arrays.asList(methodNames));
646
647
648 for (Method method : clz.getDeclaredMethods()) {
649 if (nameSet.remove(method.getName()) && nameSet.isEmpty()) {
650 break;
651 }
652 }
653 if (!nameSet.isEmpty()) {
654 throw new IllegalArgumentException("Can't find '" + nameSet + "' in " + clz.getName());
655 }
656 String[] oldMethods;
657 String[] newMethods;
658 do {
659 oldMethods = excludedMethods.get();
660 newMethods = Arrays.copyOf(oldMethods, oldMethods.length + 2 * methodNames.length);
661 for (int i = 0; i < methodNames.length; i++) {
662 newMethods[oldMethods.length + i * 2] = clz.getName();
663 newMethods[oldMethods.length + i * 2 + 1] = methodNames[i];
664 }
665 } while (!excludedMethods.compareAndSet(oldMethods, newMethods));
666 }
667
668 private static class TraceRecord extends Throwable {
669 private static final long serialVersionUID = 6065153674892850720L;
670 public static final int BOTTOM_POS = -1;
671 public static final int CLOSE_MARK_POS = -2;
672
673 private static final TraceRecord BOTTOM = new TraceRecord(false) {
674 private static final long serialVersionUID = 7396077602074694571L;
675
676
677
678
679 @Override
680 public Throwable fillInStackTrace() {
681 return this;
682 }
683 };
684
685 private final String hintString;
686 private final TraceRecord next;
687 private final int pos;
688
689 TraceRecord(TraceRecord next, Object hint) {
690
691 hintString = hint instanceof ResourceLeakHint ? ((ResourceLeakHint) hint).toHintString() : hint.toString();
692 this.next = next;
693 this.pos = next.pos + 1;
694 }
695
696 TraceRecord(TraceRecord next) {
697 hintString = null;
698 this.next = next;
699 this.pos = next.pos + 1;
700 }
701
702
703 private TraceRecord(boolean closeMarker) {
704 hintString = null;
705 next = null;
706 pos = closeMarker ? CLOSE_MARK_POS : BOTTOM_POS;
707 }
708
709 @Override
710 public String toString() {
711 StringBuilder buf = new StringBuilder(2048);
712 if (hintString != null) {
713 buf.append("\tHint: ").append(hintString).append(NEWLINE);
714 }
715
716
717 StackTraceElement[] array = getStackTrace();
718
719 out: for (int i = 3; i < array.length; i++) {
720 StackTraceElement element = array[i];
721
722 String[] exclusions = excludedMethods.get();
723 for (int k = 0; k < exclusions.length; k += 2) {
724
725
726 if (exclusions[k].equals(element.getClassName())
727 && exclusions[k + 1].equals(element.getMethodName())) {
728 continue out;
729 }
730 }
731
732 buf.append('\t');
733 buf.append(element.toString());
734 buf.append(NEWLINE);
735 }
736 return buf.toString();
737 }
738 }
739 }