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.ReferenceQueue;
27 import java.lang.ref.WeakReference;
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 this.allLeaks = allLeaks;
419
420
421
422
423 trackedHash = System.identityHashCode(referent);
424 allLeaks.add(this);
425
426 headUpdater.set(this, initialHint == null ?
427 new TraceRecord(TraceRecord.BOTTOM) : new TraceRecord(TraceRecord.BOTTOM, initialHint));
428 }
429
430 @Override
431 public void record() {
432 record0(null);
433 }
434
435 @Override
436 public void record(Object hint) {
437 record0(hint);
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
466 private void record0(Object hint) {
467
468 if (TARGET_RECORDS > 0) {
469 TraceRecord oldHead;
470 TraceRecord prevHead;
471 TraceRecord newHead;
472 boolean dropped;
473 do {
474 if ((prevHead = oldHead = headUpdater.get(this)) == null) {
475
476 return;
477 }
478 final int numElements = oldHead.pos + 1;
479 if (numElements >= TARGET_RECORDS) {
480 final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
481 if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) {
482 prevHead = oldHead.next;
483 }
484 } else {
485 dropped = false;
486 }
487 newHead = hint != null ? new TraceRecord(prevHead, hint) : new TraceRecord(prevHead);
488 } while (!headUpdater.compareAndSet(this, oldHead, newHead));
489 if (dropped) {
490 droppedRecordsUpdater.incrementAndGet(this);
491 }
492 }
493 }
494
495 boolean dispose() {
496 clear();
497 return allLeaks.remove(this);
498 }
499
500 @Override
501 public boolean close() {
502 if (allLeaks.remove(this)) {
503
504 clear();
505 headUpdater.set(this, null);
506 return true;
507 }
508 return false;
509 }
510
511 @Override
512 public boolean close(T trackedObject) {
513
514 assert trackedHash == System.identityHashCode(trackedObject);
515
516 try {
517 return close();
518 } finally {
519
520
521
522
523 reachabilityFence0(trackedObject);
524 }
525 }
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546 private static void reachabilityFence0(Object ref) {
547 if (ref != null) {
548 synchronized (ref) {
549
550 }
551 }
552 }
553
554 @Override
555 public String toString() {
556 TraceRecord oldHead = headUpdater.get(this);
557 return generateReport(oldHead);
558 }
559
560 String getReportAndClearRecords() {
561 TraceRecord oldHead = headUpdater.getAndSet(this, null);
562 return generateReport(oldHead);
563 }
564
565 private String generateReport(TraceRecord oldHead) {
566 if (oldHead == null) {
567
568 return EMPTY_STRING;
569 }
570
571 final int dropped = droppedRecordsUpdater.get(this);
572 int duped = 0;
573
574 int present = oldHead.pos + 1;
575
576 StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
577 buf.append("Recent access records: ").append(NEWLINE);
578
579 int i = 1;
580 Set<String> seen = new HashSet<String>(present);
581 for (; oldHead != TraceRecord.BOTTOM; oldHead = oldHead.next) {
582 String s = oldHead.toString();
583 if (seen.add(s)) {
584 if (oldHead.next == TraceRecord.BOTTOM) {
585 buf.append("Created at:").append(NEWLINE).append(s);
586 } else {
587 buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
588 }
589 } else {
590 duped++;
591 }
592 }
593
594 if (duped > 0) {
595 buf.append(": ")
596 .append(duped)
597 .append(" leak records were discarded because they were duplicates")
598 .append(NEWLINE);
599 }
600
601 if (dropped > 0) {
602 buf.append(": ")
603 .append(dropped)
604 .append(" leak records were discarded because the leak record count is targeted to ")
605 .append(TARGET_RECORDS)
606 .append(". Use system property ")
607 .append(PROP_TARGET_RECORDS)
608 .append(" to increase the limit.")
609 .append(NEWLINE);
610 }
611
612 buf.setLength(buf.length() - NEWLINE.length());
613 return buf.toString();
614 }
615 }
616
617 private static final AtomicReference<String[]> excludedMethods =
618 new AtomicReference<String[]>(EmptyArrays.EMPTY_STRINGS);
619
620 public static void addExclusions(Class clz, String ... methodNames) {
621 Set<String> nameSet = new HashSet<String>(Arrays.asList(methodNames));
622
623
624 for (Method method : clz.getDeclaredMethods()) {
625 if (nameSet.remove(method.getName()) && nameSet.isEmpty()) {
626 break;
627 }
628 }
629 if (!nameSet.isEmpty()) {
630 throw new IllegalArgumentException("Can't find '" + nameSet + "' in " + clz.getName());
631 }
632 String[] oldMethods;
633 String[] newMethods;
634 do {
635 oldMethods = excludedMethods.get();
636 newMethods = Arrays.copyOf(oldMethods, oldMethods.length + 2 * methodNames.length);
637 for (int i = 0; i < methodNames.length; i++) {
638 newMethods[oldMethods.length + i * 2] = clz.getName();
639 newMethods[oldMethods.length + i * 2 + 1] = methodNames[i];
640 }
641 } while (!excludedMethods.compareAndSet(oldMethods, newMethods));
642 }
643
644 private static class TraceRecord extends Throwable {
645 private static final long serialVersionUID = 6065153674892850720L;
646
647 private static final TraceRecord BOTTOM = new TraceRecord() {
648 private static final long serialVersionUID = 7396077602074694571L;
649
650
651
652
653 @Override
654 public Throwable fillInStackTrace() {
655 return this;
656 }
657 };
658
659 private final String hintString;
660 private final TraceRecord next;
661 private final int pos;
662
663 TraceRecord(TraceRecord next, Object hint) {
664
665 hintString = hint instanceof ResourceLeakHint ? ((ResourceLeakHint) hint).toHintString() : hint.toString();
666 this.next = next;
667 this.pos = next.pos + 1;
668 }
669
670 TraceRecord(TraceRecord next) {
671 hintString = null;
672 this.next = next;
673 this.pos = next.pos + 1;
674 }
675
676
677 private TraceRecord() {
678 hintString = null;
679 next = null;
680 pos = -1;
681 }
682
683 @Override
684 public String toString() {
685 StringBuilder buf = new StringBuilder(2048);
686 if (hintString != null) {
687 buf.append("\tHint: ").append(hintString).append(NEWLINE);
688 }
689
690
691 StackTraceElement[] array = getStackTrace();
692
693 out: for (int i = 3; i < array.length; i++) {
694 StackTraceElement element = array[i];
695
696 String[] exclusions = excludedMethods.get();
697 for (int k = 0; k < exclusions.length; k += 2) {
698
699
700 if (exclusions[k].equals(element.getClassName())
701 && exclusions[k + 1].equals(element.getMethodName())) {
702 continue out;
703 }
704 }
705
706 buf.append('\t');
707 buf.append(element.toString());
708 buf.append(NEWLINE);
709 }
710 return buf.toString();
711 }
712 }
713 }