Skip to content

Instantly share code, notes, and snippets.

@chetanmeh
Last active January 21, 2020 12:52
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save chetanmeh/8860776 to your computer and use it in GitHub Desktop.
Save chetanmeh/8860776 to your computer and use it in GitHub Desktop.
Following scripts looks for Classloading Leak suspects for OSGi env. This script needs to be executed from the OQL editor of JHat
var debugEnabled = false
var infoEnabled = true
var traceEnabled = false
//Maximum number of live paths to determine when a leak object is found
var maxPathsToFound = 1
//No of object instances after which a progress message
//is logged
var OBJ_DISPLAY_COUNT = 100
//For a given class if following number of objects
//are analyzed and no referrer is found then
//skip further processing
var SKIP_NON_REFERRED_OBJ_COUNT = 10
var bundleCL = heap.findClass('org.apache.felix.framework.BundleWiringImpl$BundleClassLoaderJava5')
var bundleClassLoaders = new java.util.HashMap();
var leakedClassLoaders = new java.util.HashMap();
banner("Analyzing Bundle Classloader Leak")
var leakSuspects = toArray(filterEnumeration(unwrapJavaObject(bundleCL).getInstances(false), function (cl) {
if (cl.m_wiring.m_isDisposed == true) {
leakedClassLoaders.put(objectid(cl), cl)
return true
} else {
bundleClassLoaders.put(objectid(cl), cl)
return false
}
}, true));
/**
* List of suspected objects from the leaked classloader which are being strongly
* held by code in valid bundle
*/
var suspectedObjects = new java.util.HashMap();
/*
The logic works in following mode
1. Determine the classloader id which are supposed to be GC but have not been and thus leak suspects
2. Iterate over classloader -> classes -> objects
3. For each such object traverse the object referrers and determine referrers path whnich are holding
up the object with string reference and preventing it from getting garbage collected
4. Return the list of such suspected objects
By default the JHat Web Ui would render links to such objects which enable querying for
all the live paths
*/
var suspectCounts = estimateNoOfSuspectedClazzAndObjs(leakSuspects)
info("Number of suspected classloaders - "+leakSuspects.length)
info("Number of suspected objects - "+suspectCounts.objCount)
info("Number of suspected classes - "+suspectCounts.clazzCount)
var processedObjCount = 0
var processedCLassCount = 0
var processedClasses = new java.util.HashSet();
for(var leakIdx = 0; leakIdx < leakSuspects.length; leakIdx++){
var cl = leakSuspects[leakIdx]
var noOfClasses = cl.classes.elementCount
for (var i = 0; i < noOfClasses; i++) {
processedCLassCount++;
var clazz = cl.classes.elementData[i]
var instanceCount = unwrapJavaObject(clazz).getInstancesCount(false);
//In many case we have multiple stale classloaders
//of same bundle. So better not process same class
// (part of different classloaders) multiple time
if(processedClasses.contains(clazz.name)){
trace("Skipping processing of "+clazz+" as its already processed")
processedObjCount += instanceCount
continue
}else{
processedClasses.add(clazz.name)
}
if(debugEnabled) debug("Processing "+clazz+" ("+processedCLassCount+
"/"+suspectCounts.clazzCount +") Number of instances "+instanceCount)
var clazzInstances = unwrapJavaObject(clazz).getInstances(false)
var objCount = processedObjCount
var nonReferredObjCount = 0
forEach(clazzInstances, function(obj){
objCount++
nonReferredObjCount++
var refPaths = rootsetReferencesToDepthFirst(obj)
if(objCount % OBJ_DISPLAY_COUNT == 0){
info("Processed objects so far [" + objCount+"/"+suspectCounts.objCount+"] ...")
}
if(refPaths.isEmpty()
&& nonReferredObjCount > SKIP_NON_REFERRED_OBJ_COUNT){
debug("No referred object found after processing "+
SKIP_NON_REFERRED_OBJ_COUNT+" instances");
return true
}
if(refPaths.isEmpty()){
return
}
suspectedObjects.put(obj, refPaths)
dumpReferrerDetails(obj, refPaths, true)
//Do not inspect references for other references of clazz
//as holding pattern would be mostly similar
return true
});
processedObjCount += instanceCount
if(processedObjCount % OBJ_DISPLAY_COUNT == 0){
info("Processed objects so far [" + processedObjCount+"/"+suspectCounts.objCount+"] ...")
}
}
}
filterOutDuplicates(suspectedObjects)
banner("Analysis Report Start")
info("Number of suspected objects "+ suspectedObjects.size())
var suspectedItr = suspectedObjects.entrySet().iterator()
while(suspectedItr.hasNext()){
var e = suspectedItr.next()
dumpReferrerDetails(e.getKey(), e.getValue())
}
banner("Analysis Report End")
/**
* Determines a limited number of live paths to the target object
* by traversing in a depth first mode
*
* @param target
* @returns an array of paths. Each element entry is again an
* array of object which form the live path to the target
* object
*/
function rootsetReferencesToDepthFirst(target){
target = unwrapJavaObject(target)
var pathStack = new java.util.ArrayDeque();
//Paths is a list of list
var paths = new java.util.ArrayDeque();
var visited = new java.util.HashSet();
visited.add(target);
if(!visit(target,pathStack,visited, paths)){
return new java.util.ArrayList()
}
/* //Covert list of list to array of array for easier traversal
//in JS
var itr = paths.iterator()
while(itr.hasNext()){
result.push(itr.next().toArray())
}*/
return paths;
}
/**
* Visits the referrer tree in in order mode
*
* @param target instance whose referrers are analyzed
* @param pathStack current path stack in tree
* @param visited set of visited instances
* @param paths list of live paths determined so far
*/
function visit(target, pathStack, visited, paths){
if(isLeakedClassloaderInstance(target)){
debug("Skipping further processing as instance is classloader "+target)
return false
}
pathStack.addLast(target)
var referers = target.getReferers();
if(!referers.hasMoreElements()){
//End of path in object tree reached
//Check for leak suspect
var stackArray = pathStack.toArray()
var pathArr = stackArray
var lastNormalBundleIdx = -1
var firstNormalBundleIdx = -1
var suspectedClassLoaderSeen = false
var seenClassloaderInstance = false
for(var i = pathArr.length - 1; i >= 0; i --){
var obj = pathArr[i]
var id = classLoaderId(obj)
if(bundleClassLoaders.containsKey(id) && !suspectedClassLoaderSeen){
if(firstNormalBundleIdx == -1){
firstNormalBundleIdx = i;
}
lastNormalBundleIdx = i
}else if (leakedClassLoaders.containsKey(id)){
suspectedClassLoaderSeen = true
}
if(leakedClassLoaders.containsKey(obj.getIdString())){
seenClassloaderInstance = true
break;
}
}
if(seenClassloaderInstance){
//In case a class object like Interface/Enum its possible
//object's referrer is a class and whose reference is
//classloader. This would happen mostly as a side effect
//and in most cases would not be a cause of memory leak
if(traceEnabled) trace("Found a suspected classloader as reference. Ignoring "+pathStack)
} else if(firstNormalBundleIdx != -1){
//Extract the path elements from target object upto class whose classloader
//is from valid Bundle
var actualPath = new java.util.ArrayList(
java.util.Arrays.asList(stackArray).subList(0,firstNormalBundleIdx+1))
paths.addLast(actualPath)
}else{
if(traceEnabled) trace("No class found belonging to valid classloader "+pathStack)
}
}
while (referers.hasMoreElements()) {
if(paths.size() >= maxPathsToFound){
trace("Maximum number of paths found. Not proceeding")
return true
}
var t = referers.nextElement();
if (t != null
&& !visited.contains(t)
&& !t.refersOnlyWeaklyTo(heap.snapshot, target)) {
visited.add(t);
if(!visit(t, pathStack, visited, paths)){
return false
}
}
if(!paths.isEmpty()){
var path = paths.peekLast();
if(pathStack.size() >= path.size()){
//In a object referrer tree we are only interested in
// distinct path from target upto an object from valid
// classloader. So we are at depth higher than suspected
//path length we skip rest entries
break;
}
}
}
//Pop the last entry
pathStack.removeLast()
return true
}
/**
* Determines the objectId of the classloader associated
* with given object
*/
function classLoaderId(obj){
obj = unwrapJavaObject(obj)
var loader = obj.getClazz().getLoader()
//Objects loaded from system classloader like String
//would not have Loader with id
if(!(loader instanceof hatPkg.model.HackJavaValue)){
return loader.getIdString()
}
return "<system>"
}
/**
* In case a class object like Interface/Enum its possible
* object's referrer is a class and whose reference is
* classloader. This would happen mostly as a side effect
* and in most cases would not be a cause of memory leak
* @param obj
* @returns {*}
*/
function isLeakedClassloaderInstance(obj){
//TODO this check is quite wide as we miss out on
//static fields referred by class instances. Probably
//we should just filter out on cases where obj.class is enum
return leakedClassLoaders.containsKey(obj.getIdString());
}
/**
* The suspectedObjects contains suspected object as key and list of
* referrer paths as value.
* {o1 : [
* o1-> a -> b -> c -> d -> e,
* ...
* ],
* c : [
* c -> d -> e
* ...
* ]
* }
*
* So it might happen that a suspectedObject itself is part of path and hence
* that would lead to a duplicate scenario. So this methid would remove all
* such paths
*
* @param suspectedObjects
*/
function filterOutDuplicates(suspectedObjects){
var suspectedItr = suspectedObjects.entrySet().iterator()
while(suspectedItr.hasNext()){
var e = suspectedItr.next()
var obj = e.getKey()
var paths = e.getValue()
var pathsItr = paths.iterator()
while(pathsItr.hasNext()){
var path = pathsItr.next()
var pathItr = path.iterator()
var refersToKnownObj = false
//Check if the path contains an obj which is
//already part of suspected obj list
while(pathItr.hasNext()){
var pathObj = pathItr.next()
var suspectItr = suspectedObjects.keySet().iterator()
while(suspectItr.hasNext()){
var suspectObj = suspectItr.next();
//Cannot compare by object equality as we do
//random object sampling. So better to check
//Both have same class
try {
if (suspectObj.getClazz().getName().equals(pathObj.getClazz().getName())
&& !suspectObj.equals(obj)) {
refersToKnownObj = true
break
}
}
catch (ex) {
//Object is a JavaThing and does not has a class
}
}
if(refersToKnownObj){
break
}
}
//If such an obj is found it means that obj is actual
//suspect and this is just a duplicate
if(refersToKnownObj){
pathsItr.remove()
}
}
if(paths.isEmpty()){
suspectedItr.remove()
}
}
}
function estimateNoOfSuspectedClazzAndObjs(leakSuspects){
var objCount = 0
var clazzCount = 0
for(var leakIdx = 0; leakIdx < leakSuspects.length; leakIdx++){
var cl = leakSuspects[leakIdx]
var noOfClasses = cl.classes.elementCount
clazzCount += noOfClasses
for (var i = 0; i < noOfClasses; i++) {
var clazz = cl.classes.elementData[i]
objCount += unwrapJavaObject(clazz).getInstancesCount(false)
}
}
return {objCount : objCount, clazzCount: clazzCount}
}
function dumpReferrerDetails(obj, refPaths, debugMsg){
var logFunc = debugMsg ? debug : info
logFunc("\t"+obj)
logFunc("\t Following are few of the live paths found")
forEachInCollection(refPaths, function(path){
logFunc("\t Live path")
forEachInCollection(path, function(pathEntry){
var clId = classLoaderId(pathEntry)
var marker = ""
if(bundleClassLoaders.containsKey(clId)){
marker = " [*]"
}
logFunc("\t\t"+pathEntry+marker)
})
})
}
//~-------------------------------------<Logging methods>
function info(msg){
if (infoEnabled) println(msg)
}
function debug(msg){
if (debugEnabled) println(msg)
}
function trace(msg){
if(traceEnabled) println(msg)
}
function banner(msg){
println("======================= "+ msg + " =======================")
}
function warn(msg){
println("[WARN]"+msg)
}
//~-------------------------------------<Utility Methods for traversal>
function forEach(enumeration,callback){
if (callback == undefined) callback = print;
while(enumeration.hasMoreElements()){
if(callback(enumeration.nextElement())){
return
}
}
}
function forEachArrElement(arr,callback){
if (callback == undefined) callback = print;
for(var i = 0; i < arr.length; i++){
if(callback(arr[i])){
return
}
}
}
function forEachInCollection(collection,callback){
var itr = collection.iterator()
if (callback == undefined) callback = print;
while(itr.hasNext()){
if(callback(itr.next())){
return
}
}
}
//For testing purpose
/*var refPaths = rootsetReferencesToDepthFirst(heap.findObject("0x1164dcde8"))
forEachArrElement(refPaths, function(path){
debug("\t New Chain")
forEachArrElement(path, function(pathEntry){
debug("\t\t"+pathEntry)
})
})*/
//Final result
var result = []
var resultItr = suspectedObjects.keySet().iterator()
while(resultItr.hasNext()){
result.push(resultItr.next())
}
result
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment