1. Jdk目录下的入口函数

Main函数在/openjdk/jdk/src/share/bin/main.c里面,通过设置宏定义还可以编译出两个可执行文件来。乍看应该就是java.exe和javaw.exe的入口代码了。

入口函数没干什么有意义的事儿,直接调用了同目录下java.c里面的JLI_Launch函数,传参比较多,除了argc、argv还有cp、版本,其中的program name和launcher name让我很纳闷。也许javac.exe也是从这儿触发的。

JLI_Launch里面会先设置一下执行参数,譬如控制台打印级别之类的;然后在SelectVersion中获取jre的信息,里面有个ExecJRE方法,看注释应该是先小启动一把,以确定执行的jre是预想中的版本;之后的CreateExecutionEnvironment做的应该也是类似的工作;然后加载虚拟机,就是加载dll,此时会获取里面的JNI_CreateJavaVM和JNI_GetDefaultJavaVMInitArgs两个函数,期间会先加载windows的运行时环境(如果是windows的话);之后会处理虚拟机启动参数(可执行文件名称会被舍去),具体解析过程是在ParseArguments中进行的;最后调用ContinueInNewThread函数(最终调用的是/openjdk/jdk/src/windows/bin/java_md.c里面的ContinueInNewThread0函数,当然是windows系统下)启动一个线程执行,等待结束。JavaMain在/openjdk/jdk/src/share/bin/java.c中,干的第一件事儿就是创建虚拟机,其实就是加载jvm.dll里面的其他几个函数,后续的工作应该都是调用jvm里面的函数来实现的;然后检查是不是只需要打印版本信息就可以了(譬如加了个-v的参数),是的话就打印版本后退出,-h和参数错误也在这儿处理了;后面有个FreeKnownVMs,应该是把空闲的VM释放掉;然后就是加载主类并获取其main方法,并执行了。后面的细节就要到hotspot里面看了。

2. 编译器及其他相关的工具

编译器javac的入口代码在/openjdk/langtools/src/share/classes/com/sun/tools/javac/Main.java,的确是用java写的。Tools目录下还有javah、javadoc、javap的代码。

3. JavaVM

Java虚拟机的dll里面暴露出来的函数都是在/openjdk/hotspot/src/share/vm/prims/jni.c里面定义的。写了很长时间的Java代码后,重新看C或C++代码,尤其是高手的代码,突然发现其一大Java所无法媲美的利器——宏定义。这在jni.c里面展现的淋漓尽致,宏定义满天飞,都让人感觉这都不是C风格的代码了。

 

4. 3号中断

Jni代码中处理异常时都会调用一个breakpoint的宏,实际上是嵌入汇编的3号中断,通常称为断点中断。实际验证,在普通运行环境下这个没有任何意义;只有在调试环境下才起到断点的作用。这让我想起在调试java程序的时候如果有异常抛出就会中断,不管有没有在此处打断点。也许在Java虚拟机的层面上也能实现上面的功能,不过我还是觉得用这个会更方便一些。

jni_Throw或jni_ThrowNew,单看jni_Throw,调用的是Exceptions::_throw_oop方法,这个方法又调用了_throw方法,里面也没有干什么事儿:打印堆栈,然后线程设置pending exception(里面有个判断是否特殊线程的分支,不过分过去之后处理方法没什么区别)。也许之前判断的用3号中断实现调试时异常断点有误,此处才是异常断点的实现方法。

5. 线程

jni_functions()函数提供了对JNINativeInterface_结构体的全局实例jni_NativeInterface的访问,JavaThread的initialize()函数通过调用set_jni_functions()子函数将此实例添加进了JavaThread内部的JNIEnv中。

同时注意到了Thread的current方法。它最终调用了微软提供的TlsGetValue函数获取了一个void指针,并强制转型成Thread类型。与此对应的还有TlsSetValue函数。查看微软的说明,貌似是进程给每个线程分配了一个单独的存储空间,称之为Thread Local Storage,用以上两个函数在其中存取对象,以index标示。

6. JavaVM

里面只有一个JNIInvokeInterface_ 的实例functions,而方法也都是调用functions的函数指针。 JNIInvokeInterface_ 里面有三个函数指针,分别是销毁虚拟机、绑定线程、解绑定线程和获取虚拟机环境信息相关的接口,另外有三个保留的函数指针。

static jint attach_current_thread (JavaVM *vm, void **penv, void *_args, bool

DetachCurrentThread() 调用的是jni_DetachCurrentThread (JavaVM *vm) ,过程:检查,如果虚拟机已退出则阻塞,如果 TlsGetValue 获取到的线程指针为空则已解绑定,如果还存有Java帧(应该是栈帧,还有栈帧说明有函数尚未执行完毕) 则报错;有 ThreadStateTransition :: transition_from_native ( thread, _thread_in_vm) ; 此句不解;调用JavaThread的exit函数,开头的一个状态检查不解,后面跟着的应该是对未捕获异常的处理,调用的是Java层面的代码;然后调用Java层面的Thread.exit方法;后面跟一些JVMTI接口相关处理,和处理外部挂起请求(这一段没弄懂);释放线程持有的monitor,之前申请的JNIHandleBlock也要释放掉,去掉堆栈检查,tlab也要卸掉;最后把自己从Threads的记录里面去掉,就完事儿了。

7. 关于tlab

CollectedHeap::common_mem_allocate_noinit(size_t size, TRAPS)  函数中,代码会先判断是否使用tlab,是的话优先用tlab分配;tlab分配失败的时候才使用全局的堆来分配。而在tlab分配对象时,会先看tlab当前剩余的空间是否足够,足够的话当然就直接分配了;否则,貌似是重新规划tlab,弄的更大一些,来给新对象申请空间。

8. 调用流程

jni_invoke_static

call(result, method, &java_args, CHECK) 函数,里面开头一个断言,略过,进入后面的 os::os_exception_wrapper(call_helper, result, &method, args, THREAD) 函数。里面嵌入了一些汇编,初步估计是用来切换函数调用方式的(譬如_stdcall和_fastcall之类,在调用方式上有一些区别,在C语言层面上无法区分);然后调用了f函数。好吧,回去看 call_helper

call_helper 里面开头是一些检查,是不是java环境啊、调用函数是不是空的啊,等等。后面还有一些实时编译的处理,检查当前线程是不是编译线程且要调用的方法需不需要编译,然则编译。略过,直接看调用部分,在 StubRoutines::call_stub() 处,这里返回一个函数指针,就是 StubRoutines::_call_stub_entry ,下面看这个东西使怎么来的。全工程搜索吧,在stubGenerator里面,这个有好几个版本,我看的是x86的32位版本。在其中的 void generate_initial() 方法中的第二个语句就是设置 StubRoutines::_call_stub_entry 的,调用的子函数入参是另一个 StubRoutines::_call_stub_return_address

9. BytecodeInterpreter解释器的运行部分,在bytecodeInterpreter.cpp的run函数里面。

10. 另外的一个解释器叫做模板解释器。想见识史上最牛b的代码,见templateTable.cpp里面的initialize函数。