如何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文件,类似这样就行:

Java代码  

1. ClassLoader loader = Thread.currentThread().getContextClassLoader();  
2. InputStream in = loader.getResourceAsStream("foo/bar/Baz.class");  
3. // 从in把内容拿出来,然后随便怎么处理

用Groovy的交互式解释器shell来演示一下:

Groovysh代码  

1. D:\>\sdk\groovy-1.7.2\bin\groovysh  
2. Groovy Shell (1.7.2, JVM: 1.6.0_20)  
3. Type 'help' or '\h'
4. -----------------------------------------------------------------------------  
5. groovy:000> loader = Thread.currentThread().contextClassLoader  
6. ===> org.codehaus.groovy.tools.RootLoader@61de33  
7. groovy:000> stream = loader.getResourceAsStream('java/util/ArrayList.class')  
8. ===> sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@5dfaf1  
9. groovy:000> file = new File('ArrayList.class')  
10. ===> ArrayList.class  
11. groovy:000> file.createNewFile()  
12. ===> true  
13. groovy:000> file << stream  
14. ===> ArrayList.class  
15. 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里的注释:

Java代码  

1. package
2.   
3. import
4. // ...
5.   
6. public final
7. class Class<T> implements
8.                   java.lang.reflect.GenericDeclaration,   
9.                   java.lang.reflect.Type,  
10.                               java.lang.reflect.AnnotatedElement {  
11. /**
12.      * Class Class is special cased within the Serialization Stream Protocol. 
13.      *
14.      * A Class instance is written initially into an ObjectOutputStream in the 
15.      * following format:
16.      * <pre>
17.      *      <code>TC_CLASS</code> ClassDescriptor
18.      *      A ClassDescriptor is a special cased serialization of 
19.      *      a <code>java.io.ObjectStreamClass</code> instance. 
20.      * </pre>
21.      * A new handle is generated for the initial time the class descriptor
22.      * is written into the stream. Future references to the class descriptor
23.      * are written as references to the initial class descriptor instance.
24.      *
25.      * @see java.io.ObjectStreamClass
26.      */
27. private static final
28. new ObjectStreamField[0];  
29.       
30. // ...
31. }

=================================================================

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 buildBug 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接口即可。

Java代码  

InstanceKlass对应于HotSpot中表示Java类的内部对象。Sun JDK为反射调用生成的类的名字形如sun/reflect/GeneratedMethodAccessorN,其中N是一个整数;所以只要看看类名是否以"sun/reflect/GeneratedMethodAccessor"开头就能找出来了。留意到这里包名的分隔符是“/”而不是“.”,这是Java类在JVM中的“内部名称”形式,参考Java虚拟机规范第二版4.2小节。

1. import
2. import
3.   
4. public class MyFilter implements
5. @Override
6. public boolean
7.         String klassName = kls.getName().asString();  
8. return klassName.startsWith("sun/reflect/GeneratedMethodAccessor");  
9.     }  
10. }

接下来写一个会引发JDK生成反射调用类的演示程序:

Java代码  

1. import
2.   
3. public class
4. public static void main(String[] args) throws
5. "println", String.class);  
6. for (int i = 0; i < 16; i++) {  
7. "demo");  
8.         }  
9. // block the program
10.     }  
11. }

让Demo跑起来,然后先不要让它结束。通过jps工具看看它的进程ID是多少:

Command prompt代码  

1. [sajia@sajia class_dump]$ jps  
2. 20542
3. 20554

接下来执行ClassDump,指定上面自定义的过滤器(过滤器的类要在classpath上,本例中它在./bin):

Command prompt代码  

执行结束后,可以看到dump出了一个Class文件,在./sun/reflect/GeneratedMethodAccessor1.class;.是默认的输出目录,后面的目录结构对应包名。

1. [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

用javap看看这个Class文件有啥内容:

Javap代码  

1. [sajia@sajia class_dump]$ javap -verbose sun.reflect.GeneratedMethodAccessor1  
2. public class sun.reflect.GeneratedMethodAccessor1 extends sun.reflect.MethodAccessorImpl  
3. 0
4. 46
5.   Constant pool:  
6. const #1
7. const #2 = class        #1;     //  sun/reflect/GeneratedMethodAccessor1  
8. const #3
9. const #4 = class        #3;     //  sun/reflect/MethodAccessorImpl  
10. const #5
11. const #6 = class        #5;     //  java/io/PrintStream  
12. const #7
13. const #8
14. const #9 = NameAndType  #7:#8;//  println:(Ljava/lang/String;)V  
15. const #10 = Method      #6.#9;  //  java/io/PrintStream.println:(Ljava/lang/String;)V  
16. const #11
17. const #12
18. const #13
19. const #14 = class       #13;    //  java/lang/String  
20. const #15
21. const #16 = class       #15;    //  java/lang/Throwable  
22. const #17
23. const #18 = class       #17;    //  java/lang/ClassCastException  
24. const #19
25. const #20 = class       #19;    //  java/lang/NullPointerException  
26. const #21
27. const #22 = class       #21;    //  java/lang/IllegalArgumentException  
28. const #23
29. const #24 = class       #23;    //  java/lang/reflect/InvocationTargetException  
30. const #25
31. const #26
32. const #27 = NameAndType #25:#26;//  "<init>":()V  
33. const #28 = Method      #20.#27;        //  java/lang/NullPointerException."<init>":()V  
34. const #29 = Method      #22.#27;        //  java/lang/IllegalArgumentException."<init>":()V  
35. const #30
36. const #31 = NameAndType #25:#30;//  "<init>":(Ljava/lang/String;)V  
37. const #32 = Method      #22.#31;        //  java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V  
38. const #33
39. const #34 = NameAndType #25:#33;//  "<init>":(Ljava/lang/Throwable;)V  
40. const #35 = Method      #24.#34;        //  java/lang/reflect/InvocationTargetException."<init>":(Ljava/lang/Throwable;)V  
41. const #36 = Method      #4.#27; //  sun/reflect/MethodAccessorImpl."<init>":()V  
42. const #37
43. const #38 = class       #37;    //  java/lang/Object  
44. const #39
45. const #40
46. const #41 = NameAndType #39:#40;//  toString:()Ljava/lang/String;  
47. const #42 = Method      #38.#41;        //  java/lang/Object.toString:()Ljava/lang/String;  
48. const #43
49. const #44
50. const #45
51. const #46 = class       #45;    //  java/lang/Boolean  
52. const #47
53. const #48 = NameAndType #25:#47;//  "<init>":(Z)V  
54. const #49 = Method      #46.#48;        //  java/lang/Boolean."<init>":(Z)V  
55. const #50
56. const #51
57. const #52 = NameAndType #50:#51;//  booleanValue:()Z  
58. const #53 = Method      #46.#52;        //  java/lang/Boolean.booleanValue:()Z  
59. const #54
60. const #55 = class       #54;    //  java/lang/Byte  
61. const #56
62. const #57 = NameAndType #25:#56;//  "<init>":(B)V  
63. const #58 = Method      #55.#57;        //  java/lang/Byte."<init>":(B)V  
64. const #59
65. const #60
66. const #61 = NameAndType #59:#60;//  byteValue:()B  
67. const #62 = Method      #55.#61;        //  java/lang/Byte.byteValue:()B  
68. const #63
69. const #64 = class       #63;    //  java/lang/Character  
70. const #65
71. const #66 = NameAndType #25:#65;//  "<init>":(C)V  
72. const #67 = Method      #64.#66;        //  java/lang/Character."<init>":(C)V  
73. const #68
74. const #69
75. const #70 = NameAndType #68:#69;//  charValue:()C  
76. const #71 = Method      #64.#70;        //  java/lang/Character.charValue:()C  
77. const #72
78. const #73 = class       #72;    //  java/lang/Double  
79. const #74
80. const #75 = NameAndType #25:#74;//  "<init>":(D)V  
81. const #76 = Method      #73.#75;        //  java/lang/Double."<init>":(D)V  
82. const #77
83. const #78
84. const #79 = NameAndType #77:#78;//  doubleValue:()D  
85. const #80 = Method      #73.#79;        //  java/lang/Double.doubleValue:()D  
86. const #81
87. const #82 = class       #81;    //  java/lang/Float  
88. const #83
89. const #84 = NameAndType #25:#83;//  "<init>":(F)V  
90. const #85 = Method      #82.#84;        //  java/lang/Float."<init>":(F)V  
91. const #86
92. const #87
93. const #88 = NameAndType #86:#87;//  floatValue:()F  
94. const #89 = Method      #82.#88;        //  java/lang/Float.floatValue:()F  
95. const #90
96. const #91 = class       #90;    //  java/lang/Integer  
97. const #92
98. const #93 = NameAndType #25:#92;//  "<init>":(I)V  
99. const #94 = Method      #91.#93;        //  java/lang/Integer."<init>":(I)V  
100. const #95
101. const #96
102. const #97 = NameAndType #95:#96;//  intValue:()I  
103. const #98 = Method      #91.#97;        //  java/lang/Integer.intValue:()I  
104. const #99
105. const #100 = class      #99;    //  java/lang/Long  
106. const #101
107. const #102 = NameAndType        #25:#101;//  "<init>":(J)V  
108. const #103 = Method     #100.#102;      //  java/lang/Long."<init>":(J)V  
109. const #104
110. const #105
111. const #106 = NameAndType        #104:#105;//  longValue:()J  
112. const #107 = Method     #100.#106;      //  java/lang/Long.longValue:()J  
113. const #108
114. const #109 = class      #108;   //  java/lang/Short  
115. const #110
116. const #111 = NameAndType        #25:#110;//  "<init>":(S)V  
117. const #112 = Method     #109.#111;      //  java/lang/Short."<init>":(S)V  
118. const #113
119. const #114
120. const #115 = NameAndType        #113:#114;//  shortValue:()S  
121. const #116 = Method     #109.#115;      //  java/lang/Short.shortValue:()S  
122.   
123. {  
124. public sun.reflect.GeneratedMethodAccessor1();  
125.   Code:  
126. 1, Locals=1, Args_size=1
127. 0:   aload_0  
128. 1:   invokespecial   #36; //Method sun/reflect/MethodAccessorImpl."<init>":()V  
129. 4:   return  
130.   
131. public java.lang.Object invoke(java.lang.Object, java.lang.Object[])   throws java.lang.reflect.InvocationTargetException;  
132.   Exceptions:   
133.    throws java.lang.reflect.InvocationTargetException  Code:  
134. 5, Locals=3, Args_size=3
135. 0:   aload_1  
136. 1:   ifnonnull       12
137. 4:   new     #20; //class java/lang/NullPointerException  
138. 7:   dup  
139. 8:   invokespecial   #28; //Method java/lang/NullPointerException."<init>":()V  
140. 11:  athrow  
141. 12:  aload_1  
142. 13:  checkcast       #6; //class java/io/PrintStream  
143. 16:  aload_2  
144. 17:  arraylength  
145. 18:  sipush  1
146. 21:  if_icmpeq       32
147. 24:  new     #22; //class java/lang/IllegalArgumentException  
148. 27:  dup  
149. 28:  invokespecial   #29; //Method java/lang/IllegalArgumentException."<init>":()V  
150. 31:  athrow  
151. 32:  aload_2  
152. 33:  sipush  0
153. 36:  aaload  
154. 37:  checkcast       #14; //class java/lang/String  
155. 40:  invokevirtual   #10; //Method java/io/PrintStream.println:(Ljava/lang/String;)V  
156. 43:  aconst_null  
157. 44:  areturn  
158. 45:  invokespecial   #42; //Method java/lang/Object.toString:()Ljava/lang/String;  
159. 48:  new     #22; //class java/lang/IllegalArgumentException  
160. 51:  dup_x1  
161. 52:  swap  
162. 53:  invokespecial   #32; //Method java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V  
163. 56:  athrow  
164. 57:  new     #24; //class java/lang/reflect/InvocationTargetException  
165. 60:  dup_x1  
166. 61:  swap  
167. 62:  invokespecial   #35; //Method java/lang/reflect/InvocationTargetException."<init>":(Ljava/lang/Throwable;)V  
168. 65:  athrow  
169.   Exception table:  
170.    from   to  target type  
171. 12    40    45
172.   
173. 12    40    45
174.   
175. 40    43    57
176.   
177.   
178. }

用Java来表现这个类的话,就是:

Java代码  

这段Java代码跟实际的Class文件最主要的不同的地方在于实际的Class文件是用同一个异常处理器来处理ClassCastException与NullPointerException的。如果用Java 7的多重catch语法来写的话就是:

1. package
2.   
3. public class GeneratedMethodAccessor1 extends
4. public
5. super();  
6.     }  
7.       
8. public
9. throws
10. // prepare the target and parameters
11. if (obj == null) throw new
12. try
13.             PrintStream target = (PrintStream) obj;  
14. if (args.length != 1) throw new
15. 0];  
16. catch
17. throw new
18. catch
19. throw new
20.         }  
21. // make the invocation
22. try
23.             target.println(arg0);  
24. return null;  
25. catch
26. throw new
27.         }  
28.     }  
29. }

Java代码

本来想顺带演示一下用Java反编译器把例子里的Class文件反编译为Java源码的,但用了JD和Jad都无法正确识别这里比较特别的Exceptions属性表,只好人肉反编译写出来……识别不出来也正常,毕竟Java 7之前在Java源码这层是没办法对同一个异常处理器处理指定多个异常类型。

1. package
2.   
3. public class GeneratedMethodAccessor1 extends
4. public
5. super();  
6.     }  
7.       
8. public
9. throws
10. // prepare the target and parameters
11. if (obj == null) throw new
12. try
13.             PrintStream target = (PrintStream) obj;  
14. if (args.length != 1) throw new
15. 0];  
16. catch (final
17. throw new
18.         }  
19. // make the invocation
20. try
21.             target.println(arg0);  
22. return null;  
23. catch
24. throw new
25.         }  
26.     }  
27. }

要深究的话,上面人肉反编译的Java文件跟实际Class文件还有些细节差异。

例如说JDK在生成Class文件时为了方便所以把一大堆“很可能会用到”的常量都写到常量池里了,但在代码里可能并没有用到常量池里的所有项;如果用javac编译Java源码就不会出现这种状况。

又例如生成的Class文件里一个局部变量也没用,locals=3之中三个都是参数:第一个是this,第二个是obj,第三个是args。求值的中间结果全部都直接在操作数栈上用掉了。而在Java源码里无法写出这样的代码,像是说try块不能从一个表达式的中间开始之类的。