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.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
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
60
61 public enum Level {
62
63
64
65 DISABLED,
66
67
68
69
70 SIMPLE,
71
72
73
74
75 ADVANCED,
76
77
78
79
80 PARANOID;
81
82
83
84
85
86
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
118 String levelStr = SystemPropertyUtil.get(PROP_LEVEL_OLD, defaultLevel.name());
119
120
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
136
137 @Deprecated
138 public static void setEnabled(boolean enabled) {
139 setLevel(enabled? Level.SIMPLE : Level.DISABLED);
140 }
141
142
143
144
145 public static boolean isEnabled() {
146 return getLevel().ordinal() > Level.DISABLED.ordinal();
147 }
148
149
150
151
152 public static void setLevel(Level level) {
153 ResourceLeakDetector.level = ObjectUtil.checkNotNull(level, "level");
154 }
155
156
157
158
159 public static Level getLevel() {
160 return level;
161 }
162
163
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
176
177 private volatile LeakListener leakListener;
178
179
180
181
182 @Deprecated
183 public ResourceLeakDetector(Class<?> resourceType) {
184 this(simpleClassName(resourceType));
185 }
186
187
188
189
190 @Deprecated
191 public ResourceLeakDetector(String resourceType) {
192 this(resourceType, DEFAULT_SAMPLING_INTERVAL, Long.MAX_VALUE);
193 }
194
195
196
197
198
199
200
201
202
203
204 @Deprecated
205 public ResourceLeakDetector(Class<?> resourceType, int samplingInterval, long maxActive) {
206 this(resourceType, samplingInterval);
207 }
208
209
210
211
212
213
214 @SuppressWarnings("deprecation")
215 public ResourceLeakDetector(Class<?> resourceType, int samplingInterval) {
216 this(simpleClassName(resourceType), samplingInterval, Long.MAX_VALUE);
217 }
218
219
220
221
222
223
224 @Deprecated
225 public ResourceLeakDetector(String resourceType, int samplingInterval, long maxActive) {
226 this.resourceType = ObjectUtil.checkNotNull(resourceType, "resourceType");
227 this.samplingInterval = samplingInterval;
228 }
229
230
231
232
233
234
235
236
237 @Deprecated
238 public final ResourceLeak open(T obj) {
239 return track0(obj, false);
240 }
241
242
243
244
245
246
247
248 @SuppressWarnings("unchecked")
249 public final ResourceLeakTracker<T> track(T obj) {
250 return track0(obj, false);
251 }
252
253
254
255
256
257
258
259
260
261
262 @SuppressWarnings("unchecked")
263 public ResourceLeakTracker<T> trackForcibly(T obj) {
264 return track0(obj, true);
265 }
266
267 @SuppressWarnings("unchecked")
268 private DefaultResourceLeak track0(T obj, boolean force) {
269 Level level = ResourceLeakDetector.level;
270 if (force ||
271 level == Level.PARANOID ||
272 (level != Level.DISABLED && PlatformDependent.threadLocalRandom().nextInt(samplingInterval) == 0)) {
273 reportLeak();
274 return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
275 }
276 return null;
277 }
278
279 private void clearRefQueue() {
280 for (;;) {
281 DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
282 if (ref == null) {
283 break;
284 }
285 ref.dispose();
286 }
287 }
288
289
290
291
292
293
294
295 protected boolean needReport() {
296 return logger.isErrorEnabled();
297 }
298
299 private void reportLeak() {
300 if (!needReport()) {
301 clearRefQueue();
302 return;
303 }
304
305
306 for (;;) {
307 DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
308 if (ref == null) {
309 break;
310 }
311
312 if (!ref.dispose()) {
313 continue;
314 }
315
316 String records = ref.getReportAndClearRecords();
317 if (reportedLeaks.add(records)) {
318 if (records.isEmpty()) {
319 reportUntracedLeak(resourceType);
320 } else {
321 reportTracedLeak(resourceType, records);
322 }
323
324 LeakListener listener = leakListener;
325 if (listener != null) {
326 listener.onLeak(resourceType, records);
327 }
328 }
329 }
330 }
331
332
333
334
335
336 protected void reportTracedLeak(String resourceType, String records) {
337 logger.error(
338 "LEAK: {}.release() was not called before it's garbage-collected. " +
339 "See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
340 resourceType, records);
341 }
342
343
344
345
346
347 protected void reportUntracedLeak(String resourceType) {
348 logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
349 "Enable advanced leak reporting to find out where the leak occurred. " +
350 "To enable advanced leak reporting, " +
351 "specify the JVM option '-D{}={}' or call {}.setLevel() " +
352 "See https://netty.io/wiki/reference-counted-objects.html for more information.",
353 resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
354 }
355
356
357
358
359 @Deprecated
360 protected void reportInstancesLeak(String resourceType) {
361 }
362
363
364
365
366
367
368 protected Object getInitialHint(String resourceType) {
369 return null;
370 }
371
372
373
374
375 public void setLeakListener(LeakListener leakListener) {
376 this.leakListener = leakListener;
377 }
378
379 public interface LeakListener {
380
381
382
383
384 void onLeak(String resourceType, String records);
385 }
386
387 @SuppressWarnings("deprecation")
388 private static final class DefaultResourceLeak<T>
389 extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
390
391 @SuppressWarnings("unchecked")
392 private static final AtomicReferenceFieldUpdater<DefaultResourceLeak<?>, TraceRecord> headUpdater =
393 (AtomicReferenceFieldUpdater)
394 AtomicReferenceFieldUpdater.newUpdater(DefaultResourceLeak.class, TraceRecord.class, "head");
395
396 @SuppressWarnings("unchecked")
397 private static final AtomicIntegerFieldUpdater<DefaultResourceLeak<?>> droppedRecordsUpdater =
398 (AtomicIntegerFieldUpdater)
399 AtomicIntegerFieldUpdater.newUpdater(DefaultResourceLeak.class, "droppedRecords");
400
401 @SuppressWarnings("unused")
402 private volatile TraceRecord head;
403 @SuppressWarnings("unused")
404 private volatile int droppedRecords;
405
406 private final Set<DefaultResourceLeak<?>> allLeaks;
407 private final int trackedHash;
408
409 DefaultResourceLeak(
410 Object referent,
411 ReferenceQueue<Object> refQueue,
412 Set<DefaultResourceLeak<?>> allLeaks,
413 Object initialHint) {
414 super(referent, refQueue);
415
416 assert referent != null;
417
418
419
420
421 trackedHash = System.identityHashCode(referent);
422 allLeaks.add(this);
423
424 headUpdater.set(this, initialHint == null ?
425 new TraceRecord(TraceRecord.BOTTOM) : new TraceRecord(TraceRecord.BOTTOM, initialHint));
426 this.allLeaks = allLeaks;
427 }
428
429 @Override
430 public void record() {
431 record0(null);
432 }
433
434 @Override
435 public void record(Object hint) {
436 record0(hint);
437 }
438
439
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 private void record0(Object hint) {
466
467 if (TARGET_RECORDS > 0) {
468 TraceRecord oldHead;
469 TraceRecord prevHead;
470 TraceRecord newHead;
471 boolean dropped;
472 do {
473 if ((prevHead = oldHead = headUpdater.get(this)) == null) {
474
475 return;
476 }
477 final int numElements = oldHead.pos + 1;
478 if (numElements >= TARGET_RECORDS) {
479 final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
480 if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) {
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
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
513 assert trackedHash == System.identityHashCode(trackedObject);
514
515 try {
516 return close();
517 } finally {
518
519
520
521
522 reachabilityFence0(trackedObject);
523 }
524 }
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545 private static void reachabilityFence0(Object ref) {
546 if (ref != null) {
547 synchronized (ref) {
548
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
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
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
622
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
650
651
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
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
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
690 StackTraceElement[] array = getStackTrace();
691
692 out: for (int i = 3; i < array.length; i++) {
693 StackTraceElement element = array[i];
694
695 String[] exclusions = excludedMethods.get();
696 for (int k = 0; k < exclusions.length; k += 2) {
697
698
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 }