X
  • Java |
    Friday, March 30, 2007

Retrieving .class files from a running app

Many Java applications generate .class files on-the-fly at runtime. Some applications modify loaded Java classes either at class load time or even later using hotswap. Few examples:
  • Scripting language implementations that generate .class files - like BeanShell, Groovy
  • OR mapping tools such as Toplink Essentials
  • Compilation of XSL stylesheets to .classes (assuming compilation is done at runtime rather than at build).
  • Profilers that use byte code instrumentation technique - like NetBeans profiler.

It becomes difficult to debug such applications - for example, you may get verifier error because of a bug in the .class generator somewhere. So, sometimes it is better to dump .class files of generated/modified classes for off-line debugging - for example, we may want to view such classes using tools like jclasslib. Instead of modifying each and every component that generates/modifies .class files, we would want to have a generic solution. The solution below uses attach-on-demand facility in JDK 6.




File: ClassDumperAgent.java


import java.lang.instrument.\*;
import java.io.File;
import java.io.FileOutputStream;
import java.security.ProtectionDomain;
import java.util.List;
import java.util.ArrayList;
import java.util.regex.Pattern;
/\*\*
\* This is a java.lang.instrument agent to dump .class files
\* from a running Java application.
\*/
public class ClassDumperAgent implements ClassFileTransformer {
// directory where we would write .class files
private static String dumpDir;
// classes with name matching this pattern
// will be dumped
private static Pattern classes;
public static void premain(String agentArgs, Instrumentation inst) {
agentmain(agentArgs, inst);
}
public static void agentmain(String agentArgs, Instrumentation inst) {
parseArgs(agentArgs);
inst.addTransformer(new ClassDumperAgent(), true);
// by the time we are attached, the classes to be
// dumped may have been loaded already. So, check
// for candidates in the loaded classes.
Class[] classes = inst.getAllLoadedClasses();
List<Class> candidates = new ArrayList<Class>();
for (Class c : classes) {
if (isCandidate(c.getName())) {
candidates.add(c);
}
}
try {
// if we have matching candidates, then
// retransform those classes so that we
// will get callback to transform.
if (! candidates.isEmpty()) {
inst.retransformClasses(candidates.toArray(new Class[0]));
}
} catch (UnmodifiableClassException uce) {
}
}
public byte[] transform(ClassLoader loader, String className,
Class redefinedClass, ProtectionDomain protDomain,
byte[] classBytes) {
// check and dump .class file
if (isCandidate(className)) {
dumpClass(className, classBytes);
}
// we don't mess with .class file, just
// return null
return null;
}
private static boolean isCandidate(String className) {
// ignore array classes
if (className.charAt(0) == '[') {
return false;
}
// convert the class name to external name
className = className.replace('/', '.');
// check for name pattern match
return classes.matcher(className).matches();
}
private static void dumpClass(String className, byte[] classBuf) {
try {
// create package directories if needed
className = className.replace("/", File.separator);
StringBuilder buf = new StringBuilder();
buf.append(dumpDir);
buf.append(File.separatorChar);
int index = className.lastIndexOf(File.separatorChar);
if (index != -1) {
buf.append(className.substring(0, index));
}
String dir = buf.toString();
new File(dir).mkdirs();
// write .class file
String fileName = dumpDir +
File.separator + className + ".class";
FileOutputStream fos = new FileOutputStream(fileName);
fos.write(classBuf);
fos.close();
} catch (Exception exp) {
exp.printStackTrace();
}
}
// parse agent args of the form arg1=value1,arg2=value2
private static void parseArgs(String agentArgs) {
if (agentArgs != null) {
String[] args = agentArgs.split(",");
for (String arg: args) {
String[] tmp = arg.split("=");
if (tmp.length == 2) {
String name = tmp[0];
String value = tmp[1];
if (name.equals("dumpDir")) {
dumpDir = value;
} else if (name.equals("classes")) {
classes = Pattern.compile(value);
}
}
}
}
if (dumpDir == null) {
dumpDir = ".";
}
if (classes == null) {
classes = Pattern.compile(".\*");
}
}
}

File: manifest.mf


Premain-Class: ClassDumperAgent
Agent-Class: ClassDumperAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

File: Attacher.java


import com.sun.tools.attach.\*;
/\*\*
\* Simple attach-on-demand client tool that
\* loads the given agent into the given Java process.
\*/
public class Attacher {
public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.out.println("usage: java Attach <pid> <agent-jar-full-path> [<agent-args>]");
System.exit(1);
}
// JVM is identified by process id (pid).
VirtualMachine vm = VirtualMachine.attach(args[0]);
String agentArgs = (args.length > 2)? args[2] : null;
// load a specified agent onto the JVM
vm.loadAgent(args[1], agentArgs);
}
}

Steps to build class dumper:
  • javac ClassDumperAgent.java
  • jar cvfm classdumper.jar manifest.mf ClassDumperAgent.class
  • javac -cp $JAVA_HOME/lib/tools.jar Attacher.java

Steps to run class dumper:
  • start your target process -- I used java2d demo application in JDK
  • find the process id of your process using "jps" tool
  • java -cp $JAVA_HOME/lib/tools.jar:. Attacher <pid> <full-path-of-classdumper.jar> dumpDir=<dir>,classes=<name-pattern>

The above command will dump all classes matching the given name (regex) pattern into the given directory. The default dump directory is the current working directory (of the target application!). The default pattern is ".\*" i.e., match all classes loaded/will be loaded in the target application.

Now, how about a nice GUI that

  • shows all the Java processes
  • lets you can choose dump directory
  • Optionally, shows all the classes of a selected application

As usual, that is left as an exercise to the reader :-)

Join the discussion

Comments ( 3 )
  • Jochen &quot;blackdrag&quot; Theodorou Friday, March 30, 2007
    nice idea. Actually this could be helpful in Groovy to react to verify errors. The messages from the VM are not very nice to see the error in the bytecode.
  • Moinak Ghosh Sunday, April 1, 2007
    Nice! Can be the beginnings of a core dump equivalent for Java (does such a thing already exist ?) apps if we can also dump Java runtime state.
  • A. Sundararajan Sunday, April 1, 2007
    Hi Moinak Ghosh: There is a high level core dump debugger for JVM. See also: http://blogs.sun.com/sundararajan/entry/hotspot_source_serviceability_agent
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.Captcha