如何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块不能从一个表达式的中间开始之类的。