JIT编译找不到类?

57 篇文章 0 订阅
26 篇文章 0 订阅
今天开始Sun的老blog真的搬迁了,从blogs.sun.com迁移到blogs.oracle.com。结果这些迁移了的blog里的老帖像洪水般一下就把我的reader冲爆了。

不过也好,有些老帖过了一段时间重新读也会有新体会。例如这篇,[url=https://blogs.oracle.com/jrockit/entry/why_wont_jrockit_find_my_class]Why won't JRockit find my classes[/url]

原帖里提到这样一种情况。假如在一个路径“foo”里有下面的Foo类对应的Foo.class文件:
[quote="Mattis Castergren"]
public class Foo {
public Foo () {
System.out.println("Foo created");
}
}
[/quote]
然后在“当前目录”下有下面的ClasspathTest类对应的Classpath.class文件:
[quote="Mattis Castergren"]
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();
}
}
[/quote]

那么用JRockit直接在当前目录运行ClasspathTest会遇到NoClassDefFoundError:
D:\test\test_hotspot_compilers\test_c2_Xcomp_classpath>C:\sdk\Java\jrmc-4.0.0-1.6.0\bin\java ClasspathTest
Exception 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 ClasspathTest
Foo created


原文说,造成这个差异的原因是HotSpot VM有解释器,执行一个方法并不需要事先将该方法中所有类都加载,而可以一点点执行,到方法中间碰到某个未加载的类的时候再去加载它。例子中的代码做了件很tricky的事情:改变了系统类加载器可访问到的路径。等到HotSpot VM的解释器尝试用系统类加载器去加载Foo的时候,Foo.class已经在它能找到的路径上了。

而JRockit则不同,没有解释器,所有Java方法都必须先JIT编译了才可以开始执行。其中有个实现细节,就是JRockit要求在编译某个方法的时候就保证其中涉及的类型都加载好了。但是,上面的例子里,当JRockit的JIT编译器尝试用系统类加载器去加载Foo的时候,该类加载器还没得到“foo”这个路径,所以Foo类就加载不到了。
当然并不是说只有JIT编译器而没有解释器的设计就一定会导致这种结果,只是JRockit选择了这样实现而已。

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

那么HotSpot VM里能否制造出同样的情况呢?我们知道,HotSpot VM可以通过[b]-Xcomp[/b],或者诸如[b]-XX:+UseCompiler -XX:-UseInterpreter -XX:-BackgroundCompilation[/b]这样的参数强行指定(尽量)不使用解释器而(尽量)只用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 ClasspathTest
Foo created

嗯?没报错。

那有没有可能是HotSpot VM实际上没编译ClasspathTest.main()方法呢?用[b]-XX:+PrintCompilation[/b]来看:
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()方法生成了怎样的代码呢?用[b]-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly[/b]来看:(用法参考[url=http://hllvm.group.iteye.com/group/topic/21769]以前一帖[/url])
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”地址到底是什么东西,[b]-XX:+UnlockDiagnosticVMOptions -XX:+PrintStubCode[/b]
java -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,然后用[b]-XX:+PrintOptoAssembly[/b]来看看:
java -server -Xcomp -XX:+PrintOptoAssembly ClasspathTest

000   B1: #	N1 <- BLOCK HEAD IS JUNK   Freq: 1
000 # stack bang
PUSHL EBP
SUB ESP,8 # Create frame
00e MOV ECX,#10
013 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 ; ShouldNotReachHere
018

代码里嵌着的注释说得很明白了,跳进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程序搞不好就得撞墙了 :lol:

HotSpot的client编译器在遇到编译时仍未resolve的符号的话,会在使用这些符号的位置生成一个call thunk,调用一个用于符号解析的thunk,解析好之后把直接引用patch回到原本的call thunk上。这样不用只为解决符号引用的resolution就掉回到解释器里,但生成的代码质量也会稍微差一些。

P.S. 顺带一提:HotSpot的server编译器并不会在接受某个方法的编译请求时强制要求里面涉及的所有类型已经被加载,只要求在方法签名里出现的类型在编译前已经被加载,并要求在编译前该方法里涉及的所有的字符串常量已经resolve完(具体执行这些约束的时负责分派编译请求的CompilerBroker而不是server编译器自身,见回复里的讨论)。
所以这个例子就算给了-classpath ".;foo"也会得到一样的、只含一个uncommon trap的main()方法。

P.P.S. 顺带找个地方记下ciTypeFlow的作用:
[url]http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2008-May.txt[/url]
[quote]From John.Rose at Sun.COM Fri May 16 11:23:22 2008
From: John.Rose at Sun.COM (John Rose)
Date: Fri, 16 May 2008 11:23:22 -0700
Subject: Failing typeflow responsibility assertions
In-Reply-To: <20080516084237.GA3704@redhat.com>
References: <20080516084237.GA3704@redhat.com>
Message-ID: <200E1632-3850-445C-B32E-7A0F6A497BAC@sun.com>

On May 16, 2008, at 1:42 AM, Gary Benson wrote:

> Do I have to do something to tell the CompileBroker not
> to feed me methods full of unloaded stuff? Oh, and I'm using -Xmixed.

The typeflow pass builds the basic block model and typestates
consumed by the parser. You can't build phis correctly without that
stuff.

(An earlier version tried to parse, find blocks, and compute phi
types all on the fly, but it didn't work out, so ciTypeFlow was added
as a first pass.)

(The new typestate maps might change this a little, but we have to
compile w/o the maps anyway.)

The basic block model is pruned at unreached bytecodes which would
provoke class loading.

As a result, the parser can (mostly) ignore unloaded classes, which
removes lots of buggy edge cases.

Do you use ciTypeFlow in your JIT?

-- John


From John.Rose at Sun.COM Tue May 20 10:59:04 2008
From: John.Rose at Sun.COM (John Rose)
Date: Tue, 20 May 2008 10:59:04 -0700
Subject: Failing typeflow responsibility assertions
In-Reply-To: <20080520074445.GA3861@redhat.com>
References: <20080516084237.GA3704@redhat.com>
<200E1632-3850-445C-B32E-7A0F6A497BAC@sun.com>
<20080520074445.GA3861@redhat.com>
Message-ID: <E0C35D5A-DA47-48AD-BA2D-EFC6F6F4A720@sun.com>

On May 20, 2008, at 12:44 AM, Gary Benson wrote:

> That's awesome! I wasn't using it until yesterday but I am now :)

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[/quote]
[quote]From John.Rose at Sun.COM Thu May 22 11:02:35 2008
From: John.Rose at Sun.COM (John Rose)
Date: Thu, 22 May 2008 11:02:35 -0700
Subject: Failing typeflow responsibility assertions
In-Reply-To: <20080522091725.GB3794@redhat.com>
References: <20080516084237.GA3704@redhat.com>
<200E1632-3850-445C-B32E-7A0F6A497BAC@sun.com>
<20080520074445.GA3861@redhat.com>
<E0C35D5A-DA47-48AD-BA2D-EFC6F6F4A720@sun.com>
<20080522091725.GB3794@redhat.com>
Message-ID: <3950F6EA-B65B-485F-B336-D64F048B681E@sun.com>

On May 22, 2008, at 2:17 AM, Gary Benson wrote:

> So the 'assert(will_link, "typeflow responsibility")' bits aren't
> being hit because the typeflow pass bails out? (ie env()->failing()
> returns true?) Will the compile broker retry the compilation later?

No, typeflow ends such a block with a "trap". (If we bailed out on
every unloaded class, we'd almost never compile anything.) A trap is
like a branch to the interpreter (deopt). The parser is responsible
for respecting those trap markings. A basic block that ends in a
trap never gets (in compiled code) to the branch or return that ends
the basic block in the bytecodes.

-- John[/quote]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值