/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sling.extensions.leakdetector.internal;
import java.io.PrintWriter;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
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.
* While analyzing the Heap Dump it might appear that GC roots of such classloaders (suspected)
* points to LeakDetector. This happens because they are held here through PhantomReference
* and there normal GC has not been done. So consider that as false positive
*/
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
BundleReference ref = null;
while ((ref = (BundleReference)queue.poll()) != null){
removeBundle(ref);
}
}
private void removeBundle(BundleReference ref) {
BundleInfo bi = bundleInfos.get(ref.bundleId);
synchronized (leakDetectorLock){
//bi cannot be null
bi.decrementUsageCount(ref);
refs.remove(ref);
ref.clear();
}
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);
}
}
}
pw.println();
addHelp(pw);
}
private static void addHelp(PrintWriter pw){
RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean();
List<String> argList = bean.getInputArguments();
boolean containsRequiredArgs = argList.contains("-XX:+UseConcMarkSweepGC")
&& argList.contains("-XX:+CMSClassUnloadingEnabled");
if(!containsRequiredArgs){
pw.println("Required VM Options Missing");
pw.println("===========================");
pw.println("Leak detector relies on garbage collection of classloaders. By default");
pw.println("the classloaders are not garbage collected. To enable garbage collection of");
pw.println("classloader start the JVM with following options ");
pw.println("");
pw.println(" -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled");
}
}
//~---------------------------------------<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();
/**
* The hashCode might collide for two different classloaders but then
* we cannot keep a hard reference to Classloader reference. So at best
* we keep the systemHashCode and *assume* it is unqiue at least wrt
* classloader instances
*/
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;
}
}