JIT编译找不到类?今天开始Sun的老blog真的搬迁了,从blogs.sun.com迁移到blogs.oracle.com。结果这些迁移了
JIT编译找不到类?
今天开始Sun的老blog真的搬迁了,从blogs.sun.com迁移到blogs.oracle.com。结果这些迁移了的blog里的老帖像洪水般一下就把我的reader冲爆了。
不过也好,有些老帖过了一段时间重新读也会有新体会。例如这篇,Why won't JRockit find my classes
原帖里提到这样一种情况。假如在一个路径“foo”里有下面的Foo类对应的Foo.class文件:
public class Foo { public Foo () { System.out.println("Foo created"); }}
然后在“当前目录”下有下面的ClasspathTest类对应的Classpath.class文件:
import java.io.File;import java.net.URLClassLoader;import java.net.URL;import java.lang.reflect.Method;public class ClasspathTest { private static final Class[] parameters = new Class[]{URL.class}; // Adds a URL to the classpath (by some dubious means) // method.setAccessible(true) is not the trademark of good code public static void addURL(URL u) throws Exception { Method method = URLClassLoader.class.getDeclaredMethod("addURL", parameters); method.setAccessible(true); method.invoke((URLClassLoader) ClassLoader.getSystemClassLoader(), new Object[]{u}); } public static void main(String[] arg) throws Exception{ // Add foo to the classpath, then create a Foo object addURL(new File("foo").toURL()); Foo a = new Foo(); }}
那么用JRockit直接在当前目录运行ClasspathTest会遇到NoClassDefFoundError:
D:\test\test_hotspot_compilers\test_c2_Xcomp_classpath>C:\sdk\Java\jrmc-4.0.0-1.6.0\bin\java ClasspathTestException in thread "Main Thread" java.lang.NoClassDefFoundError: Foo at ClasspathTest.main(ClasspathTest.java:20)
别人帖里的例子自己都验证一遍是个好习惯。这里我们实际运行JRockit R28确实看到跟描述一样的异常了。
原文同时也提到同一个程序用Oracle/Sun JDK的HotSpot VM来跑就没问题:
D:\test\test_hotspot_compilers\test_c2_Xcomp_classpath>D:\sdk\jdk1.6.0_25\bin\java ClasspathTestFoo created
原文说,造成这个差异的原因是HotSpot VM有解释器,执行一个方法并不需要事先将该方法中所有类都加载,而可以一点点执行,到方法中间碰到某个未加载的类的时候再去加载它。例子中的代码做了件很tricky的事情:改变了系统类加载器可访问到的路径。等到HotSpot VM的解释器尝试用系统类加载器去加载Foo的时候,Foo.class已经在它能找到的路径上了。
而JRockit则不同,没有解释器,所有Java方法都必须先JIT编译了才可以开始执行。其中有个实现细节,就是JRockit要求在编译某个方法的时候就保证其中涉及的类型都加载好了。但是,上面的例子里,当JRockit的JIT编译器尝试用系统类加载器去加载Foo的时候,该类加载器还没得到“foo”这个路径,所以Foo类就加载不到了。
当然并不是说只有JIT编译器而没有解释器的设计就一定会导致这种结果,只是JRockit选择了这样实现而已。
======================================================
那么HotSpot VM里能够制造出同样的情况呢?我们知道,HotSpot VM可以通过
-Xcomp,或者诸如
-XX:+UseCompiler -XX:-UseInterpreter -XX:-BackgroundCompilation这样的参数强行指定(尽量)不使用解释器而(尽量)只用JIT编译器来处理Java程序的执行。
在这种条件下HotSpot VM会不会也跟JRockit一样,执行上面的例子报错说找不到类呢?
让我们试试看。下面的例子都是在32位Windows XP上用JDK 6 update 25跑的:
D:\test\test_hotspot_compilers\test_c2_Xcomp_classpath>D:\sdk\jdk1.6.0_25\bin\java -server -Xcomp ClasspathTestFoo created
嗯?没报错。
那有没有可能是HotSpot VM实际上没编译ClasspathTest.main()方法呢?用
-XX:+PrintCompilation来看:
java -server -Xcomp -XX:+PrintCompilation ClasspathTest
... 567 270 b java.lang.reflect.Method::getModifiers (5 bytes) 567 271 b ClasspathTest::main (24 bytes) 567 271 made not entrant ClasspathTest::main (24 bytes) 567 272 b java.io.File::toURL (23 bytes) 567 272 made not entrant java.io.File::toURL (23 bytes) 568 273 b java.io.File::getAbsolutePath (8 bytes)...
有的,这个main()方法编译了。
但是,HotSpot的server编译器到底给main()方法生成了怎样的代码呢?用
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly来看:(用法参考以前一帖)
java -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly ClasspathTest
# {method} 'main' '([Ljava/lang/String;)V' in 'ClasspathTest' # parm0: ecx = '[Ljava/lang/String;' # [sp+0x10] (sp of caller) 0x00a33b00: mov %eax,-0x3000(%esp) 0x00a33b07: push %ebp 0x00a33b08: sub $0x8,%esp ;*synchronization entry ; - ClasspathTest::main@-1 (line 19) 0x00a33b0e: mov $0xa,%ecx 0x00a33b13: call 0x0099c700 ; OopMap{off=24} ;*new ; - ClasspathTest::main@0 (line 19) ; {runtime_call} 0x00a33b18: int3 ;*new ; - ClasspathTest::main@0 (line 19)
可以看到,生成的代码只做了这么几件事情:
1、检查调用栈是否要溢出了(0x00a33b00)
2、保存老栈帧信息,建立新栈帧(0x00a33b07-0x00a33b08)
3、?(0x00a33b0e)
4、不应该跑到这里来,否则中断(0x00a33b18)
这问号就是有趣的地方了。如果这个问号表示的代码不足以执行main()的内容的话,那main()到底是如何被执行的呢?
我们可以用另一组参数来了解这神秘的“0x0099c700”地址到底是什么东西,
-XX:+UnlockDiagnosticVMOptions -XX:+PrintStubCodejava -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintStubCode ClasspathTest
Decoding UncommonTrapBlob@0x0099c700 0x0099c6c8[Disassembling for mach='i386'] 0x0099c700: sub $0xc,%esp 0x0099c703: mov %ebp,0x8(%esp) 0x0099c707: emms 0x0099c709: mov %fs:0x0(,%eiz,1),%edx 0x0099c711: mov -0xc(%edx),%edx 0x0099c714: mov %esp,0x118(%edx) 0x0099c71a: mov %edx,(%esp) 0x0099c71d: mov %ecx,0x4(%esp) 0x0099c721: call 0x6dc77300 0x0099c726: mov %fs:0x0(,%eiz,1),%ecx 0x0099c72e: mov -0xc(%ecx),%ecx 0x0099c731: movl $0x0,0x118(%ecx) 0x0099c73b: mov %eax,%edi 0x0099c73d: add $0xc,%esp 0x0099c740: mov (%edi),%ecx 0x0099c742: add %ecx,%esp 0x0099c744: mov 0xc(%edi),%ebx 0x0099c747: mov %esp,%ecx 0x0099c749: mov %ebx,-0x1000(%ecx) 0x0099c74f: sub $0x1000,%ecx 0x0099c755: sub $0x1000,%ebx 0x0099c75b: jg 0x0099c749 0x0099c75d: mov %ebx,(%ecx) 0x0099c75f: mov %ebx,-0x1000(%ecx) 0x0099c765: mov 0x14(%edi),%ecx 0x0099c768: pop %esi 0x0099c769: mov 0x10(%edi),%esi 0x0099c76c: mov 0x8(%edi),%ebx 0x0099c76f: mov %ebx,0x20(%edi) 0x0099c772: mov 0x24(%edi),%ebp 0x0099c775: mov %esp,0x2c(%edi) 0x0099c778: mov 0x4(%edi),%ebx 0x0099c77b: sub %ebx,%esp 0x0099c77d: mov (%esi),%ebx 0x0099c77f: sub $0x8,%ebx 0x0099c782: pushl (%ecx) 0x0099c784: push %ebp 0x0099c785: mov %esp,%ebp 0x0099c787: sub %ebx,%esp 0x0099c789: mov 0x2c(%edi),%ebx 0x0099c78c: movl $0x0,-0x8(%ebp) 0x0099c793: mov %ebx,-0x4(%ebp) 0x0099c796: mov %esp,0x2c(%edi) 0x0099c799: add $0x4,%esi 0x0099c79c: add $0x4,%ecx 0x0099c79f: decl 0x20(%edi) 0x0099c7a2: jne 0x0099c77d 0x0099c7a4: pushl (%ecx) 0x0099c7a6: push %ebp 0x0099c7a7: mov %esp,%ebp 0x0099c7a9: sub $0x8,%esp 0x0099c7ac: mov %fs:0x0(,%eiz,1),%edi 0x0099c7b4: mov -0xc(%edi),%edi 0x0099c7b7: mov %ebp,0x120(%edi) 0x0099c7bd: mov %esp,0x118(%edi) 0x0099c7c3: mov %edi,(%esp) 0x0099c7c6: movl $0x2,0x4(%esp) 0x0099c7ce: call 0x6dc75d40 0x0099c7d3: mov %fs:0x0(,%eiz,1),%edi 0x0099c7db: mov -0xc(%edi),%edi 0x0099c7de: movl $0x0,0x118(%edi) 0x0099c7e8: movl $0x0,0x120(%edi) 0x0099c7f2: mov %ebp,%esp 0x0099c7f4: pop %ebp 0x0099c7f5: ret 0x0099c7f6: .byte 0xc1 0x0099c7f7: .byte 0x4
嗯,很长很诡异的代码对吧?实际上这块代码是一个“uncommon trap”,也就是当HotSpot的server编译器遇到它觉得不会发生的、或不方便处理的部分的时候会用到的一块代码。
前面提到的“问号”的代码,就是用于跳进该“uncommon trap”的。
但main()方法为啥刚一进去就要无条件跳进一个uncommon trap呢?这次我们换用一个fastdebug版的JDK 6 update 25,然后用
-XX:+PrintOptoAssembly来看看:
java -server -Xcomp -XX:+PrintOptoAssembly ClasspathTest
000 B1: #N1 <- BLOCK HEAD IS JUNK Freq: 1000 # stack bangPUSHL EBPSUB ESP,8# Create frame00e MOV ECX,#10013 CALL,static wrapper for: uncommon_trap(reason='unloaded' action='reinterpret' index='10') # ClasspathTest::main @ bci:0 L[0]=_ L[1]=_ # OopMap{off=24}018 INT3 ; ShouldNotReachHere018
代码里嵌着的注释说得很明白了,跳进uncommon trap的原因是“unloaded”,也就是该方法需要用的类要么尚未加载,要么虽然加载过但已经卸载掉了。相应采取的措施就是“reinterpret”,也就是退回到解释器去执行。
具体做检查的代码在这里:
void ciTypeFlow::StateVector::do_new(ciBytecodeStream* str) { bool will_link; ciKlass* klass = str->get_klass(will_link); if (!will_link || str->is_unresolved_klass()) { trap(str, klass, str->get_klass_index()); } else { push_object(klass); }}
也就是HotSpot的JIT编译器做初步类型分析的时候就已经发现有问题了。
看到了么?HotSpot的server编译器只是等同于给例子里的main()生成了个解释器入口,让它跳进去而已,并没有真的把main()方法编译出来。
所以说HotSpot的server编译器其实跟JRockit的JIT编译器在这点上都偷懒了…
只不过HotSpot能退回到解释器里,所以编译器偷懒了没关系,程序还能跑,就是慢一些;JRockit的编译器偷懒了,Java程序搞不好就得撞墙了
P.S. 顺带一提:HotSpot的server编译器并不会在接受某个方法的编译请求时强制要求里面涉及的所有类型已经被加载,只要求在方法签名里出现的类型在编译前已经被加载,并要求在编译前该方法里涉及的所有的字符串常量已经resolve完(具体执行这些约束的时负责分派编译请求的CompilerBroker而不是server编译器自身,见回复里的讨论)。
所以这个例子就算给了-classpath ".;foo"也会得到一样的、只含一个uncommon trap的main()方法。
P.P.S. 顺带找个地方记下ciTypeFlow的作用:
http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2008-May.txt
I'm glad it's helpful!
> So does the ciTypeFlow pass actually load the unloaded classes for
> you?
No, the JIT tries pretty hard not to load classes. IIRC, the only
exception to this rule is the call to load_signature_classes in
compileBroker.cpp.
JIT compilation should be transparent to Java execution, but loading
classes causes class loader code to execute. If the JIT causes
bytecode execution, then the JIT can cause application state changes,
which explores new application states unnecessarily. This can expose
JIT-entangled bugs in the application. You want this in stress
testing, but not in the field.
The JVM spec. allows class loading--not initialization--for any
reason, but it's better (for system reproducibility) if the JIT has
no detectable effect on app. state except speedups.
-- John
1031 // some prerequisites that are compiler specific 1032 if (compiler(comp_level)->is_c2()) { 1033 method->constants()->resolve_string_constants(CHECK_0); 1034 // Resolve all classes seen in the signature of the method 1035 // we are compiling. 1036 methodOopDesc::load_signature_classes(method, CHECK_0); 1037 } 6 楼 wkoffee 2011-05-11 发起compiler请求到底算不算compiler的职责,每个人可以自己理解,我个人认为不算。
这个要求究竟有多大实际意义也很可疑,正常的java程序在发起请求的时候这些类应该都已加载过,即使有个别极端情况没有加载,compiler简单的bailout就可以,所以我觉得这个看上去更像是为了做测试所加的内容。 7 楼 RednaxelaFX 2011-05-11 wkoffee 写道这个要求究竟有多大实际意义也很可疑,正常的java程序在发起请求的时候这些类应该都已加载过,即使有个别极端情况没有加载,compiler简单的bailout就可以,所以我觉得这个看上去更像是为了做测试所加的内容。
如果不是-Xcomp的话,以默认的mixed mode是不应该遇到方法签名上有未加载的类型的。就这点而言在HotSpot上跑的一般Java程序是不会遇到帖子里说的问题的。
话说回来,原本为啥会弄CTW模式来测试HotSpot的编译器的…CTW也确实就是只“为了做测试”而存在的模式,而那个模式下会出现很多奇怪的情况,跟-Xcomp有相似之处 8 楼 RednaxelaFX 2011-05-12 wkoffee 写道发起compiler请求到底算不算compiler的职责,每个人可以自己理解,我个人认为不算。
嗯,CompilerBroker名字都已经这么叫了,也就是个转发者的角色。如果说它是解释器与JIT编译器之间的接口上的东西也对,那么两边都算不上。但CompilerBroker在tiered compilation里多数时候都在跟compiled code打交道,从compiled code里跟compiler打交道算不算是整个compilation system的一部分就模糊了。
不过你说得对,我应该把正文的最后那段改改,强调只是个约束而不是opto主动发起的动作。
wkoffee 写道严格的说jrockit的做法是不符合java规范的,jvm的规范要求在第一次访问类的时候加载,这个程序在执行new Foo的时候classpath已经设好了,classloader应该可以加载类,所以这是个合法的程序。jrockit简单的退出是有问题的。
先前没仔细看这条回复,请问严格来说JRockit的这个做法是违反了第几版JVM规范的哪部分?给我的感觉是一个只能通过强行调用non-public方法做到的事情不会是规范里会写到的。 9 楼 wkoffee 2011-05-12 引用wkoffee 写道
严格的说jrockit的做法是不符合java规范的,jvm的规范要求在第一次访问类的时候加载,这个程序在执行new Foo的时候classpath已经设好了,classloader应该可以加载类,所以这是个合法的程序。jrockit简单的退出是有问题的。
先前没仔细看这条回复,请问严格来说JRockit的这个做法是违反了第几版JVM规范的哪部分?给我的感觉是一个只能通过强行调用non-public方法做到的事情不会是规范里会写到的。
在jvm spec 2.17.2 中
"The loading process is implemented by the class ClassLoader and its subclasses. Different subclasses of ClassLoader may implement different loading policies. In particular, a class loader may cache binary representations of classes and interfaces, prefetch them based on expected usage, or load a group of related classes together. These activities may not be completely transparent to a running application if, for example, a newly compiled version of a class is not found because an older version is cached by a class loader. It is the responsibility of a class loader, however, to reflect loading errors only at points in the program where they could have arisen without prefetching or group loading.
If an error occurs during class loading, then an instance of one of the following subclasses of class LinkageError will be thrown at any point in the program that (directly or indirectly) uses the type:"
而且下面列出NoClassDefFoundError是loading的error,那它应该在用到该类型的时候抛出。在这个例子中,main方法执行的入口时候并没有用到Foo,只有在new Foo的时候用到,jrockit的做法相当于prefetch或者group loading,但jvm有责任确保不应该因为这个引起额外的异常,(这里有点绕,我理解是如果有异常,那不使用prefetch or group loading的classloader也会报错)
这个例子里使用反射不知道是不是很重要,URLClassLoader的addUrl方法是protected的,自定义classloader就可以绕过它。但不知道是不是因为用了反射才会出现这个结果。 10 楼 wkoffee 2011-05-12 引用但CompilerBroker在tiered compilation里多数时候都在跟compiled code打交道,从compiled code里跟compiler打交道算不算是整个compilation system的一部分就模糊了。
能详细说说这一点吗,我印象中CompilerBroker只是发起请求,tiered compilation时为什么compiled code要和它打交道。 11 楼 RednaxelaFX 2011-05-13 wkoffee 写道引用wkoffee 写道
严格的说jrockit的做法是不符合java规范的,jvm的规范要求在第一次访问类的时候加载,这个程序在执行new Foo的时候classpath已经设好了,classloader应该可以加载类,所以这是个合法的程序。jrockit简单的退出是有问题的。
先前没仔细看这条回复,请问严格来说JRockit的这个做法是违反了第几版JVM规范的哪部分?给我的感觉是一个只能通过强行调用non-public方法做到的事情不会是规范里会写到的。
在jvm spec 2.17.2 中
"The loading process is implemented by the class ClassLoader and its subclasses. Different subclasses of ClassLoader may implement different loading policies. In particular, a class loader may cache binary representations of classes and interfaces, prefetch them based on expected usage, or load a group of related classes together. These activities may not be completely transparent to a running application if, for example, a newly compiled version of a class is not found because an older version is cached by a class loader. It is the responsibility of a class loader, however, to reflect loading errors only at points in the program where they could have arisen without prefetching or group loading.
If an error occurs during class loading, then an instance of one of the following subclasses of class LinkageError will be thrown at any point in the program that (directly or indirectly) uses the type:"
而且下面列出NoClassDefFoundError是loading的error,那它应该在用到该类型的时候抛出。在这个例子中,main方法执行的入口时候并没有用到Foo,只有在new Foo的时候用到,jrockit的做法相当于prefetch或者group loading,但jvm有责任确保不应该因为这个引起额外的异常,(这里有点绕,我理解是如果有异常,那不使用prefetch or group loading的classloader也会报错)
这个例子里使用反射不知道是不是很重要,URLClassLoader的addUrl方法是protected的,自定义classloader就可以绕过它。但不知道是不是因为用了反射才会出现这个结果。
前面的解释都说得通。但我觉得问题点在于system class loader到底是个什么类型的对象,在JVM规范里没定义,所以本来Java应用不应该能够假定它会是URLClassLoader的子类的实例。这样的话,依赖该class loader有addURL方法、并且通过它修改class path的做法的结果会怎样本来就应该也是未定义的吧。
刚试着搜了一下,在JVM规范里没找到"system class loader"的定义。这玩儿是在哪里定义的呢?