/*******************************************************************************
* Licensed 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.kato.katoview.commands;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.tools.diagnostics.image.CorruptData;
import javax.tools.diagnostics.image.CorruptDataException;
import javax.tools.diagnostics.image.DataUnavailable;
import javax.tools.diagnostics.image.Image;
import javax.tools.diagnostics.image.ImageAddressSpace;
import javax.tools.diagnostics.image.ImageProcess;
import javax.tools.diagnostics.image.DiagnosticException;
import javax.tools.diagnostics.image.MemoryAccessException;
import javax.tools.diagnostics.runtime.java.JavaClass;
import javax.tools.diagnostics.runtime.java.JavaClassLoader;
import javax.tools.diagnostics.runtime.java.JavaField;
import javax.tools.diagnostics.runtime.java.JavaHeap;
import javax.tools.diagnostics.runtime.java.JavaObject;
import javax.tools.diagnostics.runtime.java.JavaRuntime;
import org.apache.kato.katoview.Output;
import org.apache.kato.katoview.heapdump.HeapDumpFormatter;
import org.apache.kato.katoview.heapdump.HeapDumpSettings;
import org.apache.kato.katoview.heapdump.LongListReferenceIterator;
import org.apache.kato.katoview.heapdump.ReferenceIterator;
import org.apache.kato.katoview.heapdump.classic.ClassicHeapDumpFormatter;
/**
* Command for dumping heapdumps from Kato.
*
* Contains the heap-walking logic for building the reference tree.
*
*/
public class HeapdumpCommand extends Command
{
public static final String COMMAND_NAME = "heapdump";
public static final String DESCRIPTION = "generates a heapdump";
public static final String LONG_DESCRIPTION = "Parameters: [heapname+]\n\n"
+ "\t[heapname+] - space-separated name of heap or heaps to dump. Use \"info heap\" to get the list of heap names. Default: all heaps are dumped.\n\n"
+ "Writes a heapdump from the memory image.\n"
+ "The file name and format are controlled using the \"set heapdump\" command; the current settings "
+ "can be displayed using \"show heapdump\".\n";
private static final String PROTECTION_DOMAIN_FIELD_NAME = "protectionDomain";
/**
* Regexp pattern used to extract a subset of the versions string
*/
private static final Pattern J9_VERSION_PATTERN = Pattern.compile("(IBM J9 VM.*?\\))");
//Do not change the order this array - the indexes are used to extract type codes in the getPrimitiveTypeCode method
private static final String[] PRIMITIVE_TYPES = { "boolean", "char",
"float", "double", "byte", "short", "int", "long", "void" };
private int _numberOfObjects = 0;
private int _numberOfClasses = 0;
private int _numberOfErrors = 0;
private boolean _verbose = false;
public HeapdumpCommand(Output o)
{
super(o, COMMAND_NAME, DESCRIPTION, LONG_DESCRIPTION);
child_commands = null;
}
public void doCommand(Stack args, Image loadedImage, HashMap properties)
{
Set heapsToDump = new HashSet();
while(! args.isEmpty()) {
heapsToDump.add(args.pop());
}
_numberOfObjects = 0;
_numberOfErrors = 0;
_numberOfClasses = 0;
Object verboseModeStr = properties.get("verbose.mode");
_verbose = verboseModeStr != null ? verboseModeStr.equals("on") : false;
ImageAddressSpace addressSpace = (ImageAddressSpace) properties.get("current_address_space");
if(addressSpace == null) {
out.error("Couldn't get handle on address space");
return;
}
JavaRuntime runtime = getRuntime(addressSpace);
if(runtime == null) {
return;
}
if(! heapArgumentsAreValid(runtime,heapsToDump)) {
return;
}
String version = getVersionString(runtime);
boolean is64Bit = addressSpace.getCurrentProcess().getPointerSize() == 64;
String filename = HeapDumpSettings.getFileName(properties);
boolean phdFormat = HeapDumpSettings.areHeapDumpsPHD(properties);
try {
if(HeapDumpSettings.multipleHeapsInMultipleFiles(properties)) {
dumpMultipleHeapsInSeparateFiles(runtime,version,is64Bit,phdFormat,filename,heapsToDump);
} else {
dumpMultipleHeapsInOneFile(runtime,version,is64Bit,phdFormat,filename,heapsToDump);
}
if(_numberOfErrors == 0) {
out.print("\nSuccessfully wrote " + _numberOfObjects + " objects and " + _numberOfClasses + " classes\n");
} else {
out.print("\nWrote "
+ _numberOfObjects
+ " objects and "
+ _numberOfClasses
+ " classes and encountered "
+ _numberOfErrors
+ " errors."
+ (! _verbose ? " Start KatoView with -verbose for more detail." : "")
+ "\n");
}
}
catch (IOException ex) {
out.error("I/O error writing dump:\n");
StringWriter writer = new StringWriter();
ex.printStackTrace(new PrintWriter(writer));
out.error(writer.toString());
}
}
/**
* Checks the list of heaps to dump as specified by the user.
* @param runtime Current java runtime
* @param heapsToDump Set of strings the user passed as heaps to dump
* @return True if all the names are valid heaps, false otherwise
*/
private boolean heapArgumentsAreValid(JavaRuntime runtime, Set heapsToDump)
{
if(heapsToDump.size() == 0) {
return true;
}
Set workingSet = new HashSet();
workingSet.addAll(heapsToDump);
Iterator heapIt = runtime.getHeaps().iterator();
while(heapIt.hasNext()) {
Object potential = heapIt.next();
if(potential instanceof JavaHeap) {
JavaHeap thisHeap = (JavaHeap)potential;
workingSet.remove(thisHeap.getName());
} else if (potential instanceof CorruptData) {
reportError("Corrupt heap found. Address = " + ((CorruptData)potential).getAddress(),null);
_numberOfErrors++;
} else {
_numberOfErrors++;
reportError("Unexpected type " + potential.getClass().getName() + " found in heap iterator",null);
}
}
if(workingSet.isEmpty()) {
return true;
} else {
StringBuffer buffer = new StringBuffer();
buffer.append("These specified heaps do not exist:\n");
Iterator nameIterator = workingSet.iterator();
while(nameIterator.hasNext()) {
buffer.append("\t\t" + nameIterator.next() + "\n");
}
buffer.append("\tUse \"info heap\" to see list of heap names");
out.error(buffer.toString());
return false;
}
}
/**
* Extracts a minimal version string from the full -version output
*/
private String getVersionString(JavaRuntime runtime)
{
try {
String rawVersion = runtime.getVersion();
Matcher matcher = J9_VERSION_PATTERN.matcher(rawVersion);
if(matcher.find()) {
String minimalVersion = matcher.group(1);
return minimalVersion;
} else {
_numberOfErrors++;
reportError("Could not parse version string: " + rawVersion,null);
return rawVersion;
}
}
catch (CorruptDataException e) {
_numberOfErrors++;
out.error("Could not read version string from dump: data corrupted at "
+ e.getCorruptData().getAddress());
return "*Corrupt*";
}
}
private JavaRuntime getRuntime(ImageAddressSpace addressSpace)
{
ImageProcess process = addressSpace.getCurrentProcess();
if(process == null) {
out.error("Couldn't get handle on current process");
return null;
}
Iterator runtimeIterator = process.getRuntimes().iterator();
if(! runtimeIterator.hasNext()) {
out.error("Cannot find a runtime");
return null;
}
Object potential = runtimeIterator.next();
if(potential instanceof CorruptData) {
out.error("Runtime data is corrupt");
return null;
}
return (JavaRuntime) potential;
}
private void dumpMultipleHeapsInOneFile(JavaRuntime runtime,
String version, boolean is64Bit, boolean phdFormat, String filename, Set heapsToDump) throws IOException
{
Iterator heapIterator = runtime.getHeaps().iterator();
HeapDumpFormatter formatter = getFormatter(filename, version, is64Bit, phdFormat);
out.print("Writing " + ( phdFormat ? "PHD" : "Classic") + " format heapdump into " + filename);
dumpClasses(formatter,runtime);
while (heapIterator.hasNext()) {
Object thisHeapObj = heapIterator.next();
if (thisHeapObj instanceof CorruptData) {
out.error("Corrupt heap data found at: "
+ ((CorruptData) thisHeapObj).getAddress());
_numberOfErrors++;
continue;
}
JavaHeap thisHeap = (JavaHeap) thisHeapObj;
if(heapsToDump.size() > 0 && ! heapsToDump.contains(thisHeap.getName())) {
continue;
}
dumpHeap(formatter, thisHeap);
}
formatter.close();
}
private void dumpMultipleHeapsInSeparateFiles(JavaRuntime runtime,String version, boolean is64Bit, boolean phdFormat,String baseFileName, Set heapsToDump) throws IOException
{
Iterator heapIterator = runtime.getHeaps().iterator();
HeapDumpFormatter formatter = null;
while (heapIterator.hasNext()) {
Object thisHeapObj = heapIterator.next();
if (thisHeapObj instanceof CorruptData) {
out.error("Heap corrupted at: "
+ ((CorruptData) thisHeapObj).getAddress());
_numberOfErrors++;
continue;
}
JavaHeap thisHeap = (JavaHeap) thisHeapObj;
// Create a new heapdump formatter for every heap we find
if (formatter != null) {
formatter.close();
}
if(heapsToDump.size() > 0 && ! heapsToDump.contains(thisHeap.getName())) {
continue;
}
String fileName = getFileNameForHeap(thisHeap,baseFileName);
out.print("Writing "
+ ( phdFormat ? "PHD" : "Classic")
+ " format heapdump for heap "
+ thisHeap.getName()
+ " into "
+ fileName + "\n");
formatter = getFormatter(fileName, version, is64Bit, phdFormat);
//We have to dump classes in every heapdump
dumpClasses(formatter,runtime);
dumpHeap(formatter, thisHeap);
}
if(formatter != null) {
formatter.close();
}
}
/**
* Walks the runtime classes and passes them through the formatter interface
*/
private void dumpClasses(HeapDumpFormatter formatter, JavaRuntime runtime) throws IOException
{
Iterator classLoaderIt = runtime.getJavaClassLoaders().iterator();
int numberOfClasses = 0;
ITERATING_LOADERS:while(classLoaderIt.hasNext()) {
Object potential = classLoaderIt.next();
if(potential instanceof CorruptData) {
_numberOfErrors++;
reportError("CorruptData found in classloader list at address: " + ((CorruptData)potential).getAddress(), null);
continue ITERATING_LOADERS;
}
JavaClassLoader thisClassLoader = (JavaClassLoader)potential;
Iterator classesIt = thisClassLoader.getDefinedClasses().iterator();
ITERATING_CLASSES:while(classesIt.hasNext()) {
potential = classesIt.next();
numberOfClasses++;
try {
if(potential instanceof CorruptData) {
_numberOfErrors++;
reportError("CorruptData found in class list for classloader "
+ Long.toHexString(thisClassLoader.getObject().getID().getAddress())
+ " at address: " + ((CorruptData)potential).getAddress(), null);
continue ITERATING_CLASSES;
}
JavaClass thisJavaClass = (JavaClass)potential;
JavaClass superClass = thisJavaClass.getSuperclass();
JavaObject classObject = thisJavaClass.getObject();
int instanceSize = 0;
if(thisJavaClass.isArray()) {
instanceSize = 0;
} else {
// we need to figure out a way of determining the instance size for a class
}
formatter.addClass(thisJavaClass.getID().getAddress(),
thisJavaClass.getName(),
superClass != null ? superClass.getID().getAddress() : 0,
classObject != null ? (int)classObject.getSize() : 0,
instanceSize,
classObject != null ? (int)classObject.getPersistentHashcode() : 0,
getClassReferences(thisJavaClass) );
} catch(DiagnosticException ex) {
//Handle CorruptDataException and DataUnavailableException the same way
_numberOfErrors++;
reportError(null,ex);
continue ITERATING_CLASSES;
}
}
}
_numberOfClasses = numberOfClasses;
}
/**
* Walks the supplied heap and passes the artifacts through the formatter
*/
private void dumpHeap(HeapDumpFormatter formatter, JavaHeap thisHeap)
throws IOException
{
Iterator objectIterator = thisHeap.getObjects().iterator();
while (objectIterator.hasNext()) {
Object next = objectIterator.next();
_numberOfObjects++;
if (next instanceof CorruptData) {
_numberOfErrors++;
reportError("Corrupt object data found at " + ((CorruptData)next).getAddress() + " while walking heap " + thisHeap.getName(),null);
continue;
}
try {
JavaObject thisObject = (JavaObject) next;
JavaClass thisClass = thisObject.getJavaClass();
int hashcode = 0;
try {
hashcode = (int) thisObject.getHashcode();
}
catch (DataUnavailable ex) {
_numberOfErrors++;
reportError("Failed to get hashcode for object: " + thisObject.getID(),ex);
}
if (thisObject.isArray()) {
if (isPrimitive(thisClass.getComponentType())) {
formatter.addPrimitiveArray(thisObject.getID().getAddress(),
thisClass.getID().getAddress(),
getPrimitiveTypeCode(thisClass.getComponentType()),
(int) thisObject.getSize(),
hashcode,
thisObject.getArraySize());
}
else {
formatter.addObjectArray(thisObject.getID().getAddress(),
thisClass.getID().getAddress(),
thisClass.getName(),
thisClass.getComponentType().getID().getAddress(),
thisClass.getComponentType().getName(),
(int) thisObject.getSize(),
thisObject.getArraySize(),
hashcode,
getObjectReferences(thisObject));
}
}
else {
formatter.addObject(thisObject.getID().getAddress(),
thisClass.getID().getAddress(),
thisClass.getName(),
(int)thisObject.getSize(),
hashcode,
getObjectReferences(thisObject));
}
}
catch (CorruptDataException ex) {
_numberOfErrors++;
reportError(null,ex);
continue;
}
}
}
/* Reference code reimplemented (rather than using the Kato getReferences() API)
* because we are trying to match the behaviour of the runtime heapdump rather than
* the GC spec. The set of references we're trying to create is different.
*/
/**
* Gets the references for the supplied class
*
* @param thisJavaClass Class being examined
*/
private ReferenceIterator getClassReferences(JavaClass thisJavaClass)
{
List references = new LinkedList();
try {
//Statics
addStaticReferences(thisJavaClass, references);
addProtectionDomainReference(thisJavaClass,references);
//Classloader
JavaClassLoader loader = thisJavaClass.getClassLoader();
if(loader != null) {
JavaObject loaderObject = loader.getObject();
if(loaderObject != null) {
references.add(new Long(loaderObject.getID().getAddress()));
} else {
reportError("Null loader object returned for class: " + thisJavaClass.getName() + "(" + thisJavaClass.getID() + ")",null);
_numberOfErrors++;
}
} else {
reportError("Null classloader returned for class: " + thisJavaClass.getName() + "(" + thisJavaClass.getID() + ")",null);
_numberOfErrors++;
}
//Heap object
JavaObject classObject = thisJavaClass.getObject();
if(classObject != null) {
references.add(new Long(classObject.getID().getAddress()));
} else {
_numberOfErrors++;
reportError("Couldn't get Java class loader for: " + thisJavaClass.getName() + "(" + thisJavaClass.getID() + ")",null);
}
} catch(DiagnosticException ex) {
reportError(null,ex);
_numberOfErrors++;
}
return new LongListReferenceIterator(references);
}
private JavaField _protectionDomainField;
private void addProtectionDomainReference(JavaClass thisJavaClass,
List references) throws CorruptDataException, MemoryAccessException
{
JavaObject classObject = thisJavaClass.getObject();
//Protection domain hangs off the protectionDomain field on the class object
if(_protectionDomainField == null) {
JavaClass javaLangClassObject = classObject.getJavaClass();
if(javaLangClassObject == null) {
_numberOfErrors++;
reportError("Couldn't find java.lang.Class class",null);
return;
}
Iterator fieldsIt = javaLangClassObject.getDeclaredFields().iterator();
while(fieldsIt.hasNext()) {
Object potential = fieldsIt.next();
if(potential instanceof JavaField) {
JavaField field = (JavaField) potential;
if(field.getName().equals(PROTECTION_DOMAIN_FIELD_NAME)) {
_protectionDomainField = field;
}
} else if(potential instanceof CorruptData) {
_numberOfErrors++;
reportError("CorruptData found walking fields for java.lang.Class. Bad address = "
+ ((CorruptData)potential).getAddress()
,null);
} else {
_numberOfErrors++;
reportError("Unexpected type "
+ potential.getClass()
+ " returned from fields iterator."
,null);
}
}
if(_protectionDomainField == null) {
_numberOfErrors++;
reportError("Couldn't find protection domain field.",null);
return;
}
}
Object potential = _protectionDomainField.get(classObject);
if(potential instanceof JavaObject) {
JavaObject protectionDomain = (JavaObject)potential;
references.add(new Long(protectionDomain.getID().getAddress()));
} else if (potential instanceof CorruptData) {
_numberOfErrors++;
reportError("Corrupt data found in protectionDomainField of "
+ thisJavaClass.getName()
+ " at address "
+ ((CorruptData)potential).getAddress()
,null);
} else if (potential == null) {
//Do nothing
} else {
reportError("Unexpected type: "
+ potential.getClass().getName()
+ " found in protectionDomain field of "
+ thisJavaClass.getName()
,null);
_numberOfErrors++;
}
}
/**
* Extracts static references from class
* @param thisClass Class being examined
* @param references List to add references to
*/
private void addStaticReferences(JavaClass thisClass, List references)
throws CorruptDataException, MemoryAccessException
{
Iterator fieldsIt = thisClass.getDeclaredFields().iterator();
while(fieldsIt.hasNext()) {
Object potential = fieldsIt.next();
if(potential instanceof CorruptData) {
reportError("Corrupt field found in class "
+ thisClass.getName()
+ "(" + thisClass.getID() + ") at "
+ ((CorruptData)potential).getAddress()
,null);
_numberOfErrors++;
continue;
}
JavaField field = (JavaField) potential;
if(! Modifier.isStatic(field.getModifiers())) {
continue;
}
Object referent = field.get(thisClass.getObject());
if(referent instanceof CorruptData) {
_numberOfErrors++;
reportError("Corrupt referent found in class "
+ thisClass.getName()
+ "(" + thisClass.getID() + ") from field "
+ field.getName()
+ " at address "
+ ((CorruptData)potential).getAddress()
,null);
} else if (referent instanceof JavaObject) {
JavaObject referredObject = (JavaObject) referent;
references.add(new Long(referredObject.getID().getAddress()));
} else if (referent == null) {
references.add(new Long(0));
} else if (referent instanceof Number || referent instanceof Boolean || referent instanceof Character) {
//Ignore
} else {
reportError("Unexpected type: "
+ referent.getClass().getName()
+ " returned from field "
+ field.getName()
+ " from class "
+ thisClass.getName()
+ "(" + thisClass.getID()+ ")"
,null);
_numberOfErrors++;
}
}
}
/**
* Gets instance references for objects
* @param thisObject Object being examined
* @return Iterator of references
*/
private ReferenceIterator getObjectReferences(JavaObject thisObject)
{
List references = new LinkedList();
try {
JavaClass thisClass = thisObject.getJavaClass();
if(thisClass.isArray()) {
addArrayReferences(thisObject, references);
} else {
addRegularObjectReferences(thisObject, references,thisClass);
}
} catch(DiagnosticException ex) {
_numberOfErrors++;
reportError(null,ex);
}
return new LongListReferenceIterator(references);
}
/**
* Extracts the instance references from object arrays
* @param arrayObject Array
* @param references List to add references to
*/
private void addArrayReferences(JavaObject arrayObject, List references)
throws CorruptDataException, MemoryAccessException
{
JavaObject[] localArray = new JavaObject[arrayObject.getArraySize()];
arrayObject.arraycopy(0, localArray, 0, arrayObject.getArraySize());
for(int i=0;i!=localArray.length;i++) {
if(localArray[i] != null) {
references.add(new Long(localArray[i].getID().getAddress()));
} else {
references.add(new Long(0));
}
}
}
/**
* Extracts the instance references from a regular (non-array) object
* @param object Object being walked
* @param references List to add references to
* @param thisClass Class of object
*/
private void addRegularObjectReferences(JavaObject object,
List references, JavaClass thisClass) throws CorruptDataException,
MemoryAccessException
{
while(thisClass != null) {
Iterator fieldsIt = thisClass.getDeclaredFields().iterator();
WALKING_FIELDS: while(fieldsIt.hasNext()) {
Object potential = fieldsIt.next();
if(potential instanceof CorruptData) {
_numberOfErrors++;
reportError("Corrupt data found at address "
+ ((CorruptData)potential).getAddress()
+ " walking fields of class: "
+ thisClass.getName()
+ "(" + thisClass.getID() + ")"
,null);
continue WALKING_FIELDS;
}
JavaField field = (JavaField) potential;
if(Modifier.isStatic(field.getModifiers())) {
continue WALKING_FIELDS;
}
Object referent = field.get(object);
if(referent instanceof CorruptData) {
_numberOfErrors++;
reportError("Corrupt data found in referent at address "
+ ((CorruptData)referent).getAddress()
+ " walking field "
+ field.getName()
+ " of class: "
+ thisClass.getName()
+ "(" + thisClass.getID() + ")"
,null);
continue WALKING_FIELDS;
} else if (referent instanceof JavaObject) {
JavaObject referredObject = (JavaObject) referent;
references.add(new Long(referredObject.getID().getAddress()));
} else if (referent == null) {
references.add(new Long(0));
} else if (referent instanceof Number || referent instanceof Boolean || referent instanceof Character) {
//Ignore
} else {
reportError("Unexpected type: " + referent.getClass().getName() + " found in referent",null);
_numberOfErrors++;
}
}
thisClass = thisClass.getSuperclass();
}
}
/**
* Checks if class is primitive
*
* @param clazz
* Class under test
* @return True if clazz represents a primitive type, false otherwise
*/
private static boolean isPrimitive(JavaClass clazz)
throws CorruptDataException
{
String name = clazz.getName();
/* Fastpath, anything containing a / cannot be primitive */
if (name.indexOf("/") != -1) {
return false;
}
for (int i = 0; i != PRIMITIVE_TYPES.length; i++) {
if (PRIMITIVE_TYPES[i].equals(name)) {
return true;
}
}
return false;
}
/**
* Converts a class into a primitive type code (as used by the HeapdumpFormatter interface).
* @param clazz
* @return
*/
private int getPrimitiveTypeCode(JavaClass clazz)
throws CorruptDataException
{
String name = clazz.getName();
for (int i = 0; i != PRIMITIVE_TYPES.length; i++) {
if (PRIMITIVE_TYPES[i].equals(name)) {
return i;
}
}
throw new IllegalArgumentException("Class: " + name
+ " is not primitive");
}
/**
* Factory method for HeapDumpFormatters
* @param fileName File name for output file
* @param version Version string
* @param is64Bit True if 64 bit
* @param phdFormat True if using PhD format
*/
private HeapDumpFormatter getFormatter(String fileName, String version,
boolean is64Bit, boolean phdFormat) throws IOException
{
return new ClassicHeapDumpFormatter(new FileWriter(fileName),version,is64Bit);
}
/**
* Modifies the base file name to include the heap name, whilst maintaining
* the suffix
*/
private String getFileNameForHeap(JavaHeap thisHeap, String baseFileName)
{
int pointIndex = baseFileName.lastIndexOf(".");
if(pointIndex != -1) {
return baseFileName.substring(0,pointIndex) + "." + thisHeap.getName() + baseFileName.substring(pointIndex);
} else {
return baseFileName + "." + thisHeap.getName();
}
}
/**
* Internal error handling routine that only reports the supplied message if verbose was supplied on the command line.
*/
private void reportError(String msg,Throwable t)
{
//Property set in Session.imageFromCommandLine when -verbose specified on command line
if(!_verbose) {
return;
}
if(msg != null) {
out.error(msg);
}
if(t != null) {
StringWriter writer = new StringWriter();
t.printStackTrace(new PrintWriter(writer));
out.error(writer.toString());
}
}
}