一直以来,对Object类中的registerNatives()方法感到十分好奇,想知道它的作用到底是什么。但查阅了不少博客,目前还没找到全面彻底且浅显易懂地介绍该方法作用的博客。于是就有了写本文的想法。本文不会直接给出结论,而是按照探索的过程为线索,娓娓道来。
其实,细心的你可能会发现,不光是Object类,甚至System类、Class类、ClassLoader类、Unsafe类等等,都能在类代码中找到如下代码:
private static native void registerNatives();
static {
registerNatives();
}
为了搞清楚这四行代码的含义和作用,我们需要先了解什么是本地方法。本义方式地方法在Java类中的定义是用native进行修饰,且只有方法定义,没有方法实现。在《深入Java虚拟机》这本书的1.3.1节对Java方法有以下描述:
Java有两种方法:Java方法和本地方法。Java方法是由Java语言编写,编译成字节码,存储在class文件中。本地方法是由其他语言(比如C,C++,或者汇编)编写的,编译成和处理器相关的机器代码。本地方法保存在动态连接库中,格式是各个平台专有的。Java方法是平台无关的,单本地方法却不是。运行中的Java程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。本地方法是联系Java程序和底层主机操作系统的连接方法。
由此可知,本地方法的实现是由其他语言编写并保存在动态连接库中,因而在java类中不需要方法实现。registerNatives本质上就是一个本地方法,但这又是一个有别于一般本地方法的本地方法,从方法名我们可以猜测该方法应该是用来注册本地方法的。对,你猜的没错。上述代码的功能就是先定义了registerNatives()方法,然后当该类被加载的时候,调用该方法完成对该类中本地方法的注册。这里你可能会有一些疑惑,比如,到底注册了哪些方法?为什么要注册?具体又是怎么注册的?
我们首先看第一个问题:到底注册了哪些方法?细心的你可能还会发现,在Object类中,除了有registerNatives这个本地方法之外,还有hashCode()、clone()等本地方法,而在Class类中有forName0()这样的本地方法等等。也就是说,凡是包含registerNatives()本地方法的类,同时也包含了其他本地方法。所以,显然,当包含registerNatives()方法的类被加载的时候,注册的方法就是该类所包含的除了registerNatives()方法以外的所有本地方法。详见参考博文3。
接着我们来看第二个问题:为什么要注册?带着这个问题,我阅读了《The Java Native Interface Programmer’s Guide and Specification》这本书,书中8.3 Registering Native Methods节有如下几段描述:
Before an application executes a native method it goes through a two-step process to load the native library containing the native method implementation and then link to the native method implementation:
1.System.loadLibrary locates and loads the named native library. For example, System.loadLibrary("foo") may cause foo.dll to be loaded on Win32.
2.The virtual machine locates the native method implementation in one of the loaded native libraries. For example, a Foo.g native method call requires locating and linking the native function Java_Foo_g, which may reside in foo.dll.
This section will introduce another way to accomplish the second step.
Instead of relying on the virtual machine to search for the native method in the already loaded native libraries, the JNI programmer can manually link native methods by registering a function pointer with a class reference, method name, and method descriptor:
JNINativeMethod nm;
nm.name = "g";
/* method descriptor assigned to signature field */
nm.signature = "()V";
nm.fnPtr = g_impl;
(*env)->RegisterNatives(env, cls, &nm, 1);
The above code registers the native function g_impl as the implementation of the Foo.g native method:
void JNICALL g_impl(JNIEnv *env, jobject self);
The native function g_impl does not need to follow the JNI naming convention because only function pointers are involved, nor does it need to be exported from the library (thus there is no need to declare the function using JNIEXPORT). The native function g_impl must still, however, follow the JNICALL calling convention.
通过以上内容,我们了解到,一个Java程序要想调用一个本地方法,需要执行两个步骤:第一,通过System.loadLibrary()将包含本地方法实现的动态文件加载进内存;第二,当Java程序需要调用本地方法时,虚拟机在加载的动态文件中定位并链接该本地方法,从而得以执行本地方法。registerNatives()方法的作用就是取代第二步,让程序主动将本地方法链接到调用方,当Java程序需要调用本地方法时就可以直接调用,而不需要虚拟机再去定位并链接。
书中还总结了使用registerNatives()方法的三点好处:
其实,除了这三点好处,上面的红色部分还提到了第四点好处:通过registerNatives()方法,在定义本地方法实现的时候,可以不遵守JNI命名规范。那什么是JNI命名规范呢?举个例子,我们在Object中定义的本地方法registerNatives,那这个方法对应的本地方法名就叫Java_java_lang_Object_registerNatives,而在System类中定义的registerNatives方法对应的本地方法名叫Java_java_lang_System_registerNatives等等。也就是说,JNI命名规范要求本地方法名由“包名”+“方法名”构成,而上面的例子中,我们将Java中定义的方法名“g”和本地方法名“g_impl”链接了起来,这就是通过registerNatives方法的第四个好处。
第三个问题:具体怎么注册?这个问题涉及到registerNatives()的底层C++源码实现,有兴趣可以阅读参考博文3和5,建议先看5,再看3,因为5介绍了如何使用registerNatives方法注册本地方法,而3介绍了registerNatives实现源码。
到这里,阅读可以结束了。但如果你和我一样,对上面的红色英文提到的一些概念好奇的话,可以接着往下读。
首先,JNIEXPORT和JNICALL到底是什么?作用是什么?在jni_md.h头文件中有以下信息:
/*
* @(#)jni_md.h 1.14 03/12/19
*
* Copyright 2004 Sun Microsystems, Inc. All rights reserved.
* SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
#ifndef _JAVASOFT_JNI_MD_H_
#define _JAVASOFT_JNI_MD_H_
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
#define JNICALL __stdcall
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;
#endif
/* !_JAVASOFT_JNI_MD_H_
*/
原来JNIEXPORT和JNICALL分别是两个宏定义。那__stdcall又是什么呢?注意到上面有个名词:calling convention,翻译中文叫“调用规范”,参考博文7可知,调用规范是用来解决当高级语言函数被编译成机器码时,CPU没有办法知道一个函数调用需要多少个、什么样的参数的问题。
我们接着看#define JNIEXPORT __declspec(dllexport)。由博客8和9可知,这是一个声明,作用是将DLL中的函数和数据输出到其它程式中,以供其使用。
最后我们来看一下JNIEnv。由博文10可知,JNIEnv类型实际上代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。例如,创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等等。JNIEnv的指针会被JNI传入到本地方法的实现函数中来对Java端的代码进行操作。其实JNIEnv类中定义了很多函数可以用:
NewObject:创建Java类中的对象
NewString:创建Java类中的String对象
New
Get
Set
GetStatic
SetStatic
Call
CallStatic
如果想进一步研究,还可以自行查看jni.h。
参考博文:
1. 《深入Java虚拟机》
2. 《The Java Native Interface Programmer’s Guide and Specification》
3. https://hunterzhao.io/post/2018/04/06/hotspot-explore-register-natives/ 【JVM源码探秘】深入registerNatives()底层实现
4. http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/3462d04401ba/src Java源码地址
5. https://www.jianshu.com/p/f4b4b9006742 使用JNI_OnLoad动态注册函数
6. https://www.jianshu.com/p/216a41352fd8 JNI 学习笔记——通过RegisterNatives注册原生方法
7. https://www.cnblogs.com/findumars/p/5018184.html 关于调用约定的一点知识
8. https://www.cnblogs.com/foohack/p/4119207.html dllimport和dllexport作用与区别
9. https://blog.csdn.net/dongfengsun/article/details/1477797 DLL的Export和Import
10. https://blog.csdn.net/yuanzhihua126/article/details/78992068 JNI中JNIEnv类型和jobject类型的解释
11. https://www.jianshu.com/p/713a79293bf1 JNI 基础 - JNIEnv 的实现原理
12. http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/3462d04401ba/src/ OpenJDK源码链接
13. https://www.jianshu.com/p/be943b4958f4 Java Object.hashCode()返回的是对象内存地址