-
-
Save davidjgonzalez/fadbbe32d00a137ba5de to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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