原文出处:http://rednaxelafx.iteye.com/blog/727938/
如何dump出一个Java进程里的类对应的Class文件?
大家可能对JVM很好奇,想看看运行中某时刻上JVM里各种内部数据结构的状态。可能有人想看堆上所有对象都有哪些,分别位于哪个分代之类;可能有人想看当前所有线程的stack trace;可能有人想看一个方法是否被JIT编译过,被编译后的native代码是怎样的。对Sun HotSpot JVM而言,这些需求都有现成的API可以满足——通过Serviceability Agent(下面简称SA)。大家熟悉的jstack、jmap等工具在使用-F参数启动时其实就是通过SA来实现功能的。
这里介绍的是按需把class给dump出来的方法。
为什么我们要dump运行中的JVM里的class呢?直接从classpath上把Class文件找到不就好了么?这样的话只要用ClassLoader.getResourceAsStream(name)就能行了。例如说要找foo.bar.Baz的Class文件,类似这样就行:
- ClassLoader loader = Thread.currentThread().getContextClassLoader();
- InputStream in = loader.getResourceAsStream("foo/bar/Baz.class");
- // 从in把内容拿出来,然后随便怎么处理
用Groovy的交互式解释器shell来演示一下:
- D:\>\sdk\groovy-1.7.2\bin\groovysh
- Groovy Shell (1.7.2, JVM: 1.6.0_20)
- Type 'help' or '\h' for help.
- -----------------------------------------------------------------------------
- groovy:000> loader = Thread.currentThread().contextClassLoader
- ===> org.codehaus.groovy.tools.RootLoader@61de33
- groovy:000> stream = loader.getResourceAsStream('java/util/ArrayList.class')
- ===> sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@5dfaf1
- groovy:000> file = new File('ArrayList.class')
- ===> ArrayList.class
- groovy:000> file.createNewFile()
- ===> true
- groovy:000> file << stream
- ===> ArrayList.class
- groovy:000> quit
这样就在当前目录建了个ArrayList.class文件,把java.util.ArrayList对应的Class文件拿到手了。
问题是,上述方式其实只是借助ClassLoader把它在classpath上能找到的Class文件复制了一份而已。如果我们想dump的类在加载时被修改过(例如说某些AOP的实现会这么做),或者在运行过程中被修改过(通过HotSwap),或者干脆就是运行时才创建出来的,那就没有现成的Class文件了。
需要注意,java.lang.Class<T>这个类虽然实现了java.io.Serializable接口,但直接将一个Class对象序列化是得不到对应的Class文件的。参考src/share/classes/java/lang/Class.java里的注释:
- package java.lang;
- import java.io.ObjectStreamField;
- // ...
- public final
- class Class<T> implements java.io.Serializable,
- java.lang.reflect.GenericDeclaration,
- java.lang.reflect.Type,
- java.lang.reflect.AnnotatedElement {
- /**
- * Class Class is special cased within the Serialization Stream Protocol.
- *
- * A Class instance is written initially into an ObjectOutputStream in the
- * following format:
- * <pre>
- * <code>TC_CLASS</code> ClassDescriptor
- * A ClassDescriptor is a special cased serialization of
- * a <code>java.io.ObjectStreamClass</code> instance.
- * </pre>
- * A new handle is generated for the initial time the class descriptor
- * is written into the stream. Future references to the class descriptor
- * are written as references to the initial class descriptor instance.
- *
- * @see java.io.ObjectStreamClass
- */
- private static final ObjectStreamField[] serialPersistentFields =
- new ObjectStreamField[0];
- // ...
- }
=================================================================
HotSpot有一套私有API提供了对JVM内部数据结构的审视功能,称为Serviceability Agent。它是一套Java API,虽然HotSpot是用C++写的,但SA提供了HotSpot中重要数据结构的Java镜像类,所以可以直接写Java代码来查看一个跑在HotSpot上的Java进程的内部状态。它也提供了一些封装好的工具,可以直接在命令行上跑,包括下面提到的ClassDump工具。
SA的一个重要特征是它是“进程外审视工具”。也就是说,SA并不运行在要审视的目标进程中,而是运行在一个独立的Java进程中,通过操作系统上提供的调试API来连接到目标进程上。这样,SA的运行不会受到目标进程状态的影响,因而可以用于审视一个已经挂起的Java进程,或者是core dump文件。当然,这也就意味这一个SA进程不能用于审视自己。
一个被调试器连接上的进程会被暂停下来。所以在SA连接到目标进程时,目标进程也是一直处于暂停状态的,直到SA解除连接。如果需要在线上使用SA的话需要小心,不要通过SA做过于耗时的分析,宁可先把数据都抓出来,把SA的连接解除掉之后再离线分析。目前的使用经验是,连接上一个小Java进程的话很快就好了,但连接上一个“大”的Java进程(堆比较大、加载的类比较多)可能会在连接阶段卡住好几分钟,线上需要慎用。
目前(JDK6)在Windows上SA没有随HotSpot一起发布,所以无法在Windows上使用;在Linux、Solaris、Mac上使用都没问题。从JDK7 build 64开始Windows版JDK也带上SA,如果有兴趣尝鲜JDK7的话可以试试(http://dlc.sun.com.edgesuite.net/jdk7/binaries/index.html),当前版本是build 103;正式的JDK7今年10月份应该有指望吧。
在Windows版JDK里带上SA的相关bug是:
Bug 6743339: Enable building sa-jdi.jar and sawindbg.dll on Windows with hotspot build
Bug 6755621: Include SA binaries into Windows JDK
前面废话了那么多,接下来回到正题,介绍一下ClassDump工具。
SA自带了一个能把当前在HotSpot中加载了的类dump成Class文件的工具,称为ClassDump。它的全限定类名是sun.jvm.hotspot.tools.jcore.ClassDump,有main()方法,可以直接从命令行执行;接收一个命令行参数,是目标Java进程的进程ID,可以通过JDK自带的jps工具查找Java进程的ID。要执行该工具需要确保SA的JAR包在classpath上,位于$JAVA_HOME/lib/sa-jdi.jar。
默认条件下执行ClassDump会把当前加载的所有Java类都dump到当前目录下,如果有全限定名相同但内容不同的类同时存在于一个Java进程中,那么dump的时候会有覆盖现象,实际dump出来的是同名的类的最后一个(根据ClassDump工具的遍历顺序)。
如果需要指定被dump的类的范围,可以自己写一个过滤器,在启动ClassDump工具时指定-Dsun.jvm.hotspot.tools.jcore.filter=filterClassName,具体方法见下面例子;如果需要指定dump出来的Class文件的存放路径,可以用-Dsun.jvm.hotspot.tools.jcore.outputDir=path来指定,path替换为实际路径。
以下演示在Linux上进行。大家或许已经知道,Sun JDK对反射调用方法有一些特别的优化,会在运行时生成专门的“调用类”来提高反射调用的性能。这次演示就来看看生成的类是什么样子的。
首先编写一个自定义的过滤器。只要实现sun.jvm.hotspot.tools.jcore.ClassFilter接口即可。
- import sun.jvm.hotspot.tools.jcore.ClassFilter;
- import sun.jvm.hotspot.oops.InstanceKlass;
- public class MyFilter implements ClassFilter {
- @Override
- public boolean canInclude(InstanceKlass kls) {
- String klassName = kls.getName().asString();
- return klassName.startsWith("sun/reflect/GeneratedMethodAccessor");
- }
- }
InstanceKlass对应于HotSpot中表示Java类的内部对象。Sun JDK为反射调用生成的类的名字形如sun/reflect/GeneratedMethodAccessorN,其中N是一个整数;所以只要看看类名是否以"sun/reflect/GeneratedMethodAccessor"开头就能找出来了。留意到这里包名的分隔符是“/”而不是“.”,这是Java类在JVM中的“内部名称”形式,参考Java虚拟机规范第二版4.2小节。
接下来写一个会引发JDK生成反射调用类的演示程序:
- import java.lang.reflect.Method;
- public class Demo {
- public static void main(String[] args) throws Exception {
- Method p = System.out.getClass().getMethod("println", String.class);
- for (int i = 0; i < 16; i++) {
- p.invoke(System.out, "demo");
- }
- System.in.read(); // block the program
- }
- }
让Demo跑起来,然后先不要让它结束。通过jps工具看看它的进程ID是多少:
- [sajia@sajia class_dump]$ jps
- 20542 Demo
- 20554 Jps
接下来执行ClassDump,指定上面自定义的过滤器(过滤器的类要在classpath上,本例中它在./bin):
- [sajia@sajia class_dump]$ java -classpath ".:./bin:$JAVA_HOME/lib/sa-jdi.jar" -Dsun.jvm.hotspot.tools.jcore.filter=MyFilter sun.jvm.hotspot.tools.jcore.ClassDump 20542
执行结束后,可以看到dump出了一个Class文件,在./sun/reflect/GeneratedMethodAccessor1.class;.是默认的输出目录,后面的目录结构对应包名。
用javap看看这个Class文件有啥内容:
- [sajia@sajia class_dump]$ javap -verbose sun.reflect.GeneratedMethodAccessor1
- public class sun.reflect.GeneratedMethodAccessor1 extends sun.reflect.MethodAccessorImpl
- minor version: 0
- major version: 46
- Constant pool:
- const #1 = Asciz sun/reflect/GeneratedMethodAccessor1;
- const #2 = class #1; // sun/reflect/GeneratedMethodAccessor1
- const #3 = Asciz sun/reflect/MethodAccessorImpl;
- const #4 = class #3; // sun/reflect/MethodAccessorImpl
- const #5 = Asciz java/io/PrintStream;
- const #6 = class #5; // java/io/PrintStream
- const #7 = Asciz println;
- const #8 = Asciz (Ljava/lang/String;)V;
- const #9 = NameAndType #7:#8;// println:(Ljava/lang/String;)V
- const #10 = Method #6.#9; // java/io/PrintStream.println:(Ljava/lang/String;)V
- const #11 = Asciz invoke;
- const #12 = Asciz (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;;
- const #13 = Asciz java/lang/String;
- const #14 = class #13; // java/lang/String
- const #15 = Asciz java/lang/Throwable;
- const #16 = class #15; // java/lang/Throwable
- const #17 = Asciz java/lang/ClassCastException;
- const #18 = class #17; // java/lang/ClassCastException
- const #19 = Asciz java/lang/NullPointerException;
- const #20 = class #19; // java/lang/NullPointerException
- const #21 = Asciz java/lang/IllegalArgumentException;
- const #22 = class #21; // java/lang/IllegalArgumentException
- const #23 = Asciz java/lang/reflect/InvocationTargetException;
- const #24 = class #23; // java/lang/reflect/InvocationTargetException
- const #25 = Asciz <init>;
- const #26 = Asciz ()V;
- const #27 = NameAndType #25:#26;// "<init>":()V
- const #28 = Method #20.#27; // java/lang/NullPointerException."<init>":()V
- const #29 = Method #22.#27; // java/lang/IllegalArgumentException."<init>":()V
- const #30 = Asciz (Ljava/lang/String;)V;
- const #31 = NameAndType #25:#30;// "<init>":(Ljava/lang/String;)V
- const #32 = Method #22.#31; // java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V
- const #33 = Asciz (Ljava/lang/Throwable;)V;
- const #34 = NameAndType #25:#33;// "<init>":(Ljava/lang/Throwable;)V
- const #35 = Method #24.#34; // java/lang/reflect/InvocationTargetException."<init>":(Ljava/lang/Throwable;)V
- const #36 = Method #4.#27; // sun/reflect/MethodAccessorImpl."<init>":()V
- const #37 = Asciz java/lang/Object;
- const #38 = class #37; // java/lang/Object
- const #39 = Asciz toString;
- const #40 = Asciz ()Ljava/lang/String;;
- const #41 = NameAndType #39:#40;// toString:()Ljava/lang/String;
- const #42 = Method #38.#41; // java/lang/Object.toString:()Ljava/lang/String;
- const #43 = Asciz Code;
- const #44 = Asciz Exceptions;
- const #45 = Asciz java/lang/Boolean;
- const #46 = class #45; // java/lang/Boolean
- const #47 = Asciz (Z)V;
- const #48 = NameAndType #25:#47;// "<init>":(Z)V
- const #49 = Method #46.#48; // java/lang/Boolean."<init>":(Z)V
- const #50 = Asciz booleanValue;
- const #51 = Asciz ()Z;
- const #52 = NameAndType #50:#51;// booleanValue:()Z
- const #53 = Method #46.#52; // java/lang/Boolean.booleanValue:()Z
- const #54 = Asciz java/lang/Byte;
- const #55 = class #54; // java/lang/Byte
- const #56 = Asciz (B)V;
- const #57 = NameAndType #25:#56;// "<init>":(B)V
- const #58 = Method #55.#57; // java/lang/Byte."<init>":(B)V
- const #59 = Asciz byteValue;
- const #60 = Asciz ()B;
- const #61 = NameAndType #59:#60;// byteValue:()B
- const #62 = Method #55.#61; // java/lang/Byte.byteValue:()B
- const #63 = Asciz java/lang/Character;
- const #64 = class #63; // java/lang/Character
- const #65 = Asciz (C)V;
- const #66 = NameAndType #25:#65;// "<init>":(C)V
- const #67 = Method #64.#66; // java/lang/Character."<init>":(C)V
- const #68 = Asciz charValue;
- const #69 = Asciz ()C;
- const #70 = NameAndType #68:#69;// charValue:()C
- const #71 = Method #64.#70; // java/lang/Character.charValue:()C
- const #72 = Asciz java/lang/Double;
- const #73 = class #72; // java/lang/Double
- const #74 = Asciz (D)V;
- const #75 = NameAndType #25:#74;// "<init>":(D)V
- const #76 = Method #73.#75; // java/lang/Double."<init>":(D)V
- const #77 = Asciz doubleValue;
- const #78 = Asciz ()D;
- const #79 = NameAndType #77:#78;// doubleValue:()D
- const #80 = Method #73.#79; // java/lang/Double.doubleValue:()D
- const #81 = Asciz java/lang/Float;
- const #82 = class #81; // java/lang/Float
- const #83 = Asciz (F)V;
- const #84 = NameAndType #25:#83;// "<init>":(F)V
- const #85 = Method #82.#84; // java/lang/Float."<init>":(F)V
- const #86 = Asciz floatValue;
- const #87 = Asciz ()F;
- const #88 = NameAndType #86:#87;// floatValue:()F
- const #89 = Method #82.#88; // java/lang/Float.floatValue:()F
- const #90 = Asciz java/lang/Integer;
- const #91 = class #90; // java/lang/Integer
- const #92 = Asciz (I)V;
- const #93 = NameAndType #25:#92;// "<init>":(I)V
- const #94 = Method #91.#93; // java/lang/Integer."<init>":(I)V
- const #95 = Asciz intValue;
- const #96 = Asciz ()I;
- const #97 = NameAndType #95:#96;// intValue:()I
- const #98 = Method #91.#97; // java/lang/Integer.intValue:()I
- const #99 = Asciz java/lang/Long;
- const #100 = class #99; // java/lang/Long
- const #101 = Asciz (J)V;
- const #102 = NameAndType #25:#101;// "<init>":(J)V
- const #103 = Method #100.#102; // java/lang/Long."<init>":(J)V
- const #104 = Asciz longValue;
- const #105 = Asciz ()J;
- const #106 = NameAndType #104:#105;// longValue:()J
- const #107 = Method #100.#106; // java/lang/Long.longValue:()J
- const #108 = Asciz java/lang/Short;
- const #109 = class #108; // java/lang/Short
- const #110 = Asciz (S)V;
- const #111 = NameAndType #25:#110;// "<init>":(S)V
- const #112 = Method #109.#111; // java/lang/Short."<init>":(S)V
- const #113 = Asciz shortValue;
- const #114 = Asciz ()S;
- const #115 = NameAndType #113:#114;// shortValue:()S
- const #116 = Method #109.#115; // java/lang/Short.shortValue:()S
- {
- public sun.reflect.GeneratedMethodAccessor1();
- Code:
- Stack=1, Locals=1, Args_size=1
- 0: aload_0
- 1: invokespecial #36; //Method sun/reflect/MethodAccessorImpl."<init>":()V
- 4: return
- public java.lang.Object invoke(java.lang.Object, java.lang.Object[]) throws java.lang.reflect.InvocationTargetException;
- Exceptions:
- throws java.lang.reflect.InvocationTargetException Code:
- Stack=5, Locals=3, Args_size=3
- 0: aload_1
- 1: ifnonnull 12
- 4: new #20; //class java/lang/NullPointerException
- 7: dup
- 8: invokespecial #28; //Method java/lang/NullPointerException."<init>":()V
- 11: athrow
- 12: aload_1
- 13: checkcast #6; //class java/io/PrintStream
- 16: aload_2
- 17: arraylength
- 18: sipush 1
- 21: if_icmpeq 32
- 24: new #22; //class java/lang/IllegalArgumentException
- 27: dup
- 28: invokespecial #29; //Method java/lang/IllegalArgumentException."<init>":()V
- 31: athrow
- 32: aload_2
- 33: sipush 0
- 36: aaload
- 37: checkcast #14; //class java/lang/String
- 40: invokevirtual #10; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 43: aconst_null
- 44: areturn
- 45: invokespecial #42; //Method java/lang/Object.toString:()Ljava/lang/String;
- 48: new #22; //class java/lang/IllegalArgumentException
- 51: dup_x1
- 52: swap
- 53: invokespecial #32; //Method java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V
- 56: athrow
- 57: new #24; //class java/lang/reflect/InvocationTargetException
- 60: dup_x1
- 61: swap
- 62: invokespecial #35; //Method java/lang/reflect/InvocationTargetException."<init>":(Ljava/lang/Throwable;)V
- 65: athrow
- Exception table:
- from to target type
- 12 40 45 Class java/lang/ClassCastException
- 12 40 45 Class java/lang/NullPointerException
- 40 43 57 Class java/lang/Throwable
- }
用Java来表现这个类的话,就是:
- package sun.reflect;
- public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
- public GeneratedMethodAccessor1() {
- super();
- }
- public Object invoke(Object obj, Object[] args)
- throws IllegalArgumentException, InvocationTargetException {
- // prepare the target and parameters
- if (obj == null) throw new NullPointerException();
- try {
- PrintStream target = (PrintStream) obj;
- if (args.length != 1) throw new IllegalArgumentException();
- String arg0 = (String) args[0];
- } catch (ClassCastException e) {
- throw new IllegalArgumentException(e.toString());
- } catch (NullPointerException e) {
- throw new IllegalArgumentException(e.toString());
- }
- // make the invocation
- try {
- target.println(arg0);
- return null;
- } catch (Throwable t) {
- throw new InvocationTargetException(t);
- }
- }
- }
这段Java代码跟实际的Class文件最主要的不同的地方在于实际的Class文件是用同一个异常处理器来处理ClassCastException与NullPointerException的。如果用Java 7的多重catch语法来写的话就是:
- package sun.reflect;
- public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
- public GeneratedMethodAccessor1() {
- super();
- }
- public Object invoke(Object obj, Object[] args)
- throws IllegalArgumentException, InvocationTargetException {
- // prepare the target and parameters
- if (obj == null) throw new NullPointerException();
- try {
- PrintStream target = (PrintStream) obj;
- if (args.length != 1) throw new IllegalArgumentException();
- String arg0 = (String) args[0];
- } catch (final ClassCastException | NullPointerException e) {
- throw new IllegalArgumentException(e.toString());
- }
- // make the invocation
- try {
- target.println(arg0);
- return null;
- } catch (Throwable t) {
- throw new InvocationTargetException(t);
- }
- }
- }
本来想顺带演示一下用Java反编译器把例子里的Class文件反编译为Java源码的,但用了JD和Jad都无法正确识别这里比较特别的Exceptions属性表,只好人肉反编译写出来……识别不出来也正常,毕竟Java 7之前在Java源码这层是没办法对同一个异常处理器处理指定多个异常类型。
要深究的话,上面人肉反编译的Java文件跟实际Class文件还有些细节差异。
例如说JDK在生成Class文件时为了方便所以把一大堆“很可能会用到”的常量都写到常量池里了,但在代码里可能并没有用到常量池里的所有项;如果用javac编译Java源码就不会出现这种状况。
又例如生成的Class文件里一个局部变量也没用,locals=3之中三个都是参数:第一个是this,第二个是obj,第三个是args。求值的中间结果全部都直接在操作数栈上用掉了。而在Java源码里无法写出这样的代码,像是说try块不能从一个表达式的中间开始之类的。