1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package io.netty5.util;
18
19 import io.netty5.util.internal.EmptyArrays;
20 import io.netty5.util.internal.SystemPropertyUtil;
21 import io.netty5.util.internal.logging.InternalLogger;
22 import io.netty5.util.internal.logging.InternalLoggerFactory;
23
24 import java.lang.ref.Reference;
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.netty5.util.internal.StringUtil.EMPTY_STRING;
39 import static io.netty5.util.internal.StringUtil.NEWLINE;
40 import static io.netty5.util.internal.StringUtil.simpleClassName;
41 import static java.util.Objects.requireNonNull;
42
43 public class ResourceLeakDetector<T> {
44
45 private static final String PROP_LEVEL = "io.netty5.leakDetection.level";
46 private static final Level DEFAULT_LEVEL = Level.SIMPLE;
47
48 private static final String PROP_TARGET_RECORDS = "io.netty5.leakDetection.targetRecords";
49 private static final int DEFAULT_TARGET_RECORDS = 4;
50
51 private static final String PROP_SAMPLING_INTERVAL = "io.netty5.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 String levelStr = SystemPropertyUtil.get(PROP_LEVEL, DEFAULT_LEVEL.name());
105 Level level = Level.parseLevel(levelStr);
106
107 TARGET_RECORDS = SystemPropertyUtil.getInt(PROP_TARGET_RECORDS, DEFAULT_TARGET_RECORDS);
108 SAMPLING_INTERVAL = SystemPropertyUtil.getInt(PROP_SAMPLING_INTERVAL, DEFAULT_SAMPLING_INTERVAL);
109
110 ResourceLeakDetector.level = level;
111 if (logger.isDebugEnabled()) {
112 logger.debug("-D{}: {}", PROP_LEVEL, level.name().toLowerCase());
113 logger.debug("-D{}: {}", PROP_TARGET_RECORDS, TARGET_RECORDS);
114 }
115 }
116
117
118
119
120 public static boolean isEnabled() {
121 return getLevel().ordinal() > Level.DISABLED.ordinal();
122 }
123
124
125
126
127 public static void setLevel(Level level) {
128 requireNonNull(level, "level");
129 ResourceLeakDetector.level = level;
130 }
131
132
133
134
135 public static Level getLevel() {
136 return level;
137 }
138
139
140 private final Set<DefaultResourceLeak<?>> allLeaks = ConcurrentHashMap.newKeySet();
141
142 private final ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
143 private final Set<String> reportedLeaks =
144 Collections.newSetFromMap(new ConcurrentHashMap<>());
145 private final String resourceType;
146 private final int samplingInterval;
147
148
149
150
151
152
153 public ResourceLeakDetector(Class<?> resourceType, int samplingInterval) {
154 this.resourceType = simpleClassName(resourceType);
155 this.samplingInterval = samplingInterval;
156 }
157
158
159
160
161
162
163
164 @SuppressWarnings("unchecked")
165 public final ResourceLeakTracker<T> track(T obj) {
166 return track0(obj);
167 }
168
169 @SuppressWarnings("unchecked")
170 private DefaultResourceLeak track0(T obj) {
171 Level level = ResourceLeakDetector.level;
172 if (level == Level.DISABLED) {
173 return null;
174 }
175
176 if (level.ordinal() < Level.PARANOID.ordinal()) {
177 if (ThreadLocalRandom.current().nextInt(samplingInterval) == 0) {
178 reportLeak();
179 return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
180 }
181 return null;
182 }
183 reportLeak();
184 return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
185 }
186
187 private void clearRefQueue() {
188 for (;;) {
189 DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
190 if (ref == null) {
191 break;
192 }
193 ref.dispose();
194 }
195 }
196
197
198
199
200
201
202
203 protected boolean needReport() {
204 return logger.isErrorEnabled();
205 }
206
207 private void reportLeak() {
208 if (!needReport()) {
209 clearRefQueue();
210 return;
211 }
212
213
214 for (;;) {
215 DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
216 if (ref == null) {
217 break;
218 }
219
220 if (!ref.dispose()) {
221 continue;
222 }
223
224 String records = ref.getReportAndClearRecords();
225 if (reportedLeaks.add(records)) {
226 if (records.isEmpty()) {
227 reportUntracedLeak(resourceType);
228 } else {
229 reportTracedLeak(resourceType, records);
230 }
231 }
232 }
233 }
234
235
236
237
238
239 protected void reportTracedLeak(String resourceType, String records) {
240 logger.error(
241 "LEAK: {}.release() was not called before it's garbage-collected. " +
242 "See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
243 resourceType, records);
244 }
245
246
247
248
249
250 protected void reportUntracedLeak(String resourceType) {
251 logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
252 "Enable advanced leak reporting to find out where the leak occurred. " +
253 "To enable advanced leak reporting, " +
254 "specify the JVM option '-D{}={}' or call {}.setLevel() " +
255 "See https://netty.io/wiki/reference-counted-objects.html for more information.",
256 resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
257 }
258
259
260
261
262 @Deprecated
263 protected void reportInstancesLeak(String resourceType) {
264 }
265
266
267
268
269
270
271 protected Object getInitialHint(String resourceType) {
272 return null;
273 }
274
275 private static final class DefaultResourceLeak<T>
276 extends WeakReference<Object> implements ResourceLeakTracker<T> {
277
278 @SuppressWarnings("unchecked")
279 private static final AtomicReferenceFieldUpdater<DefaultResourceLeak<?>, TraceRecord> headUpdater =
280 (AtomicReferenceFieldUpdater)
281 AtomicReferenceFieldUpdater.newUpdater(DefaultResourceLeak.class, TraceRecord.class, "head");
282
283 @SuppressWarnings("unchecked")
284 private static final AtomicIntegerFieldUpdater<DefaultResourceLeak<?>> droppedRecordsUpdater =
285 (AtomicIntegerFieldUpdater)
286 AtomicIntegerFieldUpdater.newUpdater(DefaultResourceLeak.class, "droppedRecords");
287
288 @SuppressWarnings("unused")
289 private volatile TraceRecord head;
290 @SuppressWarnings("unused")
291 private volatile int droppedRecords;
292
293 private final Set<DefaultResourceLeak<?>> allLeaks;
294 private final int trackedHash;
295
296 DefaultResourceLeak(
297 Object referent,
298 ReferenceQueue<Object> refQueue,
299 Set<DefaultResourceLeak<?>> allLeaks,
300 Object initialHint) {
301 super(referent, refQueue);
302
303 assert referent != null;
304
305
306
307
308 trackedHash = System.identityHashCode(referent);
309 allLeaks.add(this);
310
311 headUpdater.set(this, initialHint == null ?
312 new TraceRecord(TraceRecord.BOTTOM) : new TraceRecord(TraceRecord.BOTTOM, initialHint));
313 this.allLeaks = allLeaks;
314 }
315
316 @Override
317 public void record() {
318 record0(null);
319 }
320
321 @Override
322 public void record(Object hint) {
323 record0(hint);
324 }
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352 private void record0(Object hint) {
353
354 if (TARGET_RECORDS > 0) {
355 TraceRecord oldHead;
356 TraceRecord prevHead;
357 TraceRecord newHead;
358 boolean dropped;
359 do {
360 if ((prevHead = oldHead = headUpdater.get(this)) == null) {
361
362 return;
363 }
364 final int numElements = oldHead.pos + 1;
365 if (numElements >= TARGET_RECORDS) {
366 final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
367 dropped = ThreadLocalRandom.current().nextInt(1 << backOffFactor) != 0;
368 if (dropped) {
369 prevHead = oldHead.next;
370 }
371 } else {
372 dropped = false;
373 }
374 newHead = hint != null ? new TraceRecord(prevHead, hint) : new TraceRecord(prevHead);
375 } while (!headUpdater.compareAndSet(this, oldHead, newHead));
376 if (dropped) {
377 droppedRecordsUpdater.incrementAndGet(this);
378 }
379 }
380 }
381
382 boolean dispose() {
383 clear();
384 return allLeaks.remove(this);
385 }
386
387 @Override
388 public boolean close(T trackedObject) {
389
390 assert trackedHash == System.identityHashCode(trackedObject);
391
392 try {
393 if (allLeaks.remove(this)) {
394
395 clear();
396 headUpdater.set(this, null);
397 return true;
398 }
399 return false;
400 } finally {
401
402 Reference.reachabilityFence(trackedObject);
403 }
404 }
405
406 @Override
407 public String toString() {
408 TraceRecord oldHead = headUpdater.get(this);
409 return generateReport(oldHead);
410 }
411
412 String getReportAndClearRecords() {
413 TraceRecord oldHead = headUpdater.getAndSet(this, null);
414 return generateReport(oldHead);
415 }
416
417 private String generateReport(TraceRecord oldHead) {
418 if (oldHead == null) {
419
420 return EMPTY_STRING;
421 }
422
423 final int dropped = droppedRecordsUpdater.get(this);
424 int duped = 0;
425
426 int present = oldHead.pos + 1;
427
428 StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
429 buf.append("Recent access records: ").append(NEWLINE);
430
431 int i = 1;
432 Set<String> seen = new HashSet<>(present);
433 for (; oldHead != TraceRecord.BOTTOM; oldHead = oldHead.next) {
434 String s = oldHead.toString();
435 if (seen.add(s)) {
436 if (oldHead.next == TraceRecord.BOTTOM) {
437 buf.append("Created at:").append(NEWLINE).append(s);
438 } else {
439 buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
440 }
441 } else {
442 duped++;
443 }
444 }
445
446 if (duped > 0) {
447 buf.append(": ")
448 .append(duped)
449 .append(" leak records were discarded because they were duplicates")
450 .append(NEWLINE);
451 }
452
453 if (dropped > 0) {
454 buf.append(": ")
455 .append(dropped)
456 .append(" leak records were discarded because the leak record count is targeted to ")
457 .append(TARGET_RECORDS)
458 .append(". Use system property ")
459 .append(PROP_TARGET_RECORDS)
460 .append(" to increase the limit.")
461 .append(NEWLINE);
462 }
463
464 buf.setLength(buf.length() - NEWLINE.length());
465 return buf.toString();
466 }
467 }
468
469 private static final AtomicReference<String[]> excludedMethods =
470 new AtomicReference<>(EmptyArrays.EMPTY_STRINGS);
471
472 public static void addExclusions(Class<?> clz, String ... methodNames) {
473 Set<String> nameSet = new HashSet<>(Arrays.asList(methodNames));
474
475
476 for (Method method : clz.getDeclaredMethods()) {
477 if (nameSet.remove(method.getName()) && nameSet.isEmpty()) {
478 break;
479 }
480 }
481 if (!nameSet.isEmpty()) {
482 throw new IllegalArgumentException("Can't find '" + nameSet + "' in " + clz.getName());
483 }
484 String[] oldMethods;
485 String[] newMethods;
486 do {
487 oldMethods = excludedMethods.get();
488 newMethods = Arrays.copyOf(oldMethods, oldMethods.length + 2 * methodNames.length);
489 for (int i = 0; i < methodNames.length; i++) {
490 newMethods[oldMethods.length + i * 2] = clz.getName();
491 newMethods[oldMethods.length + i * 2 + 1] = methodNames[i];
492 }
493 } while (!excludedMethods.compareAndSet(oldMethods, newMethods));
494 }
495
496 private static class TraceRecord extends Throwable {
497 private static final long serialVersionUID = 6065153674892850720L;
498
499 private static final TraceRecord BOTTOM = new TraceRecord();
500
501 private final String hintString;
502 private final TraceRecord next;
503 private final int pos;
504
505 TraceRecord(TraceRecord next, Object hint) {
506
507 hintString = hint instanceof ResourceLeakHint ? ((ResourceLeakHint) hint).toHintString() : hint.toString();
508 this.next = next;
509 pos = next.pos + 1;
510 }
511
512 TraceRecord(TraceRecord next) {
513 hintString = null;
514 this.next = next;
515 pos = next.pos + 1;
516 }
517
518
519 private TraceRecord() {
520 super(null, null, false, false);
521 hintString = null;
522 next = null;
523 pos = -1;
524 }
525
526 @Override
527 public String toString() {
528 StringBuilder buf = new StringBuilder(2048);
529 if (hintString != null) {
530 buf.append("\tHint: ").append(hintString).append(NEWLINE);
531 }
532
533
534 StackTraceElement[] array = getStackTrace();
535
536 out: for (int i = 3; i < array.length; i++) {
537 StackTraceElement element = array[i];
538
539 String[] exclusions = excludedMethods.get();
540 for (int k = 0; k < exclusions.length; k += 2) {
541
542
543 if (exclusions[k].equals(element.getClassName())
544 && exclusions[k + 1].equals(element.getMethodName())) {
545 continue out;
546 }
547 }
548
549 buf.append('\t');
550 buf.append(element.toString());
551 buf.append(NEWLINE);
552 }
553 return buf.toString();
554 }
555 }
556 }