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, DEFAULT_LEVEL.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 @Deprecated
178 public ResourceLeakDetector(Class<?> resourceType) {
179 this(simpleClassName(resourceType));
180 }
181
182
183
184
185 @Deprecated
186 public ResourceLeakDetector(String resourceType) {
187 this(resourceType, DEFAULT_SAMPLING_INTERVAL, Long.MAX_VALUE);
188 }
189
190
191
192
193
194
195
196
197
198
199 @Deprecated
200 public ResourceLeakDetector(Class<?> resourceType, int samplingInterval, long maxActive) {
201 this(resourceType, samplingInterval);
202 }
203
204
205
206
207
208
209 @SuppressWarnings("deprecation")
210 public ResourceLeakDetector(Class<?> resourceType, int samplingInterval) {
211 this(simpleClassName(resourceType), samplingInterval, Long.MAX_VALUE);
212 }
213
214
215
216
217
218
219 @Deprecated
220 public ResourceLeakDetector(String resourceType, int samplingInterval, long maxActive) {
221 this.resourceType = ObjectUtil.checkNotNull(resourceType, "resourceType");
222 this.samplingInterval = samplingInterval;
223 }
224
225
226
227
228
229
230
231
232 @Deprecated
233 public final ResourceLeak open(T obj) {
234 return track0(obj);
235 }
236
237
238
239
240
241
242
243 @SuppressWarnings("unchecked")
244 public final ResourceLeakTracker<T> track(T obj) {
245 return track0(obj);
246 }
247
248 @SuppressWarnings("unchecked")
249 private DefaultResourceLeak track0(T obj) {
250 Level level = ResourceLeakDetector.level;
251 if (level == Level.DISABLED) {
252 return null;
253 }
254
255 if (level.ordinal() < Level.PARANOID.ordinal()) {
256 if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) {
257 reportLeak();
258 return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
259 }
260 return null;
261 }
262 reportLeak();
263 return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
264 }
265
266 private void clearRefQueue() {
267 for (;;) {
268 DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
269 if (ref == null) {
270 break;
271 }
272 ref.dispose();
273 }
274 }
275
276
277
278
279
280
281
282 protected boolean needReport() {
283 return logger.isErrorEnabled();
284 }
285
286 private void reportLeak() {
287 if (!needReport()) {
288 clearRefQueue();
289 return;
290 }
291
292
293 for (;;) {
294 DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
295 if (ref == null) {
296 break;
297 }
298
299 if (!ref.dispose()) {
300 continue;
301 }
302
303 String records = ref.getReportAndClearRecords();
304 if (reportedLeaks.add(records)) {
305 if (records.isEmpty()) {
306 reportUntracedLeak(resourceType);
307 } else {
308 reportTracedLeak(resourceType, records);
309 }
310 }
311 }
312 }
313
314
315
316
317
318 protected void reportTracedLeak(String resourceType, String records) {
319 logger.error(
320 "LEAK: {}.release() was not called before it's garbage-collected. " +
321 "See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
322 resourceType, records);
323 }
324
325
326
327
328
329 protected void reportUntracedLeak(String resourceType) {
330 logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
331 "Enable advanced leak reporting to find out where the leak occurred. " +
332 "To enable advanced leak reporting, " +
333 "specify the JVM option '-D{}={}' or call {}.setLevel() " +
334 "See https://netty.io/wiki/reference-counted-objects.html for more information.",
335 resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
336 }
337
338
339
340
341 @Deprecated
342 protected void reportInstancesLeak(String resourceType) {
343 }
344
345
346
347
348
349
350 protected Object getInitialHint(String resourceType) {
351 return null;
352 }
353
354 @SuppressWarnings("deprecation")
355 private static final class DefaultResourceLeak<T>
356 extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
357
358 @SuppressWarnings("unchecked")
359 private static final AtomicReferenceFieldUpdater<DefaultResourceLeak<?>, TraceRecord> headUpdater =
360 (AtomicReferenceFieldUpdater)
361 AtomicReferenceFieldUpdater.newUpdater(DefaultResourceLeak.class, TraceRecord.class, "head");
362
363 @SuppressWarnings("unchecked")
364 private static final AtomicIntegerFieldUpdater<DefaultResourceLeak<?>> droppedRecordsUpdater =
365 (AtomicIntegerFieldUpdater)
366 AtomicIntegerFieldUpdater.newUpdater(DefaultResourceLeak.class, "droppedRecords");
367
368 @SuppressWarnings("unused")
369 private volatile TraceRecord head;
370 @SuppressWarnings("unused")
371 private volatile int droppedRecords;
372
373 private final Set<DefaultResourceLeak<?>> allLeaks;
374 private final int trackedHash;
375
376 DefaultResourceLeak(
377 Object referent,
378 ReferenceQueue<Object> refQueue,
379 Set<DefaultResourceLeak<?>> allLeaks,
380 Object initialHint) {
381 super(referent, refQueue);
382
383 assert referent != null;
384
385
386
387
388 trackedHash = System.identityHashCode(referent);
389 allLeaks.add(this);
390
391 headUpdater.set(this, initialHint == null ?
392 new TraceRecord(TraceRecord.BOTTOM) : new TraceRecord(TraceRecord.BOTTOM, initialHint));
393 this.allLeaks = allLeaks;
394 }
395
396 @Override
397 public void record() {
398 record0(null);
399 }
400
401 @Override
402 public void record(Object hint) {
403 record0(hint);
404 }
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432 private void record0(Object hint) {
433
434 if (TARGET_RECORDS > 0) {
435 TraceRecord oldHead;
436 TraceRecord prevHead;
437 TraceRecord newHead;
438 boolean dropped;
439 do {
440 if ((prevHead = oldHead = headUpdater.get(this)) == null) {
441
442 return;
443 }
444 final int numElements = oldHead.pos + 1;
445 if (numElements >= TARGET_RECORDS) {
446 final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
447 if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) {
448 prevHead = oldHead.next;
449 }
450 } else {
451 dropped = false;
452 }
453 newHead = hint != null ? new TraceRecord(prevHead, hint) : new TraceRecord(prevHead);
454 } while (!headUpdater.compareAndSet(this, oldHead, newHead));
455 if (dropped) {
456 droppedRecordsUpdater.incrementAndGet(this);
457 }
458 }
459 }
460
461 boolean dispose() {
462 clear();
463 return allLeaks.remove(this);
464 }
465
466 @Override
467 public boolean close() {
468 if (allLeaks.remove(this)) {
469
470 clear();
471 headUpdater.set(this, null);
472 return true;
473 }
474 return false;
475 }
476
477 @Override
478 public boolean close(T trackedObject) {
479
480 assert trackedHash == System.identityHashCode(trackedObject);
481
482 try {
483 return close();
484 } finally {
485
486
487
488
489 reachabilityFence0(trackedObject);
490 }
491 }
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512 private static void reachabilityFence0(Object ref) {
513 if (ref != null) {
514 synchronized (ref) {
515
516 }
517 }
518 }
519
520 @Override
521 public String toString() {
522 TraceRecord oldHead = headUpdater.get(this);
523 return generateReport(oldHead);
524 }
525
526 String getReportAndClearRecords() {
527 TraceRecord oldHead = headUpdater.getAndSet(this, null);
528 return generateReport(oldHead);
529 }
530
531 private String generateReport(TraceRecord oldHead) {
532 if (oldHead == null) {
533
534 return EMPTY_STRING;
535 }
536
537 final int dropped = droppedRecordsUpdater.get(this);
538 int duped = 0;
539
540 int present = oldHead.pos + 1;
541
542 StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
543 buf.append("Recent access records: ").append(NEWLINE);
544
545 int i = 1;
546 Set<String> seen = new HashSet<String>(present);
547 for (; oldHead != TraceRecord.BOTTOM; oldHead = oldHead.next) {
548 String s = oldHead.toString();
549 if (seen.add(s)) {
550 if (oldHead.next == TraceRecord.BOTTOM) {
551 buf.append("Created at:").append(NEWLINE).append(s);
552 } else {
553 buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
554 }
555 } else {
556 duped++;
557 }
558 }
559
560 if (duped > 0) {
561 buf.append(": ")
562 .append(duped)
563 .append(" leak records were discarded because they were duplicates")
564 .append(NEWLINE);
565 }
566
567 if (dropped > 0) {
568 buf.append(": ")
569 .append(dropped)
570 .append(" leak records were discarded because the leak record count is targeted to ")
571 .append(TARGET_RECORDS)
572 .append(". Use system property ")
573 .append(PROP_TARGET_RECORDS)
574 .append(" to increase the limit.")
575 .append(NEWLINE);
576 }
577
578 buf.setLength(buf.length() - NEWLINE.length());
579 return buf.toString();
580 }
581 }
582
583 private static final AtomicReference<String[]> excludedMethods =
584 new AtomicReference<String[]>(EmptyArrays.EMPTY_STRINGS);
585
586 public static void addExclusions(Class clz, String ... methodNames) {
587 Set<String> nameSet = new HashSet<String>(Arrays.asList(methodNames));
588
589
590 for (Method method : clz.getDeclaredMethods()) {
591 if (nameSet.remove(method.getName()) && nameSet.isEmpty()) {
592 break;
593 }
594 }
595 if (!nameSet.isEmpty()) {
596 throw new IllegalArgumentException("Can't find '" + nameSet + "' in " + clz.getName());
597 }
598 String[] oldMethods;
599 String[] newMethods;
600 do {
601 oldMethods = excludedMethods.get();
602 newMethods = Arrays.copyOf(oldMethods, oldMethods.length + 2 * methodNames.length);
603 for (int i = 0; i < methodNames.length; i++) {
604 newMethods[oldMethods.length + i * 2] = clz.getName();
605 newMethods[oldMethods.length + i * 2 + 1] = methodNames[i];
606 }
607 } while (!excludedMethods.compareAndSet(oldMethods, newMethods));
608 }
609
610 private static class TraceRecord extends Throwable {
611 private static final long serialVersionUID = 6065153674892850720L;
612
613 private static final TraceRecord BOTTOM = new TraceRecord() {
614 private static final long serialVersionUID = 7396077602074694571L;
615
616
617
618
619 @Override
620 public Throwable fillInStackTrace() {
621 return this;
622 }
623 };
624
625 private final String hintString;
626 private final TraceRecord next;
627 private final int pos;
628
629 TraceRecord(TraceRecord next, Object hint) {
630
631 hintString = hint instanceof ResourceLeakHint ? ((ResourceLeakHint) hint).toHintString() : hint.toString();
632 this.next = next;
633 this.pos = next.pos + 1;
634 }
635
636 TraceRecord(TraceRecord next) {
637 hintString = null;
638 this.next = next;
639 this.pos = next.pos + 1;
640 }
641
642
643 private TraceRecord() {
644 hintString = null;
645 next = null;
646 pos = -1;
647 }
648
649 @Override
650 public String toString() {
651 StringBuilder buf = new StringBuilder(2048);
652 if (hintString != null) {
653 buf.append("\tHint: ").append(hintString).append(NEWLINE);
654 }
655
656
657 StackTraceElement[] array = getStackTrace();
658
659 out: for (int i = 3; i < array.length; i++) {
660 StackTraceElement element = array[i];
661
662 String[] exclusions = excludedMethods.get();
663 for (int k = 0; k < exclusions.length; k += 2) {
664
665
666 if (exclusions[k].equals(element.getClassName())
667 && exclusions[k + 1].equals(element.getMethodName())) {
668 continue out;
669 }
670 }
671
672 buf.append('\t');
673 buf.append(element.toString());
674 buf.append(NEWLINE);
675 }
676 return buf.toString();
677 }
678 }
679 }