Java JNI实现原理初探

前言

写这个是出于好奇。

我们知道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

直觉告诉我,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方法的时候,用刚才的例子中提到的方法进行调用。

评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值