感谢楼主把讨论串从ItEye的站内信迁移到HLLVM群组的论坛来
我觉得有必要理清比较抽象的工作流程再去回答您问的问题,
HotSpot VM是解释执行与编译混合模式的执行引擎。为了实现这点,Java方法的元数据对象methodOop里记录这两个入口地址,一个是from_interpreted_entry(),用于由解释模式的代码跳入该methodOop所代表的Java方法;另一个是from_compiled_entry(),用于由JIT编译后的Java方法跳入该方法。
几个相关的字段是这样的:
class methodOopDesc : public oopDesc {
// Entry point for calling both from and to the interpreter.
address _i2i_entry; // All-args-on-stack calling convention
// Adapter blob (i2c/c2i) for this methodOop. Set once when method is linked.
AdapterHandlerEntry* _adapter;
// Entry point for calling from compiled code, to compiled code if it exists
// or else the interpreter.
volatile address _from_compiled_entry; // Cache of: _code ? _code->entry_point() : _adapter->c2i_entry()
// The entry point for calling both from and to compiled code is
// "_code->entry_point()". Because of tiered compilation and de-opt, this
// field can come and go. It can transition from NULL to not-null at any
// time (whenever a compile completes). It can transition from not-null to
// NULL only at safepoints (because of a de-opt).
nmethod* volatile _code; // Points to the corresponding piece of native code
volatile address _from_interpreted_entry; // Cache of _code ? _adapter->i2c_entry() : _i2i_entry
};
这些字段都是什么时候初始化的呢?
在类加载的“初始化”阶段,可以有这样的调用路径:
instanceKlass::initialize()
-> instanceKlass::initialize_impl()
-> instanceKlass::link_class()
-> instanceKlass::link_class_impl()
-> instanceKlass::rewrite_class()
-> Rewriter::rewrite()
-> Rewriter::Rewriter()
-> methodOopDesc::link_method()
一个Java类里的所有方法都会在此时link上。这里就会初始化Java方法的解释模式和编译模式入口:
// Called when the method_holder is getting linked. Setup entrypoints so the method
// is ready to be called from interpreter, compiler, and vtables.
void methodOopDesc::link_method(methodHandle h_method, TRAPS) {
// If the code cache is full, we may reenter this function for the
// leftover methods that weren't linked.
if (_i2i_entry != NULL) return;
assert(_adapter == NULL, "init'd to NULL" );
assert( _code == NULL, "nothing compiled yet" );
// Setup interpreter entrypoint
assert(this == h_method(), "wrong h_method()" );
address entry = Interpreter::entry_for_method(h_method);
assert(entry != NULL, "interpreter entry must be non-null");
// Sets both _i2i_entry and _from_interpreted_entry
set_interpreter_entry(entry);
if (is_native() && !is_method_handle_intrinsic()) {
set_native_function(
SharedRuntime::native_method_throw_unsatisfied_link_error_entry(),
!native_bind_event_is_interesting);
}
// Setup compiler entrypoint. This is made eagerly, so we do not need
// special handling of vtables. An alternative is to make adapters more
// lazily by calling make_adapter() from from_compiled_entry() for the
// normal calls. For vtable calls life gets more complicated. When a
// call-site goes mega-morphic we need adapters in all methods which can be
// called from the vtable. We need adapters on such methods that get loaded
// later. Ditto for mega-morphic itable calls. If this proves to be a
// problem we'll make these lazily later.
(void) make_adapters(h_method, CHECK);
// ONLY USE the h_method now as make_adapter may have blocked
}
_i2i_entry 与 _from_interpreted_entry:
从methodOopDesc::link_method()调用methodOopDesc:: set_interpreter_entry()
void set_interpreter_entry(address entry) { _i2i_entry = entry; _from_interpreted_entry = entry; }
_adapter 与 _from_compiled_entry:
从methodOopDesc::link_method()调用methodOopDesc::make_adapters()
address methodOopDesc::make_adapters(methodHandle mh, TRAPS) {
// Adapters for compiled code are made eagerly here. They are fairly
// small (generally < 100 bytes) and quick to make (and cached and shared)
// so making them eagerly shouldn't be too expensive.
AdapterHandlerEntry* adapter = AdapterHandlerLibrary::get_adapter(mh);
if (adapter == NULL ) {
THROW_MSG_NULL(vmSymbols::java_lang_VirtualMachineError(), "out of space in CodeCache for adapters");
}
mh->set_adapter_entry(adapter);
mh->_from_compiled_entry = adapter->get_c2i_entry();
return adapter->get_c2i_entry();
}
这个初始化反映了HotSpot VM的混合模式执行引擎默认以解释模式启动。
可以看到:
_i2i_entry 指向该方法的解释器入口。这个值设定好就不会变了。
_from_interpreted_entry 初始的值与 _i2i_entry 一样。但后面当该Java方法被JIT编译并“安装”之后,_from_interpreted_entry 就会被设置为指向 i2c adapter stub。而如果因为某些原因需要抛弃掉之前已经编译并安装好的机器码,则 _from_interpreted_entry 会被恢复为 _i2i_entry。
_adapter 指向该Java方法的签名(signature)所对应的 i2c2i adapter stub。其实是一个 i2c stub 和一个 c2i stub 粘在一起这样的对象,可以看到用的时候都是从 _adapter 取 get_i2c_entry() 或 get_c2i_entry()。这些adapter stub用于在HotSpot VM里的解释模式与编译模式的代码之间适配其
calling convention。HotSpot VM里的解释模式calling convention用栈来传递参数,而编译模式的calling convention更多采用寄存器来传递参数,两者不兼容,因而从解释模式的代码调用已经被编译的方法,或者反之,都需要在调用时进行适配。
_from_compiled_entry 初始值指向c2i adapter stub。原因上面已经说了,因为一开始该方法尚未被JIT编译,需要在解释模式执行,那么从已经JIT编译好的Java方法调用过来的话就需要进行calling convention的转换,把参数挪到正确的位置上。当该方法被JIT编译并“安装”完之后,_from_compiled_entry 就会指向编译出来的机器码的入口,具体说时指向verified entry point。如果要抛弃之前编译好的机器码,那么 _from_compiled_entry 会恢复为指向 c2i stub。
_code 这个字段指向含有JIT编译后的机器码。初始值为NULL,意味着该方法尚未被JIT编译(或者说至少尚未被“标准编译”;OSR编译的入口不在这里)。当一个方法被JIT编译并“安装”后,_code 就会指向编译生成的 nmethod 。而要抛弃编译好的代码时 _code 会恢复为 NULL。
JIT编译的产物包装在nmethod对象里。编译完成后“安装”的逻辑在:
// Install compiled code. Instantly it can execute.
void methodOopDesc::set_code(methodHandle mh, nmethod *code) {
assert( code, "use clear_code to remove code" );
assert( mh->check_code(), "" );
guarantee(mh->adapter() != NULL, "Adapter blob must already exist!");
// These writes must happen in this order, because the interpreter will
// directly jump to from_interpreted_entry which jumps to an i2c adapter
// which jumps to _from_compiled_entry.
mh->_code = code; // Assign before allowing compiled code to exec
int comp_level = code->comp_level();
// In theory there could be a race here. In practice it is unlikely
// and not worth worrying about.
if (comp_level > mh->highest_comp_level()) {
mh->set_highest_comp_level(comp_level);
}
OrderAccess::storestore();
mh->_from_compiled_entry = code->verified_entry_point();
OrderAccess::storestore();
// Instantly compiled code can execute.
if (!mh->is_method_handle_intrinsic())
mh->_from_interpreted_entry = mh->get_i2c_entry();
}
所谓“安装”其实就是把nmethod与其对应的methodOop关联起来,把各入口都设置上。
而抛弃已编译好的机器码时与“安装”相反的“卸载”逻辑在:
// Revert to using the interpreter and clear out the nmethod
void methodOopDesc::clear_code() {
// this may be NULL if c2i adapters have not been made yet
// Only should happen at allocate time.
if (_adapter == NULL) {
_from_compiled_entry = NULL;
} else {
_from_compiled_entry = _adapter->get_c2i_entry();
}
OrderAccess::storestore();
_from_interpreted_entry = _i2i_entry;
OrderAccess::storestore();
_code = NULL;
}
在HotSpot VM的实现里,除了CompileTheWorld(CTW)这个用于测试动态编译器的特殊模式之外,一个类被加载进来之后,里面的方法最早最早也要到它即将第一次被执行的时候才有可能被JIT编译器所编译。在那之前,非abstract非native的Java方法的“代码”部分都只是好好的以Java字节码的形式存在,没有对应的机器码(上面所说的 _code 字段为NULL)。
启用-Xcomp模式的时候,UseInterpreter被设置为false,于是所有Java方法都会被认为是“首次执行前就必须被编译”的:
// Returns true if m must be compiled before executing it
// This is intended to force compiles for methods (usually for
// debugging) that would otherwise be interpreted for some reason.
bool CompilationPolicy::must_be_compiled(methodHandle m, int comp_level) {
if (m->has_compiled_code()) return false; // already compiled
if (!can_be_compiled(m, comp_level)) return false;
return !UseInterpreter || // must compile all methods
(UseCompiler && AlwaysCompileLoopMethods && m->has_loops() && CompileBroker::should_compile_new_jobs()); // eagerly compile loop methods
}
有两个地方会受-Xcomp的影响。
一是当解释器对某个方法做resolution的时候(通常发生在该方法即将被调用时):
void CallInfo::set_common(KlassHandle resolved_klass, KlassHandle selected_klass, methodHandle resolved_method, methodHandle selected_method, int vtable_index, TRAPS) {
// ...
if (CompilationPolicy::must_be_compiled(selected_method)) {
// This path is unusual, mostly used by the '-Xcomp' stress test mode.
// ...
CompileBroker::compile_method(selected_method, InvocationEntryBci,
CompilationPolicy::policy()->initial_compile_level(),
methodHandle(), 0, "must_be_compiled", CHECK);
}
}
另一个是外部代码通过JNI的invocation API来调用Java方法时:
void JavaCalls::call_helper(JavaValue* result, methodHandle* m, JavaCallArguments* args, TRAPS) {
methodHandle method = *m;
// ...
if (CompilationPolicy::must_be_compiled(method)) {
CompileBroker::compile_method(method, InvocationEntryBci,
CompilationPolicy::policy()->initial_compile_level(),
methodHandle(), 0, "must_be_compiled", CHECK);
}
// ...
}
这样就保证某个Java方法在首次被调用的那个瞬间会先被JIT编译,然后再跳进目标方法去执行。
JIT编译的产物时method对象,里面包含一些描述信息(元数据),以及编译生成的机器码本身。每个method有两个实际入口,一个是unverified entry point(UEP),用于实现虚方法分派的monomorphic inline cache;另一个是verified entry point(VEP),是方法的真正入口。只有需要虚方法分派的方法才会有独立的UEP;对静态方法、私有成员方法之类的Java方法,UEP与VEP实际上在同一个位置。关于UEP与VEP的更详细介绍,请参考HotSpot的wiki:
https://wiki.openjdk.java.net/display/HotSpot/VirtualCalls (不过这个wiki页面当前把UEP与VEP弄反了,我稍后会去修正)
================================================
之前也提到JNI invocation API的实现总是从解释器的一侧进入。原因是解释器的calling convention比较简单,传参数都在栈上,这样从native code把参数转到解释器所要求的位置上就比较简单,逻辑不太受参数个数的影响。反正通过methodOop的 from_interpreted_entry 也能正确的进入Java方法已被编译的版本,这样实现就比较方便一些。
================================================
具体到Java层的main()方法在-Xcomp模式下的执行。假设我们的Main-Class名为JavaMainClass,下面为了区分java launcher里C的main()与Java层程序里的main(),把后者写作JavaMainClass.main()。
从刚进入C的main()开始:
primordial thread:
main()
-> //... 做一些参数检查
-> //... 开启新线程作为main线程,让它从JavaMain()开始执行;该线程等待main线程执行结束
main thread:
JavaMain()
-> //... 找到指定的JVM
-> //... 加载并初始化JVM
-> //... 根据Main-Class指定的类名加载JavaMainClass
-> //... 在JavaMainClass类里找到名为"main",签名为"([Ljava/lang/String;)V",修饰符是public的静态方法
-> (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); // 通过JNI调用JavaMainClass.main()
// 以上步骤都还在java launcher的控制下;当控制权转移到JavaMainClass.main()之后就没java launcher什么事了,等JavaMainClass.main()返回之后java launcher才接手过来清理和关闭JVM。
// 下面就都在HotSpot VM里了。
-> jni_CallStaticVoidMethod() // HotSpot VM里对JNI的CallStaticVoidMethod的实现。留意要传给Java方法的参数以C的可变长度参数(…)传入,这个函数将其收集打包为JNI_ArgumentPusherVaArg对象
-> jni_invoke_static() // 这里进一步将要传给Java的参数转换为JavaCallArguments对象传下去
-> JavaCalls::call() // 真正底层实现的开始。这个方法只是层皮,把JavaCalls::call_helper()用os::os_exception_wrapper()包装起来,目的是设置HotSpot VM的C++层面的异常处理
-> JavaCalls::call_helper()
-> //... 检查目标方法是否为空方法,是的话直接返回
-> //... 检查目标方法是否“首次执行前就必须被编译”,是的话调用JIT编译器去编译目标方法
-> //... 获取目标方法的解释模式入口from_interpreted_entry,下面将其称为entry_point
-> //... 确保Java栈溢出检查机制正确启动
-> //... 创建一个JavaCallWrapper,用于管理JNIHandleBlock的分配与释放,以及在调用Java方法前后保存和恢复Java的frame pointer/stack pointer
-> StubRoutines::call_stub()( ... ) //... StubRoutines::call_stub()返回一个指向call stub的函数指针,紧接着调用这个call stub,传入前面获取的entry_point和要传给Java方法的参数等信息
// call stub是在VM初始化时生成的。对应的代码在StubGenerator::generate_call_stub()。它的功能可以参考代码前面的注释。
-> //... 把相关寄存器的状态调整到解释器所需的状态
-> //... 把要传给Java方法的参数从JavaCallArguments对象解包展开到解释模式calling convention所要求的位置
-> //... 跳转到前面传入的entry_point,也就是目标方法的from_interpreted_entry
-> //... 在-Xcomp模式下,实际跳入的是i2c adapter stub,将解释模式calling convention传入的参数挪到编译模式calling convention所要求的位置
-> //... 跳转到目标方法被JIT编译后的代码里,也就是跳到 nmethod 的 VEP 所指向的位置
-> //... 正式开始执行目标方法被JIT编译好的代码 <- 这里大概就是楼主想要的“main()方法的真正入口”
================================================
回到楼主的问题:
gaolingep 写道
【NO1】这个link没有过多注释,看代码似乎就是把class字节码编译成asm bytecode,不知道我理解得对不对
不对。JavaCallWrapper做的事情非常简单,注释里也说清楚了:
// A JavaCallWrapper is constructed before each JavaCall and destructed after the call.
// Its purpose is to allocate/deallocate a new handle block and to save/restore the last
// Java fp/sp. A pointer to the JavaCallWrapper is stored on the stack.
把Java字节码编译为机器码的工作之前就在调用CompileBroker::compile_method()的时候做完了。
另外没有“asm bytecode”这种东西。机器码就是机器码,一般不将其称为bytecode(能只用一个字节放opcode的机器这年头也不多了⋯)。
gaolingep 写道
【NO2】然后就执行 StubRoutines::call_stub()(这个代码,就跳转到前面提到的"不知道干嘛的"asm,还是没捉住入口。
“不知道干嘛”做的事情主要就是把要传给Java的参数从传入的JavaCallArguments对象里解开,放到解释器预期的位置上。
解开完了之后会用一个call指令(在x86/x64上)跳转到之前传入的entry_point,这样就进入目标方法了。
gaolingep 写道
【NO3】我认为入口应该形如
0x0283d250: mov "11111111",%edx
0x0283d25b: call 0x027fd3c0 ; printf
0x0283d250: mov "22222222",%edx
0x0283d25b: call 0x027fd3c0 ; printf
这样的代码,但是眼睛看疼了也没找到
其实这事情非常简单。您想看到JavaMainClass.main()在JIT编译后生成的机器码在哪里,内容是怎样的,只要打开-XX:+PrintAssembly来启动即可。启用该参数需要hsdis插件,请在HLLVM群组里搜一下,已经有很多人问过了。
假设要运行的代码如下:
在我的Mac OS X上用Oracle JDK 1.7.0_05来运行
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp JavaMainClass
得到的输出可以看到
Decoding compiled method 0x000000010a54cb10:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} 'main' '([Ljava/lang/String;)V' in 'JavaMainClass'
# parm0: rsi:rsi = '[Ljava/lang/String;'
# [sp+0x20] (sp of caller)
0x000000010a54cc40: mov %eax,-0x14000(%rsp)
0x000000010a54cc47: push %rbp
0x000000010a54cc48: sub $0x10,%rsp ;*synchronization entry
; - JavaMainClass::main@-1 (line 3)
0x000000010a54cc4c: mov $0x11,%esi
0x000000010a54cc51: xchg %ax,%ax
0x000000010a54cc53: callq 0x000000010a50c020 ; OopMap{off=24}
;*getstatic out
; - JavaMainClass::main@0 (line 3)
; {runtime_call}
0x000000010a54cc58: callq 0x0000000109caf432 ;*getstatic out
; - JavaMainClass::main@0 (line 3)
; {runtime_call}
0x000000010a54cc5d: hlt
0x000000010a54cc5e: hlt
0x000000010a54cc5f: hlt
[Exception Handler]
[Stub Code]
0x000000010a54cc60: jmpq 0x000000010a5308a0 ; {no_reloc}
[Deopt Handler Code]
0x000000010a54cc65: callq 0x000000010a54cc6a
0x000000010a54cc6a: subq $0x5,(%rsp)
0x000000010a54cc6f: jmpq 0x000000010a50bc00 ; {runtime_call}
0x000000010a54cc74: hlt
0x000000010a54cc75: hlt
0x000000010a54cc76: hlt
0x000000010a54cc77: hlt
这个0x000000010a54cc40地址就是-Xcomp模式下JavaMainClass.main()货真价实的编译后入口。
在调试的时候,您可以等-XX:+PrintAssembly功能打印出JavaMainClass.main()的内容之后,再去看methodOop里各入口的状态,留意_from_compiled_entry的值是否指向这里的起始地址。如果不是的话说明您查看methodOop内容的方式不太对⋯
但编译出来的代码看起来怪怪的对吧,System.out.println()跑哪儿去了?
这就请参考我之前写的一帖:
http://rednaxelafx.iteye.com/blog/1038324,说的就是这个问题。
您原本所想像的
0x0283d250: mov "11111111",%edx
0x0283d25b: call 0x027fd3c0 ; printf
0x0283d250: mov "22222222",%edx
0x0283d25b: call 0x027fd3c0 ; printf
即便在没碰到上面那帖说的问题时也跟HotSpot VM实际会编译出来的代码不太一样。println()方法会被内联到JavaMainClass.main()里,于是最终生成的JavaMainClass.main()的机器码会比您想像的要更复杂一些。
就算通过配置文件来禁用方法内联,编译出来的机器码也会时这个样子的:
Decoding compiled method 0x0000000109b5e410:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} 'main_' '([Ljava/lang/String;)V' in 'JavaMainClass'
# parm0: rsi:rsi = '[Ljava/lang/String;'
# [sp+0x20] (sp of caller)
0x0000000109b5e560: mov %eax,-0x14000(%rsp)
0x0000000109b5e567: push %rbp
0x0000000109b5e568: sub $0x10,%rsp ;*synchronization entry
; - JavaMainClass::main_@-1 (line 10)
0x0000000109b5e56c: movabs $0x7e4cb0d08,%rbp ; {oop(a 'java/lang/Class' = 'java/lang/System')}
0x0000000109b5e576: mov 0x74(%rbp),%r11d ;*getstatic out
; - JavaMainClass::main_@0 (line 10)
0x0000000109b5e57a: test %r11d,%r11d
0x0000000109b5e57d: je 0x0000000109b5e5bc ;*invokevirtual println
; - JavaMainClass::main_@5 (line 10)
0x0000000109b5e57f: lea (%r12,%r11,8),%rsi ;*getstatic out
; - JavaMainClass::main_@0 (line 10)
0x0000000109b5e583: movabs $0x7e4dc7118,%rdx ; {oop("111111111111")}
0x0000000109b5e58d: xchg %ax,%ax
0x0000000109b5e58f: callq 0x0000000109a5dc60 ; OopMap{rbp=Oop off=52}
;*invokevirtual println
; - JavaMainClass::main_@5 (line 10)
; {optimized virtual_call}
0x0000000109b5e594: mov 0x74(%rbp),%r10d ;*getstatic out
; - JavaMainClass::main_@8 (line 11)
0x0000000109b5e598: test %r10d,%r10d
0x0000000109b5e59b: je 0x0000000109b5e5cd ;*invokevirtual println
; - JavaMainClass::main_@13 (line 11)
0x0000000109b5e59d: lea (%r12,%r10,8),%rsi ;*getstatic out
; - JavaMainClass::main_@8 (line 11)
0x0000000109b5e5a1: movabs $0x7e4dc7440,%rdx ; {oop("222222222222")}
0x0000000109b5e5ab: callq 0x0000000109a5dc60 ; OopMap{off=80}
;*invokevirtual println
; - JavaMainClass::main_@13 (line 11)
; {optimized virtual_call}
0x0000000109b5e5b0: add $0x10,%rsp
0x0000000109b5e5b4: pop %rbp
0x0000000109b5e5b5: test %eax,-0xe9d5bb(%rip) # 0x0000000108cc1000
; {poll_return}
0x0000000109b5e5bb: retq
还是比您想像的要复杂那么一点。
gaolingep 写道
【NO4】上一次回复中,你提到“如果目标方法当前没有JIT编译好的版本”、“如果目标方法已经被JIT编译好,并且编译好的代码已经完成安装”
这些我不知道是怎么判断的,代码在哪里
HotSpot VM判断目标方法有没有JIT编译好的逻辑很简单,就是看methodOop里的 _code 字段是否非NULL。如果不是NULL说明已经有JIT编译好的代码而且安装好了,如果是NULL说明还没JIT编译好或者是还没安装好。
只考虑方法调用的时候发生的事,其实不需要显式判断目标方法(被调用方)有没有被编译及安装,只要知道自己(调用方)是解释模式还是编译模式的,据此选择 _from_interpreted_entry 或 _from_compiled_entry 进入就好了,这两个入口本身就暗含着目标方法是解释模式还是编译模式的正确处理逻辑。
gaolingep 写道
【NO5】"如果只是在jni_invoke_static()的入口的地方设断点来看,那个时候main()都还没被编译"
从前面讲的整体工作流程可以看出,jni_invoke_static()在一开始的地方把传入的methodID解析为具体的methodOop,然后用一个methodHandle包住它。但在这个时候JavaMainClass.main()方法尚未被JIT编译,它对应的methodOop里的各入口都还在解释模式的初始状态,如果在这个地方下断点去观察那个methodOop的内容,恐怕与您预期的内容会不一样。
如果要观察JavaMainClass.main()已经被JIT编译好的时候其methodOop的状态,一个可下断点的地方是在JavaCalls::call_helper()里从CompileBroker::compile_method()返回之后。
(注意这里的讨论全部都基于-Xcomp模式这个前提条件。不然的话JavaMainClass.main()就应该正常的从解释器进入)