点击打开链接
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虚拟机的层面上也能实现上面的功能,不过我还是觉得用这个会更方便一些。
Java异常抛出动作,应该就是jni.cpp里面的jni_Throw或jni_ThrowNew,单看jni_Throw,调用的是Exceptions::_throw_oop方法,这个方法又调用了_throw方法,里面也没有干什么事儿:打印堆栈,然后线程设置pending exception(里面有个判断是否特殊线程的分支,不过分过去之后处理方法没什么区别)。也许之前判断的用3号中断实现调试时异常断点有误,此处才是异常断点的实现方法。
5. 线程
追踪jni入口函数句柄传给java虚拟机对象的过程,到了线程那一部分,在JavaThread的初始化函数里面有相关操作。在jni.hpp中定义的JNINativeInterface_结构体里面包含几乎全部的jni入口函数,例如类加载、初始化、操作栈帧之类,以及Java层数据类型在虚拟机代码级的操作。Jni.cpp里面的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_ 里面有三个函数指针,分别是销毁虚拟机、绑定线程、解绑定线程和获取虚拟机环境信息相关的接口,另外有三个保留的函数指针。
其中的AttachCurrentThread函数,最终调用的是 static jint attach_current_thread (JavaVM *vm, void **penv, void *_args, bool daemon) 这个函数,里面的操作:先用上文提到过的 TlsGetValue 获取其中的线程指针,如果有的话,说明已经绑定,设置环境指针后返回;否则说明还没绑定(如此看来,那个 TlsSetValue 应该是在JavaThread初始化中调用的),先创建JavaThread实例,设置各种标志位,记录堆栈状态、初始化线程本地存储(此段不解),然后调用操作系统相关的一些接口获取当前线程并放入JavaThread实例中(其中调用的复制线程句柄的函数让我不解,难道这段代码还会被两个进程共享吗);之后使能堆栈溢出检查、初始化tlab,设置JNIHandBlock(应该和垃圾收集有关),然后把线程指针放到Threads里面记录起来;后面是设置线程组和名称,与Java层的Thread绑定,设置环境指针,完成后恢复线程状态,完毕。
函数 jint 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
在网上搜到的相关描述,说这个东西使sun用来优化对象申请用的,在线程本地,就不需要加锁了。实际看代码,在openjdk/hotspot/src/share/vm/gc_interface/collectedHeap.inline.hpp中的 HeapWord* CollectedHeap::common_mem_allocate_noinit(size_t size, TRAPS) 函数中,代码会先判断是否使用tlab,是的话优先用tlab分配;tlab分配失败的时候才使用全局的堆来分配。而在tlab分配对象时,会先看tlab当前剩余的空间是否足够,足够的话当然就直接分配了;否则,貌似是重新规划tlab,弄的更大一些,来给新对象申请空间。
8. 调用流程
先从jni里面的 jni_invoke_static 看吧,入参和通常的JNI调用函数差不多。其中有个 jmethodID 类型的参数,这个类型在jni.cpp里面声明是个结构体,但是具体的定义却找不到;具体在这个函数里面的使用,只是强制转型成了methodOop类型。
函数里面,先构造了一个methodHandle,方法句柄;后边的ResourceMark应该和垃圾收集相关;然后是整理入参,顺道还设置了返回类型。接下来进入 JavaCalls::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 ,先不管它,到子函数里面看看。吓一跳,整个一java级的汇编么。
9. BytecodeInterpreter解释器的运行部分,在bytecodeInterpreter.cpp的run函数里面。
10. 另外的一个解释器叫做模板解释器。想见识史上最牛b的代码,见templateTable.cpp里面的initialize函数。