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
25 import java.lang.ref.ReferenceQueue;
26 import java.lang.ref.WeakReference;
27 import java.lang.reflect.Method;
28 import java.util.Arrays;
29 import java.util.Collections;
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 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 = ConcurrentHashMap.newKeySet();
165
166 private final ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
167 private final Set<String> reportedLeaks = ConcurrentHashMap.newKeySet();
168
169 private final String resourceType;
170 private final int samplingInterval;
171
172
173
174
175 private volatile LeakListener leakListener;
176
177
178
179
180 @Deprecated
181 public ResourceLeakDetector(Class<?> resourceType) {
182 this(simpleClassName(resourceType));
183 }
184
185
186
187
188 @Deprecated
189 public ResourceLeakDetector(String resourceType) {
190 this(resourceType, DEFAULT_SAMPLING_INTERVAL, Long.MAX_VALUE);
191 }
192
193
194
195
196
197
198
199
200
201
202 @Deprecated
203 public ResourceLeakDetector(Class<?> resourceType, int samplingInterval, long maxActive) {
204 this(resourceType, samplingInterval);
205 }
206
207
208
209
210
211
212 @SuppressWarnings("deprecation")
213 public ResourceLeakDetector(Class<?> resourceType, int samplingInterval) {
214 this(simpleClassName(resourceType), samplingInterval, Long.MAX_VALUE);
215 }
216
217
218
219
220
221
222 @Deprecated
223 public ResourceLeakDetector(String resourceType, int samplingInterval, long maxActive) {
224 this.resourceType = ObjectUtil.checkNotNull(resourceType, "resourceType");
225 this.samplingInterval = samplingInterval;
226 }
227
228
229
230
231
232
233
234
235 @Deprecated
236 public final ResourceLeak open(T obj) {
237 return track0(obj, false);
238 }
239
240
241
242
243
244
245
246 @SuppressWarnings("unchecked")
247 public final ResourceLeakTracker<T> track(T obj) {
248 return track0(obj, false);
249 }
250
251
252
253
254
255
256
257
258
259
260 @SuppressWarnings("unchecked")
261 public ResourceLeakTracker<T> trackForcibly(T obj) {
262 return track0(obj, true);
263 }
264
265 @SuppressWarnings("unchecked")
266 private DefaultResourceLeak track0(T obj, boolean force) {
267 Level level = ResourceLeakDetector.level;
268 if (force ||
269 level == Level.PARANOID ||
270 (level != Level.DISABLED && ThreadLocalRandom.current().nextInt(samplingInterval) == 0)) {
271 reportLeak();
272 return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
273 }
274 return null;
275 }
276
277 private void clearRefQueue() {
278 for (;;) {
279 DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
280 if (ref == null) {
281 break;
282 }
283 ref.dispose();
284 }
285 }
286
287
288
289
290
291
292
293 protected boolean needReport() {
294 return logger.isErrorEnabled();
295 }
296
297 private void reportLeak() {
298 if (!needReport()) {
299 clearRefQueue();
300 return;
301 }
302
303
304 for (;;) {
305 DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
306 if (ref == null) {
307 break;
308 }
309
310 if (!ref.dispose()) {
311 continue;
312 }
313
314 String records = ref.getReportAndClearRecords();
315 if (reportedLeaks.add(records)) {
316 if (records.isEmpty()) {
317 reportUntracedLeak(resourceType);
318 } else {
319 reportTracedLeak(resourceType, records);
320 }
321
322 LeakListener listener = leakListener;
323 if (listener != null) {
324 listener.onLeak(resourceType, records);
325 }
326 }
327 }
328 }
329
330
331
332
333
334 protected void reportTracedLeak(String resourceType, String records) {
335 logger.error(
336 "LEAK: {}.release() was not called before it's garbage-collected. " +
337 "See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
338 resourceType, records);
339 }
340
341
342
343
344
345 protected void reportUntracedLeak(String resourceType) {
346 logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
347 "Enable advanced leak reporting to find out where the leak occurred. " +
348 "To enable advanced leak reporting, " +
349 "specify the JVM option '-D{}={}' or call {}.setLevel() " +
350 "See https://netty.io/wiki/reference-counted-objects.html for more information.",
351 resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
352 }
353
354
355
356
357 @Deprecated
358 protected void reportInstancesLeak(String resourceType) {
359 }
360
361
362
363
364
365
366 protected Object getInitialHint(String resourceType) {
367 return null;
368 }
369
370
371
372
373 public void setLeakListener(LeakListener leakListener) {
374 this.leakListener = leakListener;
375 }
376
377 public interface LeakListener {
378
379
380
381
382 void onLeak(String resourceType, String records);
383 }
384
385 @SuppressWarnings("deprecation")
386 private static final class DefaultResourceLeak<T>
387 extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
388
389 @SuppressWarnings("unchecked")
390 private static final AtomicReferenceFieldUpdater<DefaultResourceLeak<?>, TraceRecord> headUpdater =
391 (AtomicReferenceFieldUpdater)
392 AtomicReferenceFieldUpdater.newUpdater(DefaultResourceLeak.class, TraceRecord.class, "head");
393
394 @SuppressWarnings("unchecked")
395 private static final AtomicIntegerFieldUpdater<DefaultResourceLeak<?>> droppedRecordsUpdater =
396 (AtomicIntegerFieldUpdater)
397 AtomicIntegerFieldUpdater.newUpdater(DefaultResourceLeak.class, "droppedRecords");
398
399 @SuppressWarnings("unused")
400 private volatile TraceRecord head;
401 @SuppressWarnings("unused")
402 private volatile int droppedRecords;
403
404 private final Set<DefaultResourceLeak<?>> allLeaks;
405 private final int trackedHash;
406
407 DefaultResourceLeak(
408 Object referent,
409 ReferenceQueue<Object> refQueue,
410 Set<DefaultResourceLeak<?>> allLeaks,
411 Object initialHint) {
412 super(referent, refQueue);
413
414 assert referent != null;
415
416 this.allLeaks = allLeaks;
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 }
427
428 @Override
429 public void record() {
430 record0(null);
431 }
432
433 @Override
434 public void record(Object hint) {
435 record0(hint);
436 }
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 private void record0(Object hint) {
465
466 if (TARGET_RECORDS > 0) {
467 TraceRecord oldHead;
468 TraceRecord prevHead;
469 TraceRecord newHead;
470 boolean dropped;
471 do {
472 if ((prevHead = oldHead = headUpdater.get(this)) == null) {
473
474 return;
475 }
476 final int numElements = oldHead.pos + 1;
477 if (numElements >= TARGET_RECORDS) {
478 final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
479 dropped = ThreadLocalRandom.current().nextInt(1 << backOffFactor) != 0;
480 if (dropped) {
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 }