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(Class<?> resourceType, int samplingInterval, long maxActive) {
209 this(resourceType, samplingInterval);
210 }
211
212
213
214
215
216
217 @SuppressWarnings("deprecation")
218 public ResourceLeakDetector(Class<?> resourceType, int samplingInterval) {
219 this(simpleClassName(resourceType), samplingInterval, Long.MAX_VALUE);
220 }
221
222
223
224
225
226
227 @Deprecated
228 public ResourceLeakDetector(String resourceType, int samplingInterval, long maxActive) {
229 this.resourceType = ObjectUtil.checkNotNull(resourceType, "resourceType");
230 this.samplingInterval = samplingInterval;
231 }
232
233
234
235
236
237
238
239
240 @Deprecated
241 public final ResourceLeak open(T obj) {
242 return track0(obj, false);
243 }
244
245
246
247
248
249
250
251 @SuppressWarnings("unchecked")
252 public final ResourceLeakTracker<T> track(T obj) {
253 return track0(obj, false);
254 }
255
256
257
258
259
260
261
262
263
264
265 @SuppressWarnings("unchecked")
266 public ResourceLeakTracker<T> trackForcibly(T obj) {
267 return track0(obj, true);
268 }
269
270 @SuppressWarnings("unchecked")
271 private DefaultResourceLeak track0(T obj, boolean force) {
272 Level level = ResourceLeakDetector.level;
273 if (force ||
274 level == Level.PARANOID ||
275 (level != Level.DISABLED && ThreadLocalRandom.current().nextInt(samplingInterval) == 0)) {
276 reportLeak();
277 return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
278 }
279 return null;
280 }
281
282 private void clearRefQueue() {
283 for (;;) {
284 DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
285 if (ref == null) {
286 break;
287 }
288 ref.dispose();
289 }
290 }
291
292
293
294
295
296
297
298 protected boolean needReport() {
299 return logger.isErrorEnabled();
300 }
301
302 private void reportLeak() {
303 if (!needReport()) {
304 clearRefQueue();
305 return;
306 }
307
308
309 for (;;) {
310 DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
311 if (ref == null) {
312 break;
313 }
314
315 if (!ref.dispose()) {
316 continue;
317 }
318
319 String records = ref.getReportAndClearRecords();
320 if (reportedLeaks.add(records)) {
321 if (records.isEmpty()) {
322 reportUntracedLeak(resourceType);
323 } else {
324 reportTracedLeak(resourceType, records);
325 }
326
327 LeakListener listener = leakListener;
328 if (listener != null) {
329 listener.onLeak(resourceType, records);
330 }
331 }
332 }
333 }
334
335
336
337
338
339 protected void reportTracedLeak(String resourceType, String records) {
340 logger.error(
341 "LEAK: {}.release() was not called before it's garbage-collected. " +
342 "See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
343 resourceType, records);
344 }
345
346
347
348
349
350 protected void reportUntracedLeak(String resourceType) {
351 logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
352 "Enable advanced leak reporting to find out where the leak occurred. " +
353 "To enable advanced leak reporting, " +
354 "specify the JVM option '-D{}={}' or call {}.setLevel() " +
355 "See https://netty.io/wiki/reference-counted-objects.html for more information.",
356 resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
357 }
358
359
360
361
362 @Deprecated
363 protected void reportInstancesLeak(String resourceType) {
364 }
365
366
367
368
369
370
371 protected Object getInitialHint(String resourceType) {
372 return null;
373 }
374
375
376
377
378 public void setLeakListener(LeakListener leakListener) {
379 this.leakListener = leakListener;
380 }
381
382 public interface LeakListener {
383
384
385
386
387 void onLeak(String resourceType, String records);
388 }
389
390 @SuppressWarnings("deprecation")
391 private static final class DefaultResourceLeak<T>
392 extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
393
394 @SuppressWarnings("unchecked")
395 private static final AtomicReferenceFieldUpdater<DefaultResourceLeak<?>, TraceRecord> headUpdater =
396 (AtomicReferenceFieldUpdater)
397 AtomicReferenceFieldUpdater.newUpdater(DefaultResourceLeak.class, TraceRecord.class, "head");
398
399 @SuppressWarnings("unchecked")
400 private static final AtomicIntegerFieldUpdater<DefaultResourceLeak<?>> droppedRecordsUpdater =
401 (AtomicIntegerFieldUpdater)
402 AtomicIntegerFieldUpdater.newUpdater(DefaultResourceLeak.class, "droppedRecords");
403
404 @SuppressWarnings("unused")
405 private volatile TraceRecord head;
406 @SuppressWarnings("unused")
407 private volatile int droppedRecords;
408
409 private final Set<DefaultResourceLeak<?>> allLeaks;
410 private final int trackedHash;
411
412 DefaultResourceLeak(
413 Object referent,
414 ReferenceQueue<Object> refQueue,
415 Set<DefaultResourceLeak<?>> allLeaks,
416 Object initialHint) {
417 super(referent, refQueue);
418
419 assert referent != null;
420
421 this.allLeaks = allLeaks;
422
423
424
425
426 trackedHash = System.identityHashCode(referent);
427 allLeaks.add(this);
428
429 headUpdater.set(this, initialHint == null ?
430 new TraceRecord(TraceRecord.BOTTOM) : new TraceRecord(TraceRecord.BOTTOM, initialHint));
431 }
432
433 @Override
434 public void record() {
435 record0(null);
436 }
437
438 @Override
439 public void record(Object hint) {
440 record0(hint);
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
469 private void record0(Object hint) {
470
471 if (TARGET_RECORDS > 0) {
472 TraceRecord oldHead;
473 TraceRecord prevHead;
474 TraceRecord newHead;
475 boolean dropped;
476 do {
477 if ((prevHead = oldHead = headUpdater.get(this)) == null ||
478 oldHead.pos == TraceRecord.CLOSE_MARK_POS) {
479
480 return;
481 }
482 final int numElements = oldHead.pos + 1;
483 if (numElements >= TARGET_RECORDS) {
484 final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
485 dropped = ThreadLocalRandom.current().nextInt(1 << backOffFactor) != 0;
486 if (dropped) {
487 prevHead = oldHead.next;
488 }
489 } else {
490 dropped = false;
491 }
492 newHead = hint != null ? new TraceRecord(prevHead, hint) : new TraceRecord(prevHead);
493 } while (!headUpdater.compareAndSet(this, oldHead, newHead));
494 if (dropped) {
495 droppedRecordsUpdater.incrementAndGet(this);
496 }
497 }
498 }
499
500 boolean dispose() {
501 clear();
502 return allLeaks.remove(this);
503 }
504
505 @Override
506 public boolean close() {
507 if (allLeaks.remove(this)) {
508
509 clear();
510 headUpdater.set(this, TRACK_CLOSE ? new TraceRecord(true) : null);
511 return true;
512 }
513 return false;
514 }
515
516 @Override
517 public boolean close(T trackedObject) {
518
519 assert trackedHash == System.identityHashCode(trackedObject);
520
521 try {
522 return close();
523 } finally {
524
525
526
527
528 reachabilityFence0(trackedObject);
529 }
530 }
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
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<String>(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<String[]>(EmptyArrays.EMPTY_STRINGS);
633
634 public static void addExclusions(Class clz, String ... methodNames) {
635 Set<String> nameSet = new HashSet<String>(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 }