View Javadoc
1   /*
2    * Copyright 2013 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package io.netty.util;
18  
19  import io.netty.util.internal.PlatformDependent;
20  import io.netty.util.internal.SystemPropertyUtil;
21  import io.netty.util.internal.logging.InternalLogger;
22  import io.netty.util.internal.logging.InternalLoggerFactory;
23  
24  import java.lang.ref.PhantomReference;
25  import java.lang.ref.ReferenceQueue;
26  import java.util.ArrayDeque;
27  import java.util.Deque;
28  import java.util.EnumSet;
29  import java.util.concurrent.ConcurrentMap;
30  import java.util.concurrent.atomic.AtomicBoolean;
31  
32  import static io.netty.util.internal.StringUtil.*;
33  
34  public final class ResourceLeakDetector<T> {
35  
36      private static final String PROP_LEVEL = "io.netty.leakDetectionLevel";
37      private static final Level DEFAULT_LEVEL = Level.SIMPLE;
38  
39      /**
40       * Represents the level of resource leak detection.
41       */
42      public enum Level {
43          /**
44           * Disables resource leak detection.
45           */
46          DISABLED,
47          /**
48           * Enables simplistic sampling resource leak detection which reports there is a leak or not,
49           * at the cost of small overhead (default).
50           */
51          SIMPLE,
52          /**
53           * Enables advanced sampling resource leak detection which reports where the leaked object was accessed
54           * recently at the cost of high overhead.
55           */
56          ADVANCED,
57          /**
58           * Enables paranoid resource leak detection which reports where the leaked object was accessed recently,
59           * at the cost of the highest possible overhead (for testing purposes only).
60           */
61          PARANOID
62      }
63  
64      private static Level level;
65  
66      private static final InternalLogger logger = InternalLoggerFactory.getInstance(ResourceLeakDetector.class);
67  
68      static {
69          String levelStr = SystemPropertyUtil.get(PROP_LEVEL, DEFAULT_LEVEL.name()).trim().toUpperCase();
70          Level level = DEFAULT_LEVEL;
71          for (Level l: EnumSet.allOf(Level.class)) {
72              if (levelStr.equals(l.name()) || levelStr.equals(String.valueOf(l.ordinal()))) {
73                  level = l;
74              }
75          }
76  
77          ResourceLeakDetector.level = level;
78          if (logger.isDebugEnabled()) {
79              logger.debug("-D{}: {}", PROP_LEVEL, level.name().toLowerCase());
80          }
81      }
82  
83      private static final int DEFAULT_SAMPLING_INTERVAL = 113;
84  
85      /**
86       * Sets the resource leak detection level.
87       */
88      public static void setLevel(Level level) {
89          if (level == null) {
90              throw new NullPointerException("level");
91          }
92          ResourceLeakDetector.level = level;
93      }
94  
95      /**
96       * Returns the current resource leak detection level.
97       */
98      public static Level getLevel() {
99          return level;
100     }
101 
102     /** the linked list of active resources */
103     private final DefaultResourceLeak head = new DefaultResourceLeak(null);
104     private final DefaultResourceLeak tail = new DefaultResourceLeak(null);
105 
106     private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
107     private final ConcurrentMap<String, Boolean> reportedLeaks = PlatformDependent.newConcurrentHashMap();
108 
109     private final String resourceType;
110     private final int samplingInterval;
111     private final long maxActive;
112     private long active;
113     private final AtomicBoolean loggedTooManyActive = new AtomicBoolean();
114 
115     private long leakCheckCnt;
116 
117     public ResourceLeakDetector(Class<?> resourceType) {
118         this(simpleClassName(resourceType));
119     }
120 
121     public ResourceLeakDetector(String resourceType) {
122         this(resourceType, DEFAULT_SAMPLING_INTERVAL, Long.MAX_VALUE);
123     }
124 
125     public ResourceLeakDetector(Class<?> resourceType, int samplingInterval, long maxActive) {
126         this(simpleClassName(resourceType), samplingInterval, maxActive);
127     }
128 
129     public ResourceLeakDetector(String resourceType, int samplingInterval, long maxActive) {
130         if (resourceType == null) {
131             throw new NullPointerException("resourceType");
132         }
133         if (samplingInterval <= 0) {
134             throw new IllegalArgumentException("samplingInterval: " + samplingInterval + " (expected: 1+)");
135         }
136         if (maxActive <= 0) {
137             throw new IllegalArgumentException("maxActive: " + maxActive + " (expected: 1+)");
138         }
139 
140         this.resourceType = resourceType;
141         this.samplingInterval = samplingInterval;
142         this.maxActive = maxActive;
143 
144         head.next = tail;
145         tail.prev = head;
146     }
147 
148     /**
149      * Creates a new {@link ResourceLeak} which is expected to be closed via {@link ResourceLeak#close()} when the
150      * related resource is deallocated.
151      *
152      * @return the {@link ResourceLeak} or {@code null}
153      */
154     public ResourceLeak open(T obj) {
155         Level level = ResourceLeakDetector.level;
156         if (level == Level.DISABLED) {
157             return null;
158         }
159 
160         if (level.ordinal() < Level.PARANOID.ordinal()) {
161             if (leakCheckCnt ++ % samplingInterval == 0) {
162                 reportLeak(level);
163                 return new DefaultResourceLeak(obj);
164             } else {
165                 return null;
166             }
167         } else {
168             reportLeak(level);
169             return new DefaultResourceLeak(obj);
170         }
171     }
172 
173     private void reportLeak(Level level) {
174         if (!logger.isErrorEnabled()) {
175             for (;;) {
176                 @SuppressWarnings("unchecked")
177                 DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
178                 if (ref == null) {
179                     break;
180                 }
181                 ref.close();
182             }
183             return;
184         }
185 
186         // Report too many instances.
187         int samplingInterval = level == Level.PARANOID? 1 : this.samplingInterval;
188         if (active * samplingInterval > maxActive && loggedTooManyActive.compareAndSet(false, true)) {
189             logger.error("LEAK: You are creating too many " + resourceType + " instances.  " +
190                     resourceType + " is a shared resource that must be reused across the JVM," +
191                     "so that only a few instances are created.");
192         }
193 
194         // Detect and report previous leaks.
195         for (;;) {
196             @SuppressWarnings("unchecked")
197             DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
198             if (ref == null) {
199                 break;
200             }
201 
202             ref.clear();
203 
204             if (!ref.close()) {
205                 continue;
206             }
207 
208             String records = ref.toString();
209             if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
210                 if (records.isEmpty()) {
211                     logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
212                             "Enable advanced leak reporting to find out where the leak occurred. " +
213                             "To enable advanced leak reporting, " +
214                             "specify the JVM option '-D{}={}' or call {}.setLevel() " +
215                             "See http://netty.io/wiki/reference-counted-objects.html for more information.",
216                             resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
217                 } else {
218                     logger.error(
219                             "LEAK: {}.release() was not called before it's garbage-collected. " +
220                             "See http://netty.io/wiki/reference-counted-objects.html for more information.{}",
221                             resourceType, records);
222                 }
223             }
224         }
225     }
226 
227     private final class DefaultResourceLeak extends PhantomReference<Object> implements ResourceLeak {
228 
229         private static final int MAX_RECORDS = 4;
230 
231         private final String creationRecord;
232         private final Deque<String> lastRecords = new ArrayDeque<String>();
233         private final AtomicBoolean freed;
234         private DefaultResourceLeak prev;
235         private DefaultResourceLeak next;
236 
237         DefaultResourceLeak(Object referent) {
238             super(referent, referent != null? refQueue : null);
239 
240             if (referent != null) {
241                 Level level = getLevel();
242                 if (level.ordinal() >= Level.ADVANCED.ordinal()) {
243                     creationRecord = newRecord(null, 3);
244                 } else {
245                     creationRecord = null;
246                 }
247 
248                 // TODO: Use CAS to update the list.
249                 synchronized (head) {
250                     prev = head;
251                     next = head.next;
252                     head.next.prev = this;
253                     head.next = this;
254                     active ++;
255                 }
256                 freed = new AtomicBoolean();
257             } else {
258                 creationRecord = null;
259                 freed = new AtomicBoolean(true);
260             }
261         }
262 
263         @Override
264         public void record() {
265             record0(null, 3);
266         }
267 
268         @Override
269         public void record(Object hint) {
270             record0(hint, 3);
271         }
272 
273         private void record0(Object hint, int recordsToSkip) {
274             if (creationRecord != null) {
275                 String value = newRecord(hint, recordsToSkip);
276 
277                 synchronized (lastRecords) {
278                     int size = lastRecords.size();
279                     if (size == 0 || !lastRecords.getLast().equals(value)) {
280                         lastRecords.add(value);
281                     }
282                     if (size > MAX_RECORDS) {
283                         lastRecords.removeFirst();
284                     }
285                 }
286             }
287         }
288 
289         @Override
290         public boolean close() {
291             if (freed.compareAndSet(false, true)) {
292                 synchronized (head) {
293                     active --;
294                     prev.next = next;
295                     next.prev = prev;
296                     prev = null;
297                     next = null;
298                 }
299                 return true;
300             }
301             return false;
302         }
303 
304         @Override
305         public String toString() {
306             if (creationRecord == null) {
307                 return "";
308             }
309 
310             Object[] array;
311             synchronized (lastRecords) {
312                 array = lastRecords.toArray();
313             }
314 
315             StringBuilder buf = new StringBuilder(16384)
316                 .append(NEWLINE)
317                 .append("Recent access records: ")
318                 .append(array.length)
319                 .append(NEWLINE);
320 
321             if (array.length > 0) {
322                 for (int i = array.length - 1; i >= 0; i --) {
323                     buf.append('#')
324                        .append(i + 1)
325                        .append(':')
326                        .append(NEWLINE)
327                        .append(array[i]);
328                 }
329             }
330 
331             buf.append("Created at:")
332                .append(NEWLINE)
333                .append(creationRecord);
334 
335             buf.setLength(buf.length() - NEWLINE.length());
336             return buf.toString();
337         }
338     }
339 
340     private static final String[] STACK_TRACE_ELEMENT_EXCLUSIONS = {
341             "io.netty.util.ReferenceCountUtil.touch(",
342             "io.netty.buffer.AdvancedLeakAwareByteBuf.touch(",
343             "io.netty.buffer.AbstractByteBufAllocator.toLeakAwareBuffer(",
344     };
345 
346     static String newRecord(Object hint, int recordsToSkip) {
347         StringBuilder buf = new StringBuilder(4096);
348 
349         // Append the hint first if available.
350         if (hint != null) {
351             buf.append("\tHint: ");
352             // Prefer a hint string to a simple string form.
353             if (hint instanceof ResourceLeakHint) {
354                 buf.append(((ResourceLeakHint) hint).toHintString());
355             } else {
356                 buf.append(hint);
357             }
358             buf.append(NEWLINE);
359         }
360 
361         // Append the stack trace.
362         StackTraceElement[] array = new Throwable().getStackTrace();
363         for (StackTraceElement e: array) {
364             if (recordsToSkip > 0) {
365                 recordsToSkip --;
366             } else {
367                 String estr = e.toString();
368 
369                 // Strip the noisy stack trace elements.
370                 boolean excluded = false;
371                 for (String exclusion: STACK_TRACE_ELEMENT_EXCLUSIONS) {
372                     if (estr.startsWith(exclusion)) {
373                         excluded = true;
374                         break;
375                     }
376                 }
377 
378                 if (!excluded) {
379                     buf.append('\t');
380                     buf.append(estr);
381                     buf.append(NEWLINE);
382                 }
383             }
384         }
385 
386         return buf.toString();
387     }
388 }