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.PlatformDependent;
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.WeakReference;
26 import java.lang.ref.ReferenceQueue;
27 import java.lang.reflect.Method;
28 import java.util.Arrays;
29 import java.util.HashSet;
30 import java.util.Set;
31 import java.util.concurrent.ConcurrentMap;
32 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
33 import java.util.concurrent.atomic.AtomicReference;
34 import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
35
36 import static io.netty.util.internal.StringUtil.EMPTY_STRING;
37 import static io.netty.util.internal.StringUtil.NEWLINE;
38 import static io.netty.util.internal.StringUtil.simpleClassName;
39
40 public class ResourceLeakDetector<T> {
41
42 private static final String PROP_LEVEL_OLD = "io.netty.leakDetectionLevel";
43 private static final String PROP_LEVEL = "io.netty.leakDetection.level";
44 private static final Level DEFAULT_LEVEL = Level.SIMPLE;
45
46 private static final String PROP_TARGET_RECORDS = "io.netty.leakDetection.targetRecords";
47 private static final int DEFAULT_TARGET_RECORDS = 4;
48
49 private static final int TARGET_RECORDS;
50
51
52
53
54 public enum Level {
55
56
57
58 DISABLED,
59
60
61
62
63 SIMPLE,
64
65
66
67
68 ADVANCED,
69
70
71
72
73 PARANOID;
74
75
76
77
78
79
80
81 static Level parseLevel(String levelStr) {
82 String trimmedLevelStr = levelStr.trim();
83 for (Level l : values()) {
84 if (trimmedLevelStr.equalsIgnoreCase(l.name()) || trimmedLevelStr.equals(String.valueOf(l.ordinal()))) {
85 return l;
86 }
87 }
88 return DEFAULT_LEVEL;
89 }
90 }
91
92 private static Level level;
93
94 private static final InternalLogger logger = InternalLoggerFactory.getInstance(ResourceLeakDetector.class);
95
96 static {
97 final boolean disabled;
98 if (SystemPropertyUtil.get("io.netty.noResourceLeakDetection") != null) {
99 disabled = SystemPropertyUtil.getBoolean("io.netty.noResourceLeakDetection", false);
100 logger.debug("-Dio.netty.noResourceLeakDetection: {}", disabled);
101 logger.warn(
102 "-Dio.netty.noResourceLeakDetection is deprecated. Use '-D{}={}' instead.",
103 PROP_LEVEL, DEFAULT_LEVEL.name().toLowerCase());
104 } else {
105 disabled = false;
106 }
107
108 Level defaultLevel = disabled? Level.DISABLED : DEFAULT_LEVEL;
109
110
111 String levelStr = SystemPropertyUtil.get(PROP_LEVEL_OLD, defaultLevel.name());
112
113
114 levelStr = SystemPropertyUtil.get(PROP_LEVEL, levelStr);
115 Level level = Level.parseLevel(levelStr);
116
117 TARGET_RECORDS = SystemPropertyUtil.getInt(PROP_TARGET_RECORDS, DEFAULT_TARGET_RECORDS);
118
119 ResourceLeakDetector.level = level;
120 if (logger.isDebugEnabled()) {
121 logger.debug("-D{}: {}", PROP_LEVEL, level.name().toLowerCase());
122 logger.debug("-D{}: {}", PROP_TARGET_RECORDS, TARGET_RECORDS);
123 }
124 }
125
126
127 static final int DEFAULT_SAMPLING_INTERVAL = 128;
128
129
130
131
132 @Deprecated
133 public static void setEnabled(boolean enabled) {
134 setLevel(enabled? Level.SIMPLE : Level.DISABLED);
135 }
136
137
138
139
140 public static boolean isEnabled() {
141 return getLevel().ordinal() > Level.DISABLED.ordinal();
142 }
143
144
145
146
147 public static void setLevel(Level level) {
148 if (level == null) {
149 throw new NullPointerException("level");
150 }
151 ResourceLeakDetector.level = level;
152 }
153
154
155
156
157 public static Level getLevel() {
158 return level;
159 }
160
161
162 private final ConcurrentMap<DefaultResourceLeak<?>, LeakEntry> allLeaks = PlatformDependent.newConcurrentHashMap();
163
164 private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
165 private final ConcurrentMap<String, Boolean> reportedLeaks = PlatformDependent.newConcurrentHashMap();
166
167 private final String resourceType;
168 private final int samplingInterval;
169
170
171
172
173 @Deprecated
174 public ResourceLeakDetector(Class<?> resourceType) {
175 this(simpleClassName(resourceType));
176 }
177
178
179
180
181 @Deprecated
182 public ResourceLeakDetector(String resourceType) {
183 this(resourceType, DEFAULT_SAMPLING_INTERVAL, Long.MAX_VALUE);
184 }
185
186
187
188
189
190
191
192
193
194
195 @Deprecated
196 public ResourceLeakDetector(Class<?> resourceType, int samplingInterval, long maxActive) {
197 this(resourceType, samplingInterval);
198 }
199
200
201
202
203
204
205 @SuppressWarnings("deprecation")
206 public ResourceLeakDetector(Class<?> resourceType, int samplingInterval) {
207 this(simpleClassName(resourceType), samplingInterval, Long.MAX_VALUE);
208 }
209
210
211
212
213
214
215 @Deprecated
216 public ResourceLeakDetector(String resourceType, int samplingInterval, long maxActive) {
217 if (resourceType == null) {
218 throw new NullPointerException("resourceType");
219 }
220
221 this.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);
259 }
260 return null;
261 }
262 reportLeak();
263 return new DefaultResourceLeak(obj, refQueue, allLeaks);
264 }
265
266 private void clearRefQueue() {
267 for (;;) {
268 @SuppressWarnings("unchecked")
269 DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
270 if (ref == null) {
271 break;
272 }
273 ref.dispose();
274 }
275 }
276
277 private void reportLeak() {
278 if (!logger.isErrorEnabled()) {
279 clearRefQueue();
280 return;
281 }
282
283
284 for (;;) {
285 @SuppressWarnings("unchecked")
286 DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
287 if (ref == null) {
288 break;
289 }
290
291 if (!ref.dispose()) {
292 continue;
293 }
294
295 String records = ref.toString();
296 if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
297 if (records.isEmpty()) {
298 reportUntracedLeak(resourceType);
299 } else {
300 reportTracedLeak(resourceType, records);
301 }
302 }
303 }
304 }
305
306
307
308
309
310 protected void reportTracedLeak(String resourceType, String records) {
311 logger.error(
312 "LEAK: {}.release() was not called before it's garbage-collected. " +
313 "See http://netty.io/wiki/reference-counted-objects.html for more information.{}",
314 resourceType, records);
315 }
316
317
318
319
320
321 protected void reportUntracedLeak(String resourceType) {
322 logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
323 "Enable advanced leak reporting to find out where the leak occurred. " +
324 "To enable advanced leak reporting, " +
325 "specify the JVM option '-D{}={}' or call {}.setLevel() " +
326 "See http://netty.io/wiki/reference-counted-objects.html for more information.",
327 resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
328 }
329
330
331
332
333 @Deprecated
334 protected void reportInstancesLeak(String resourceType) {
335 }
336
337 @SuppressWarnings("deprecation")
338 private static final class DefaultResourceLeak<T>
339 extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
340
341 @SuppressWarnings("unchecked")
342 private static final AtomicReferenceFieldUpdater<DefaultResourceLeak<?>, Record> headUpdater =
343 (AtomicReferenceFieldUpdater)
344 AtomicReferenceFieldUpdater.newUpdater(DefaultResourceLeak.class, Record.class, "head");
345
346 @SuppressWarnings("unchecked")
347 private static final AtomicIntegerFieldUpdater<DefaultResourceLeak<?>> droppedRecordsUpdater =
348 (AtomicIntegerFieldUpdater)
349 AtomicIntegerFieldUpdater.newUpdater(DefaultResourceLeak.class, "droppedRecords");
350
351 @SuppressWarnings("unused")
352 private volatile Record head;
353 @SuppressWarnings("unused")
354 private volatile int droppedRecords;
355
356 private final ConcurrentMap<DefaultResourceLeak<?>, LeakEntry> allLeaks;
357 private final int trackedHash;
358
359 DefaultResourceLeak(
360 Object referent,
361 ReferenceQueue<Object> refQueue,
362 ConcurrentMap<DefaultResourceLeak<?>, LeakEntry> allLeaks) {
363 super(referent, refQueue);
364
365 assert referent != null;
366
367
368
369
370 trackedHash = System.identityHashCode(referent);
371 allLeaks.put(this, LeakEntry.INSTANCE);
372 headUpdater.set(this, Record.BOTTOM);
373 this.allLeaks = allLeaks;
374 }
375
376 @Override
377 public void record() {
378 record0(null);
379 }
380
381 @Override
382 public void record(Object hint) {
383 record0(hint);
384 }
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412 private void record0(Object hint) {
413
414 if (TARGET_RECORDS > 0) {
415 Record oldHead;
416 Record prevHead;
417 Record newHead;
418 boolean dropped;
419 do {
420 if ((prevHead = oldHead = headUpdater.get(this)) == null) {
421
422 return;
423 }
424 final int numElements = oldHead.pos + 1;
425 if (numElements >= TARGET_RECORDS) {
426 final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
427 if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) {
428 prevHead = oldHead.next;
429 }
430 } else {
431 dropped = false;
432 }
433 newHead = hint != null ? new Record(prevHead, hint) : new Record(prevHead);
434 } while (!headUpdater.compareAndSet(this, oldHead, newHead));
435 if (dropped) {
436 droppedRecordsUpdater.incrementAndGet(this);
437 }
438 }
439 }
440
441 boolean dispose() {
442 clear();
443 return allLeaks.remove(this, LeakEntry.INSTANCE);
444 }
445
446 @Override
447 public boolean close() {
448
449 if (allLeaks.remove(this, LeakEntry.INSTANCE)) {
450
451 clear();
452 headUpdater.set(this, null);
453 return true;
454 }
455 return false;
456 }
457
458 @Override
459 public boolean close(T trackedObject) {
460
461 assert trackedHash == System.identityHashCode(trackedObject);
462
463
464
465
466
467 return close() && trackedObject != null;
468 }
469
470 @Override
471 public String toString() {
472 Record oldHead = headUpdater.getAndSet(this, null);
473 if (oldHead == null) {
474
475 return EMPTY_STRING;
476 }
477
478 final int dropped = droppedRecordsUpdater.get(this);
479 int duped = 0;
480
481 int present = oldHead.pos + 1;
482
483 StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
484 buf.append("Recent access records: ").append(NEWLINE);
485
486 int i = 1;
487 Set<String> seen = new HashSet<String>(present);
488 for (; oldHead != Record.BOTTOM; oldHead = oldHead.next) {
489 String s = oldHead.toString();
490 if (seen.add(s)) {
491 if (oldHead.next == Record.BOTTOM) {
492 buf.append("Created at:").append(NEWLINE).append(s);
493 } else {
494 buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
495 }
496 } else {
497 duped++;
498 }
499 }
500
501 if (duped > 0) {
502 buf.append(": ")
503 .append(dropped)
504 .append(" leak records were discarded because they were duplicates")
505 .append(NEWLINE);
506 }
507
508 if (dropped > 0) {
509 buf.append(": ")
510 .append(dropped)
511 .append(" leak records were discarded because the leak record count is targeted to ")
512 .append(TARGET_RECORDS)
513 .append(". Use system property ")
514 .append(PROP_TARGET_RECORDS)
515 .append(" to increase the limit.")
516 .append(NEWLINE);
517 }
518
519 buf.setLength(buf.length() - NEWLINE.length());
520 return buf.toString();
521 }
522 }
523
524 private static final AtomicReference<String[]> excludedMethods =
525 new AtomicReference<String[]>(EmptyArrays.EMPTY_STRINGS);
526
527 public static void addExclusions(Class clz, String ... methodNames) {
528 Set<String> nameSet = new HashSet<String>(Arrays.asList(methodNames));
529
530
531 for (Method method : clz.getDeclaredMethods()) {
532 if (nameSet.remove(method.getName()) && nameSet.isEmpty()) {
533 break;
534 }
535 }
536 if (!nameSet.isEmpty()) {
537 throw new IllegalArgumentException("Can't find '" + nameSet + "' in " + clz.getName());
538 }
539 String[] oldMethods;
540 String[] newMethods;
541 do {
542 oldMethods = excludedMethods.get();
543 newMethods = Arrays.copyOf(oldMethods, oldMethods.length + 2 * methodNames.length);
544 for (int i = 0; i < methodNames.length; i++) {
545 newMethods[oldMethods.length + i * 2] = clz.getName();
546 newMethods[oldMethods.length + i * 2 + 1] = methodNames[i];
547 }
548 } while (!excludedMethods.compareAndSet(oldMethods, newMethods));
549 }
550
551 private static final class Record extends Throwable {
552 private static final long serialVersionUID = 6065153674892850720L;
553
554 private static final Record BOTTOM = new Record();
555
556 private final String hintString;
557 private final Record next;
558 private final int pos;
559
560 Record(Record next, Object hint) {
561
562 hintString = hint instanceof ResourceLeakHint ? ((ResourceLeakHint) hint).toHintString() : hint.toString();
563 this.next = next;
564 this.pos = next.pos + 1;
565 }
566
567 Record(Record next) {
568 hintString = null;
569 this.next = next;
570 this.pos = next.pos + 1;
571 }
572
573
574 private Record() {
575 hintString = null;
576 next = null;
577 pos = -1;
578 }
579
580 @Override
581 public String toString() {
582 StringBuilder buf = new StringBuilder(2048);
583 if (hintString != null) {
584 buf.append("\tHint: ").append(hintString).append(NEWLINE);
585 }
586
587
588 StackTraceElement[] array = getStackTrace();
589
590 out: for (int i = 3; i < array.length; i++) {
591 StackTraceElement element = array[i];
592
593 String[] exclusions = excludedMethods.get();
594 for (int k = 0; k < exclusions.length; k += 2) {
595 if (exclusions[k].equals(element.getClassName())
596 && exclusions[k + 1].equals(element.getMethodName())) {
597 continue out;
598 }
599 }
600
601 buf.append('\t');
602 buf.append(element.toString());
603 buf.append(NEWLINE);
604 }
605 return buf.toString();
606 }
607 }
608
609 private static final class LeakEntry {
610 static final LeakEntry INSTANCE = new LeakEntry();
611 private static final int HASH = System.identityHashCode(INSTANCE);
612
613 private LeakEntry() {
614 }
615
616 @Override
617 public int hashCode() {
618 return HASH;
619 }
620
621 @Override
622 public boolean equals(Object obj) {
623 return obj == this;
624 }
625 }
626 }