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 }