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 private DefaultResourceLeak<T> track0(T obj, boolean force) {
271 Level level = ResourceLeakDetector.level;
272 if (force ||
273 level == Level.PARANOID ||
274 (level != Level.DISABLED && ThreadLocalRandom.current().nextInt(samplingInterval) == 0)) {
275 reportLeak();
276 return new DefaultResourceLeak<>(obj, refQueue, allLeaks, getInitialHint(resourceType));
277 }
278 return null;
279 }
280
281 private void clearRefQueue() {
282 for (;;) {
283 DefaultResourceLeak<?> ref = (DefaultResourceLeak<?>) refQueue.poll();
284 if (ref == null) {
285 break;
286 }
287 ref.dispose();
288 }
289 }
290
291
292
293
294
295
296
297 protected boolean needReport() {
298 return logger.isErrorEnabled();
299 }
300
301 private void reportLeak() {
302 if (!needReport()) {
303 clearRefQueue();
304 return;
305 }
306
307
308 for (;;) {
309 DefaultResourceLeak<?> ref = (DefaultResourceLeak<?>) refQueue.poll();
310 if (ref == null) {
311 break;
312 }
313
314 if (!ref.dispose()) {
315 continue;
316 }
317
318 String records = ref.getReportAndClearRecords();
319 if (reportedLeaks.add(records)) {
320 if (records.isEmpty()) {
321 reportUntracedLeak(resourceType);
322 } else {
323 reportTracedLeak(resourceType, records);
324 }
325
326 LeakListener listener = leakListener;
327 if (listener != null) {
328 listener.onLeak(resourceType, records);
329 }
330 }
331 }
332 }
333
334
335
336
337
338 protected void reportTracedLeak(String resourceType, String records) {
339 logger.error(
340 "LEAK: {}.release() was not called before it's garbage-collected. " +
341 "See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
342 resourceType, records);
343 }
344
345
346
347
348
349 protected void reportUntracedLeak(String resourceType) {
350 logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
351 "Enable advanced leak reporting to find out where the leak occurred. " +
352 "To enable advanced leak reporting, " +
353 "specify the JVM option '-D{}={}' or call {}.setLevel() " +
354 "See https://netty.io/wiki/reference-counted-objects.html for more information.",
355 resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
356 }
357
358
359
360
361 @Deprecated
362 protected void reportInstancesLeak(String resourceType) {
363 }
364
365
366
367
368
369
370 protected Object getInitialHint(String resourceType) {
371 return null;
372 }
373
374
375
376
377 public void setLeakListener(LeakListener leakListener) {
378 this.leakListener = leakListener;
379 }
380
381 public interface LeakListener {
382
383
384
385
386 void onLeak(String resourceType, String records);
387 }
388
389 @SuppressWarnings("deprecation")
390 private static final class DefaultResourceLeak<T>
391 extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
392
393 @SuppressWarnings({"unchecked", "rawtypes"})
394 private static final AtomicReferenceFieldUpdater<DefaultResourceLeak<?>, TraceRecord> headUpdater =
395 (AtomicReferenceFieldUpdater)
396 AtomicReferenceFieldUpdater.newUpdater(DefaultResourceLeak.class, TraceRecord.class, "head");
397
398 @SuppressWarnings({"unchecked", "rawtypes"})
399 private static final AtomicIntegerFieldUpdater<DefaultResourceLeak<?>> droppedRecordsUpdater =
400 (AtomicIntegerFieldUpdater)
401 AtomicIntegerFieldUpdater.newUpdater(DefaultResourceLeak.class, "droppedRecords");
402
403 @SuppressWarnings("unused")
404 private volatile TraceRecord head;
405 @SuppressWarnings("unused")
406 private volatile int droppedRecords;
407
408 private final Set<DefaultResourceLeak<?>> allLeaks;
409 private final int trackedHash;
410
411 DefaultResourceLeak(
412 Object referent,
413 ReferenceQueue<Object> refQueue,
414 Set<DefaultResourceLeak<?>> allLeaks,
415 Object initialHint) {
416 super(referent, refQueue);
417
418 assert referent != null;
419
420 this.allLeaks = allLeaks;
421
422
423
424
425 trackedHash = System.identityHashCode(referent);
426 allLeaks.add(this);
427
428 headUpdater.set(this, initialHint == null ?
429 new TraceRecord(TraceRecord.BOTTOM) : new TraceRecord(TraceRecord.BOTTOM, initialHint));
430 }
431
432 @Override
433 public void record() {
434 record0(null);
435 }
436
437 @Override
438 public void record(Object hint) {
439 record0(hint);
440 }
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468 private void record0(Object hint) {
469
470 if (TARGET_RECORDS > 0) {
471 TraceRecord oldHead;
472 TraceRecord prevHead;
473 TraceRecord newHead;
474 boolean dropped;
475 do {
476 if ((prevHead = oldHead = headUpdater.get(this)) == null ||
477 oldHead.pos == TraceRecord.CLOSE_MARK_POS) {
478
479 return;
480 }
481 final int numElements = oldHead.pos + 1;
482 if (numElements >= TARGET_RECORDS) {
483 final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
484 dropped = ThreadLocalRandom.current().nextInt(1 << backOffFactor) != 0;
485 if (dropped) {
486 prevHead = oldHead.next;
487 }
488 } else {
489 dropped = false;
490 }
491 newHead = hint != null ? new TraceRecord(prevHead, hint) : new TraceRecord(prevHead);
492 } while (!headUpdater.compareAndSet(this, oldHead, newHead));
493 if (dropped) {
494 droppedRecordsUpdater.incrementAndGet(this);
495 }
496 }
497 }
498
499 boolean dispose() {
500 clear();
501 return allLeaks.remove(this);
502 }
503
504 @Override
505 public boolean close() {
506 if (allLeaks.remove(this)) {
507
508 clear();
509 headUpdater.set(this, TRACK_CLOSE ? new TraceRecord(true) : null);
510 return true;
511 }
512 return false;
513 }
514
515 @Override
516 public boolean close(T trackedObject) {
517
518 assert trackedHash == System.identityHashCode(trackedObject);
519
520 try {
521 return close();
522 } finally {
523
524
525
526
527 reachabilityFence0(trackedObject);
528 }
529 }
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550 @SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "EmptySynchronizedStatement"})
551 private static void reachabilityFence0(Object ref) {
552 if (ref != null) {
553 synchronized (ref) {
554
555 }
556 }
557 }
558
559 @Override
560 public @Nullable Throwable getCloseStackTraceIfAny() {
561 TraceRecord head = headUpdater.get(this);
562 if (head != null && head.pos == TraceRecord.CLOSE_MARK_POS) {
563 return head;
564 }
565 return null;
566 }
567
568 @Override
569 public String toString() {
570 TraceRecord oldHead = headUpdater.get(this);
571 return generateReport(oldHead);
572 }
573
574 String getReportAndClearRecords() {
575 TraceRecord oldHead = headUpdater.getAndSet(this, null);
576 return generateReport(oldHead);
577 }
578
579 private String generateReport(TraceRecord oldHead) {
580 if (oldHead == null) {
581
582 return EMPTY_STRING;
583 }
584
585 final int dropped = droppedRecordsUpdater.get(this);
586 int duped = 0;
587
588 int present = oldHead.pos + 1;
589
590 StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
591 buf.append("Recent access records: ").append(NEWLINE);
592
593 int i = 1;
594 Set<String> seen = new HashSet<>(present);
595 for (; oldHead != TraceRecord.BOTTOM; oldHead = oldHead.next) {
596 String s = oldHead.toString();
597 if (seen.add(s)) {
598 if (oldHead.next == TraceRecord.BOTTOM) {
599 buf.append("Created at:").append(NEWLINE).append(s);
600 } else {
601 buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
602 }
603 } else {
604 duped++;
605 }
606 }
607
608 if (duped > 0) {
609 buf.append(": ")
610 .append(duped)
611 .append(" leak records were discarded because they were duplicates")
612 .append(NEWLINE);
613 }
614
615 if (dropped > 0) {
616 buf.append(": ")
617 .append(dropped)
618 .append(" leak records were discarded because the leak record count is targeted to ")
619 .append(TARGET_RECORDS)
620 .append(". Use system property ")
621 .append(PROP_TARGET_RECORDS)
622 .append(" to increase the limit.")
623 .append(NEWLINE);
624 }
625
626 buf.setLength(buf.length() - NEWLINE.length());
627 return buf.toString();
628 }
629 }
630
631 private static final AtomicReference<String[]> excludedMethods =
632 new AtomicReference<>(EmptyArrays.EMPTY_STRINGS);
633
634 public static void addExclusions(@SuppressWarnings("rawtypes") Class clz, String ... methodNames) {
635 Set<String> nameSet = new HashSet<>(Arrays.asList(methodNames));
636
637
638 for (Method method : clz.getDeclaredMethods()) {
639 if (nameSet.remove(method.getName()) && nameSet.isEmpty()) {
640 break;
641 }
642 }
643 if (!nameSet.isEmpty()) {
644 throw new IllegalArgumentException("Can't find '" + nameSet + "' in " + clz.getName());
645 }
646 String[] oldMethods;
647 String[] newMethods;
648 do {
649 oldMethods = excludedMethods.get();
650 newMethods = Arrays.copyOf(oldMethods, oldMethods.length + 2 * methodNames.length);
651 for (int i = 0; i < methodNames.length; i++) {
652 newMethods[oldMethods.length + i * 2] = clz.getName();
653 newMethods[oldMethods.length + i * 2 + 1] = methodNames[i];
654 }
655 } while (!excludedMethods.compareAndSet(oldMethods, newMethods));
656 }
657
658 private static class TraceRecord extends Throwable {
659 private static final long serialVersionUID = 6065153674892850720L;
660 public static final int BOTTOM_POS = -1;
661 public static final int CLOSE_MARK_POS = -2;
662
663 private static final TraceRecord BOTTOM = new TraceRecord(false) {
664 private static final long serialVersionUID = 7396077602074694571L;
665
666
667
668
669 @Override
670 public Throwable fillInStackTrace() {
671 return this;
672 }
673 };
674
675 private final String hintString;
676 private final TraceRecord next;
677 private final int pos;
678
679 TraceRecord(TraceRecord next, Object hint) {
680
681 hintString = hint instanceof ResourceLeakHint ? ((ResourceLeakHint) hint).toHintString() : hint.toString();
682 this.next = next;
683 this.pos = next.pos + 1;
684 }
685
686 TraceRecord(TraceRecord next) {
687 hintString = null;
688 this.next = next;
689 this.pos = next.pos + 1;
690 }
691
692
693 private TraceRecord(boolean closeMarker) {
694 hintString = null;
695 next = null;
696 pos = closeMarker ? CLOSE_MARK_POS : BOTTOM_POS;
697 }
698
699 @Override
700 public String toString() {
701 StringBuilder buf = new StringBuilder(2048);
702 if (hintString != null) {
703 buf.append("\tHint: ").append(hintString).append(NEWLINE);
704 }
705
706
707 StackTraceElement[] array = getStackTrace();
708
709 out: for (int i = 3; i < array.length; i++) {
710 StackTraceElement element = array[i];
711
712 String[] exclusions = excludedMethods.get();
713 for (int k = 0; k < exclusions.length; k += 2) {
714
715
716 if (exclusions[k].equals(element.getClassName())
717 && exclusions[k + 1].equals(element.getMethodName())) {
718 continue out;
719 }
720 }
721
722 buf.append('\t');
723 buf.append(element.toString());
724 buf.append(NEWLINE);
725 }
726 return buf.toString();
727 }
728 }
729 }