This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-extensions-classloader-leak-detector.git
commit 14169705f4b4e5a772f4ead22cbd3ccb257bee4e Author: Chetan Mehrotra <[email protected]> AuthorDate: Fri Jan 31 11:52:08 2014 +0000 SLING-3359 - Classloader Leak Detector Console Tab Initial commit. git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1563112 13f79535-47bb-0310-9956-ffa450edef68 --- README.md | 27 ++ pom.xml | 83 ++++++ .../leakdetector/internal/LeakDetector.java | 304 +++++++++++++++++++++ 3 files changed, 414 insertions(+) diff --git a/README.md b/README.md new file mode 100644 index 0000000..3de6411 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +Sling Classloader Leak Detector +=============================== + +This bundle provides support for tracing classloader leaks which occur due to +improper cleanup in bundles. Refer to [SLING-3359][1] for background details + +The bundle registers a Felix Configuration Printer which dumps out a list of +suspected classloaders which are not getting garbage collected. it can be accessed +at http://localhost:8080/system/console/status-leakdetector + + Possible classloader leak detected + Number of suspicious bundles - 1 + + * org.apache.sling.sample.leakdetector.bad-bundle (0.0.1.SNAPSHOT) - Classloader Count [2] + - Bundle Id - 204 + - Leaked classloaders + - Identity HashCode - 4a273519, Creation time 31.01.2014 15:22:58.407 + +### JVM Arguments + + By default on Oracle JDK the classloaders and related classes from Permgen are + not garbage collected by default. This bundle relies on Classloaders getting + gced for it work. So to enable that pass on following arguments + + -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled + +[1]: https://issues.apache.org/jira/browse/SLING-3359 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..537b8e2 --- /dev/null +++ b/pom.xml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<!-- +/************************************************************************* + * + * ADOBE CONFIDENTIAL + * __________________ + * + * Copyright 2012 Adobe Systems Incorporated + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe Systems Incorporated and its suppliers, + * if any. The intellectual and technical concepts contained + * herein are proprietary to Adobe Systems Incorporated and its + * suppliers and are protected by trade secret or copyright law. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe Systems Incorporated. + **************************************************************************/ +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.sling</groupId> + <artifactId>sling</artifactId> + <version>18</version> + </parent> + + <artifactId>org.apache.sling.extensions.classloader-leak-detector</artifactId> + <packaging>bundle</packaging> + <version>0.0.1-SNAPSHOT</version> + + <name>Adobe Sling ClassLoader Leak Detector</name> + <description> + Provides a web console configuration printer to provide details around classloader leaks + </description> + + <scm> + <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/contrib/extensions/leak-detector</connection> + <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/contrib/extensions/leak-detector + </developerConnection> + <url>http://svn.apache.org/viewvc/sling/trunk/contrib/extensions/leak-detector</url> + </scm> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Bundle-Activator>org.apache.sling.extensions.leakdetector.internal.LeakDetector</Bundle-Activator> + </instructions> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + <version>4.3.0</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.compendium</artifactId> + <version>4.3.0</version> + <scope>provided</scope> + </dependency> + </dependencies> + +</project> diff --git a/src/main/java/org/apache/sling/extensions/leakdetector/internal/LeakDetector.java b/src/main/java/org/apache/sling/extensions/leakdetector/internal/LeakDetector.java new file mode 100644 index 0000000..38e4446 --- /dev/null +++ b/src/main/java/org/apache/sling/extensions/leakdetector/internal/LeakDetector.java @@ -0,0 +1,304 @@ +package org.apache.sling.extensions.leakdetector.internal; + +import java.io.PrintWriter; +import java.lang.ref.PhantomReference; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.Constants; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.util.tracker.BundleTracker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LeakDetector implements Runnable, BundleActivator { + /** + * Set of PhantomReferences such that PhantomReference itself is not GC + */ + private final Set<Reference<?>> refs = Collections.synchronizedSet(new HashSet<Reference<?>>()); + + /** + * Lock to control concurrent access to internal data structures + */ + private final Object leakDetectorLock = new Object(); + + private final ReferenceQueue<ClassLoader> queue = new ReferenceQueue<ClassLoader>(); + + private final ConcurrentMap<Long, BundleInfo> bundleInfos = new ConcurrentHashMap<Long, BundleInfo>(); + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private Thread referencePoller; + + private BundleContext context; + + private BundleTracker bundleTracker; + + public void start(BundleContext context) { + this.context = context; + this.bundleTracker = new LeakDetectorBundleTracker(context); + + referencePoller = new Thread(this, "Bundle Leak Detector Thread"); + referencePoller.setDaemon(true); + referencePoller.start(); + + Dictionary<String,Object> printerProps = new Hashtable<String, Object>(); + printerProps.put(Constants.SERVICE_VENDOR, "Apache Software Foundation"); + printerProps.put(Constants.SERVICE_DESCRIPTION, "Sling Log Configuration Printer"); + printerProps.put("felix.webconsole.label", "leakdetector"); + printerProps.put("felix.webconsole.title", "Classloader Leak Detector"); + printerProps.put("felix.webconsole.configprinter.modes", "always"); + + context.registerService(LeakDetector.class.getName(), this, printerProps); + } + + public void stop(BundleContext context) { + this.bundleTracker.close(); + referencePoller.interrupt(); + } + + private class LeakDetectorBundleTracker extends BundleTracker { + public LeakDetectorBundleTracker(BundleContext context) { + //Only listen for started + super(context, Bundle.ACTIVE, null); + this.open(); + } + + @Override + public Object addingBundle(Bundle bundle, BundleEvent event) { + synchronized (leakDetectorLock) { + registerBundle(bundle); + } + return bundle; + } + } + + private void registerBundle(Bundle bundle) { + ClassLoader cl = getClassloader(bundle); + //cl would be null for Fragment bundle + if (cl != null) { + BundleReference ref = new BundleReference(bundle, cl); + refs.add(ref); + + //Note that a bundle can be started multiple times + //for e.g. when refreshed So we need to account for that also + BundleInfo bi = bundleInfos.get(bundle.getBundleId()); + if (bi == null) { + bi = new BundleInfo(bundle); + bundleInfos.put(bundle.getBundleId(), bi); + } + bi.incrementUsageCount(ref); + log.info("Registered bundle [{}] with Classloader [{}]", bi, ref.classloaderInfo); + } + } + + //~----------------------------------------<GC Callback> + + public void run() { + while (!Thread.currentThread().isInterrupted()) { + try { + BundleReference ref = (BundleReference) queue.remove(); + if (ref != null) { + removeBundle(ref); + } + } catch (InterruptedException e) { + break; + } + } + + log.info("Shutting down reference collector for Classloader LeakDetector"); + //Drain out the queue + while (queue.poll() != null); + } + + private void removeBundle(BundleReference ref) { + BundleInfo bi = bundleInfos.get(ref.bundleId); + + synchronized (leakDetectorLock){ + //bi cannot be null + bi.decrementUsageCount(ref); + refs.remove(ref); + } + + log.info("Detected garbage collection of bundle [{}] - Classloader [{}]", bi, ref.classloaderInfo); + } + + + + //~---------------------------------------<Configuration Printer> + + /** + * @see org.apache.felix.webconsole.ConfigurationPrinter#printConfiguration(java.io.PrintWriter) + */ + @SuppressWarnings("UnusedDeclaration") + public void printConfiguration(PrintWriter pw) { + //Try to force GC + //TODO Should we do by default or let user do it explicitly via + //Felix Web Console + //System.gc(); + + Set<Long> activeBundleIds = new HashSet<Long>(); + for (Bundle b : context.getBundles()) { + activeBundleIds.add(b.getBundleId()); + } + + List<BundleInfo> suspiciousBundles = new ArrayList<BundleInfo>(bundleInfos.values()); + Iterator<BundleInfo> itr = suspiciousBundles.iterator(); + while (itr.hasNext()) { + BundleInfo bi = itr.next(); + + //Filter out bundles which are active and have + //only one classloader created for them + if (bi.hasSingleInstance() + && activeBundleIds.contains(bi.bundleId)) { + itr.remove(); + } + } + + if (suspiciousBundles.isEmpty()) { + pw.println("No classloader leak detected"); + } else { + pw.println("Possible classloader leak detected"); + pw.printf("Number of suspicious bundles - %d %n", suspiciousBundles.size()); + pw.println(); + + final String tab = " "; + + for(BundleInfo bi : suspiciousBundles){ + pw.printf("* %s %n", bi); + pw.printf("%s - Bundle Id - %d %n", tab, bi.bundleId); + pw.printf("%s - Leaked classloaders %n", tab); + for(ClassloaderInfo ci : bi.leakedClassloaders()){ + pw.printf("%s%s - %s %n", tab, tab, ci); + } + } + } + } + + //~---------------------------------------<Data Model> + + private static class BundleInfo { + final String symbolicName; + final String version; + final long bundleId; + private final Set<ClassloaderInfo> classloaderInfos = + Collections.synchronizedSet(new HashSet<ClassloaderInfo>()); + + public BundleInfo(Bundle b) { + this.symbolicName = b.getSymbolicName(); + this.version = b.getVersion().toString(); + this.bundleId = b.getBundleId(); + } + + public synchronized void incrementUsageCount(BundleReference ref) { + classloaderInfos.add(ref.classloaderInfo); + } + + public synchronized void decrementUsageCount(BundleReference ref) { + classloaderInfos.remove(ref.classloaderInfo); + } + + public synchronized boolean hasSingleInstance() { + return classloaderInfos.size() == 1; + } + + public synchronized List<ClassloaderInfo> leakedClassloaders(){ + if(hasSingleInstance()){ + return new ArrayList<ClassloaderInfo>(classloaderInfos); + }else{ + List<ClassloaderInfo> cis = new ArrayList<ClassloaderInfo>(classloaderInfos); + Collections.sort(cis); + + //Leave out the latest classloader entry as that is + //associated with running bundle + return cis.subList(0, cis.size() - 1); + } + } + + @Override + public String toString() { + return String.format("%s (%s) - Classloader Count [%s]", symbolicName, + version, classloaderInfos.size()); + } + } + + private static class ClassloaderInfo implements Comparable<ClassloaderInfo> { + final Long creationTime = System.currentTimeMillis(); + final long systemHashCode; + + private ClassloaderInfo(ClassLoader cl) { + this.systemHashCode = System.identityHashCode(cl); + } + + public int compareTo(ClassloaderInfo o) { + return creationTime.compareTo(o.creationTime); + } + + public String getAddress(){ + return Long.toHexString(systemHashCode); + } + + public String getCreationDate(){ + SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss.SSS"); + return dateFormat.format(new Date(creationTime)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ClassloaderInfo that = (ClassloaderInfo) o; + + if (systemHashCode != that.systemHashCode) return false; + + return true; + } + + @Override + public int hashCode() { + return (int) (systemHashCode ^ (systemHashCode >>> 32)); + } + + @Override + public String toString() { + return String.format("Identity HashCode - %s, Creation time %s", getAddress(), getCreationDate()); + } + } + + private class BundleReference extends PhantomReference<ClassLoader> { + final Long bundleId; + final ClassloaderInfo classloaderInfo; + + public BundleReference(Bundle bundle, ClassLoader cl) { + super(cl, queue); + this.bundleId = bundle.getBundleId(); + this.classloaderInfo = new ClassloaderInfo(cl); + } + } + + private static ClassLoader getClassloader(Bundle b) { + //Somehow it fails to compile on JDK 7. Explicit cast helps + BundleWiring bw = (BundleWiring) b.adapt(BundleWiring.class); + if(bw != null){ + return bw.getClassLoader(); + } + return null; + } +} -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
