前言
写这个是出于好奇。
我们知道cpu只认得 “0101101” 类似这种符号, C、C++ 这些代码最终都得通过编译、汇编成二进制代码,cpu才能识别。而Java比C、C++又多了一层虚拟机,过程也复杂许多。Java代码经过编译成class文件、虚拟机装载等步骤最终在虚拟机中执行。class文件里面就是一个结构复杂的表,而最终告诉虚拟机怎么执行的就靠里面的字节码说明。
Java虚拟机在执行的时候,可以采用解释执行和编译执行的方式执行,但最终都是转化为机器码执行。
Java虚拟机运行时的数据区,包括方法区、虚拟机栈、堆、程序计数器、本地方法栈。
问题来了,按我目前的理解,如果是解释执行,那么方法区中应该存的是字节码,那执行的时候,通过JNI 动态装载的c、c++库,放哪去?怎么执行?这个问题,搜索了许多标题写着”JNI实现原理”的文章,都是抄来抄去,并没去探究如何实现的,只是讲了java如何使用JNI。好吧,就从如何使用JNI开始。
JNI的简单实现
参考文章:《Java JNI简单实现》、《JAVA基础之理解JNI原理》
假设当前的目录结构如下:
-
| - hackooo
| Test.java
1.首先编写java文件
Test.java
package hackooo;
public class Test{
static{
System.loadLibrary("bridge");
}
public native int nativeAdd(int x,int y);
public static void main(String[] args){
Test obj = new Test();
System.out.printf("%d\n",obj.nativeAdd(2012,3));
}
}
代码很简单,这里声明了nativeAdd(int x,inty)
的方法,执行的时候简单的打出执行的结果。另外这里调用API加载名称叫bridge
的库,接下来就来实现这个库。
2.生成JNI调用需要的头文件
javac hackooo/Test.java
javah -jni hackooo.Test
现在目录结构是这样的:
-
| - hackooo
| Test.java
| Test.class
| - hackooo_Test.h
hackooo_Test.h头文件内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class hackooo_Test */
#ifndef _Included_hackooo_Test
#define _Included_hackooo_Test
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: hackooo_Test
* Method: nativeAdd
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_hackooo_Test_nativeAdd
(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
#endif
3.native方法的实现
这里新增bridge.c
文件来实现之前声明的native方法,目录结构如下:
-
| - hackooo
| Test.java
| Test.class
| - hackooo_Test.h
| - bridge.c
bridge.c的内容如下:
#include "hackooo_Test.h"
JNIEXPORT jint JNICALL Java_hackooo_Test_nativeAdd
(JNIEnv * env, jobject obj, jint x, jint y){
return x+y;
}
这里的实现只是简单的把两个参数相加,然后返回。
4.生成动态链接库
gcc -shared -I /usr/lib/jdk1.6.0_45/include
-I /usr/lib/jdk1.6.0_45/include/linux bridge.c -o libbridge.so
注意这里几个gcc的选项,-shared
是说明要生成动态库,而两个 -I
的选项,是因为我们用到<jni.h>
相关的头文件,放在<jdk>/include
和 <jdk>/include/linux
两个目录下。
最后需要注意一点的是 -o
选项,我们在java代码中调用的是System.loadLibrary("xxx")
,那么生成的动态链接库的名称就必须是libxxx.so
的形式(这里指Linux环境),否则在执行java代码的时候,就会报 java.lang.UnsatisfiedLinkError: no XXX in java.library.path
的错误!也就是说找不到这个库,我在这里被坑了一小段时间。
好了,现在的目录结构如下:
-
| - hackooo
| Test.java
| Test.class
| - hackooo_Test.h
| - bridge.c
| - libbridge.so
5.执行代码验证结果
java -Djava.library.path=. hackooo.Test
2015
ok,Java 使用JNI的最简单的例子就完成了。
JNI实现原理
那么,我们的问题还没解决,刚刚生成的动态链接库”libbridge.so”是怎么装进内存的?native方法怎么调用?跟普通的方法调用有什么区别吗?
我们把Test.java改改,增加普通的方法”int add(int x,int y)”
Test.java
package hackooo;
public class Test{
static{
System.loadLibrary("bridge");
}
public native int nativeAdd(int x,int y);
public int add(int x,int y){
return x+y;
}
public static void main(String[] args){
Test obj = new Test();
System.out.printf("%d\n",obj.nativeAdd(2012,3));
System.out.printf("%d\n",obj.add(2012,3));
}
}
我们把它编译成class文件,再看看class文件中,native方法和普通方法有何区别:
javac hackooo/Test.java
javap -verbose hackooo.Test
解析后,”nativeAdd”和”add”两个方法的结果如下:
public native int nativeAdd(int, int);
flags: ACC_PUBLIC, ACC_NATIVE
public int add(int, int);
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: ireturn
LineNumberTable:
line 8: 0
可见,普通的“add”方法是直接把字节码放到code属性表中,而native方法,与普通的方法通过一个标志“ACC_NATIVE”区分开来。java在执行普通的方法调用的时候,可以通过找方法表,再找到相应的code属性表,最终解释执行代码,那么,对于native方法,在class文件中,并没有体现native代码在哪里,只有一个“ACC_NATIVE”的标识,那么在执行的时候改怎么找到动态链接库的代码呢?
接着跟踪代码,只能从System.loadLibrary()
入手了!下面是曲折的跟踪过程:
System.loadLibrary
↓↓↓ java.lang.System.java
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
↓↓↓ java.lang.Runtime.java
synchronized void loadLibrary0(Class<?> fromClass, String libname) {
......中间省略......
ClassLoader.loadLibrary(fromClass, libname, false);
}
↓↓↓ java.lang.ClassLoader.java
// Invoked in the java.lang.Runtime class to implement load and loadLibrary.
static void loadLibrary(Class<?> fromClass, String name,
boolean isAbsolute) {
......
if (sys_paths == null) {
usr_paths = initializePath("java.library.path");
sys_paths = initializePath("sun.boot.library.path");
}
if (isAbsolute) {
if (loadLibrary0(fromClass, new File(name))) {
return;
}
......
}
if (loader != null) {
String libfilename = loader.findLibrary(name);
if (libfilename != null) {
......
if (loadLibrary0(fromClass, libfile)) {
return;
}
......
}
}
for (int i = 0 ; i < sys_paths.length ; i++) {
......
if (loadLibrary0(fromClass, libfile)) {
return;
}
......
}
for (int i = 0 ; i < usr_paths.length ; i++) {
......
if (loadLibrary0(fromClass, libfile)) {
return;
}
......
}
......
}
这段代码有点长,大概的查找顺序是:
1.如果是绝对路径,按绝对路径查找
2.让classLoader去查找
3.到sys_paths去查找
4.到usr_paths去查找
最终都是调用classLoadder.loadLibrary0()
↓↓↓ java.lang.ClassLoader.java
private static boolean loadLibrary0(Class<?> fromClass, final File file) {
......
Vector<NativeLibrary> libs =
loader != null ? loader.nativeLibraries : systemNativeLibraries;
synchronized (libs) {
int size = libs.size();
for (int i = 0; i < size; i++) {
......
}
synchronized (loadedLibraryNames) {
......
int n = nativeLibraryContext.size();
for (int i = 0; i < n; i++) {
......
}
NativeLibrary lib = new NativeLibrary(fromClass, name, isBuiltin);
nativeLibraryContext.push(lib);
try {
lib.load(name, isBuiltin);
} finally {
nativeLibraryContext.pop();
}
......
}
}
}
这里找到关键的一个类 NativeLibrary
↓↓↓ java.lang.ClassLoader.java
static class NativeLibrary {
// opaque handle to native library, used in native code.
long handle;
......
// the loader this native library belongs.
private final Class<?> fromClass;
// the canonicalized name of the native library.
// or static library name
String name;
// Indicates if the native library is linked into the VM
boolean isBuiltin;
// Indicates if the native library is loaded
boolean loaded;
native void load(String name, boolean isBuiltin);
native long find(String name);
native void unload(String name, boolean isBuiltin);
......
}
好吧,当我看到 native void load(String name, boolean isBuiltin);
的时候,脸一黑,卧槽~你tm耍我呢,我正想跟踪一下native怎么实现的,最终跟到这一步的load( )却是native实现的!不过从刚才我们实现的简单的JNI调用知道,虚拟机内部肯定也得有个函数库,里面的函数声明类似“ load(xxx…)”。
ok,下载openJDK的源码,在hotspot虚拟机源码目录下,搜一下看看有没有类似 “load( xxx…)”的函数。很不幸,我并没有找到函数声明完全符合的load函数,不知道是不是openJDK源码版本与JDK版本不一致的原因(我下载的openJDK版本是“openjdk-7u40-fcs-src-b43-26_aug_2013.zip”,而系统刚才跟踪代码是在netbeans里跟踪的,里面用到的代码是最新的java8版本,如果读者有用一致的版本进行尝试的话请留言跟踪的结果,谢谢!)不过,让我喜出望外的是,看到了这个函数:
直觉告诉我,classLoader下的这个os::dll_load就是我要找的,继续跟进:
↓↓↓ openjdk/hotspot/src/os/linux/vm/os_linux.cpp
void * os::dll_load(const char *filename, char *ebuf, int ebuflen)
{
......
bool load_attempted = false;
......
if (!load_attempted) {
result = os::Linux::dlopen_helper(filename, ebuf, ebuflen);
}
if (result != NULL) {
// Successful loading
return result;
}
......
}
↓↓↓ openjdk/hotspot/src/os/linux/vm/os_linux.cpp
void * os::Linux::dlopen_helper(const char *filename, char *ebuf, int ebuflen) {
void * result = ::dlopen(filename, RTLD_LAZY);
......
return result;
}
↓↓↓ /usr/include/dlfcn.h
/* Open the shared object FILE and map it in; return a handle that can be
passed to `dlsym' to get symbol values from it. */
extern void *dlopen (const char *__file, int __mode) __THROW;
Oh my God!原来最终使用的是系统的一个库!
于是又开始一小段学习之旅…..
dlopen、dlsym的使用
搜索dlopen的用法,这里参考两篇文章:
《dlopen 与dlsym》
《Dynamically Loaded (DL) Libraries》
简单的说,dlopen、dlsym提供一种动态转载库到内存的机制,在需要的时候,可以调用库中的方法。
让我们用一小段代码来演示一下,用C/C++怎么调用动态装载的库中的函数的。
1.首先创建我们的共享库,功能很简单,实现一个简单的加法然后返回:
hello.c
int add(int a,int b){return a+b;}
编译成共享库
gcc -shared hello.c -o libhello.so
2.写个简单的程序test.c,装载共享库,执行共享库中的add(int,int)
方法
test.c
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
//这里为了演示方便去掉错误检查相关的代码
int main(int argc,char*argv[]){
void * handle;
int (*func)(int,int);
char *error;
int a,b;
//加载libhello.so库
handle = dlopen("libhello.so",RTLD_LAZY);
func = dlsym(handle,"add");
//读取两个参数
sscanf(argv[1],"%d",&a);
sscanf(argv[2],"%d",&b);
//输出结果
printf("a + b = %d\n",(*func)(a,b));
dlclose(handle);
}
3.编译test.c,验证结果:
gcc test.c -o test -ldl
./test 2012 3
2015
ok,总结一下怎么使用这几个函数,dlopen相当于打开一个共享库,打开的时候可以使用RTLD_LAZY标志,等函数真正被调用的时候才去装载库。dlopen返回一个handle,这个句柄是使用其它的dlxxx函数用的,dlsym就是使用dlopen返回的handle,去查找相应的符号,然后返回相应的函数指针的。
如果读者回头去看看刚才分析的Java的NativeLibrary
类,会发现里面有个handle成员!
小结
Java动态装载共享库,靠的是系统的 “dlxxx” 相关的函数实现的,而JNI的实现,回到我们最开始的问题:“刚刚生成的动态链接库”libbridge.so”是怎么装进内存的?native方法怎么调用?跟普通的方法调用有什么区别吗?”,第一个和第三个问题已经解决。而第二个问题,“native方法怎么调用?”相信读者也应该有了答案,其实就跟普通的方法差不多,对于装载的共享库,java虚拟机,也会有缓存,在装载共享库的时候,会读取共享库的header,并且解析并保存里面的符号表,当调用native方法的时候,用刚才的例子中提到的方法进行调用。