向JVM注册本地方法是怎么实现的

前言

Java 中我们经常会遇到要调用本地方法的情况,而且 Java 核心库中的很多类也大量使用了本地方法,使用 JNI 时本地函数需要按照约定好的格式进行命名,如果不想写长长的函数名则需要将方法注册到 JVM 中,这里看看怎么向 JVM 注册本地方法。

命名约定

JVM 中对本地方法名有约定,在使用 JNI 时需要遵守,即为Java_<fully qualified class name>_method

比如这里编写一个 Java 类提供本地加密的方法,其中加密方法为本地方法,实现是在ByteCodeEncryptor动态库,那么它本地对应的函数名为Java_com_seaboat_bytecode_ByteCodeEncryptor_encrypt

package com.seaboat.bytecode;

public class ByteCodeEncryptor {
  static{
    System.loadLibrary("ByteCodeEncryptor"); 
  }
  
  public native static byte[] encrypt(byte[] text);
  
}
复制代码

registerNatives

如果觉得本地函数的命名约定比较繁琐,那么可以使用 registerNatives 方式来注册本地函数,这样就可以随意命名函数。而且认为经过 registerNatives 往 JVM 中注册的函数在执行时会更加高效,因为函数的查找更快了。

如何注册

有两种方式可以实现本地方法注册:

1、Java 中静态块

  • 在 Java 类中声明一个registerNatives静态方法。
  • 在本地代码中定义一个Java_<fully qualified class name>_registerNatives函数。
  • 在调用其他本地函数前要先调用registerNatives方法。

比如对于 Object 类,在类中进行如下操作:

private static native void registerNatives();
static {
    registerNatives();
  }

复制代码

本地中通过registerNatives将指定的本地方法绑定到指定函数,比如这里将hashCodeclone本地方法绑定到JVM_IHashCodeJVM_IHashCode函数。

static JNINativeMethod methods[] = {
    {"hashCode",    "()I",                    (void *)&JVM_IHashCode},
    {"clone",       "()Ljava/lang/Object;",   (void *)&JVM_Clone},
};

JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls,
                            methods, sizeof(methods)/sizeof(methods[0]));
}
复制代码

2、使用JNI_OnLoad

JNI_OnLoad函数在 JVM 执行System.loadLibrary方法时被调用,所以可以在该方法中调用RegisterNatives函数注册本地函数。通过该种方式注册本地方法则无需在 Java 类中声明RegisterNatives本地方法来注册了。

static JNINativeMethod methods[] = {
    {"hashCode",    "()I",                    (void *)&JVM_IHashCode},
    {"clone",       "()Ljava/lang/Object;",   (void *)&JVM_Clone},
};

int JNI_OnLoad(JavaVM* vm, void* reserved)
{
...
if ((*env)->RegisterNatives(env, cls,
                            methods, sizeof(methods)/sizeof(methods[0])) < 0)
{
    return JNI_ERR;
}
...
}
复制代码

registerNatives干了什么

定义了 JNINativeMethod 结构体用于声明方法和函数,如下,name 表示 Java 的本地方法名,signature 表示方法的签名,fnPtr 表示函数指针。

typedef struct {
    char *name;
    char *signature;
    void *fnPtr;
} JNINativeMethod;
复制代码

主要要看(*env)->RegisterNatives这个函数,

JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls,
                            methods, sizeof(methods)/sizeof(methods[0]));
}
复制代码

该方法的声明在一个JNINativeInterface_结构体中,该结构体包含了 JNI 的所有接口函数声明,JVM 中定义了结构体变量jni_NativeInterface来使用,这里只列出RegisterNatives函数的声明,其他函数省略。

struct JNINativeInterface_ {
    ...
    jint (JNICALL *RegisterNatives) (JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
    ...
}

struct JNINativeInterface_ jni_NativeInterface = {
    ...
    jni_RegisterNatives,
    ...
}
复制代码

在看jni_RegisterNatives函数的实现前先了解JNI_ENTRYJNI_END宏,这两个宏将共同的部分都抽离出来了。其中JNI_END比较简单,就两个结束大括号。

#define JNI_ENTRY(result_type, header) JNI_ENTRY_NO_PRESERVE(result_type, header) WeakPreserveExceptionMark __wem(thread);

#define JNI_END } }
复制代码

JNI_ENTRY主要逻辑:

  • 获取当前执行线程 JavaThread 指针对象。
  • 创建 ThreadInVMfromNative 对象。
  • TRACE_CALL ,这里什么都不干。
  • 创建 HandleMarkCleaner 对象。
  • 将 thread 赋值给 Exceptions 中的 THREAD。
  • 校验栈对齐。
  • 创建 WeakPreserveExceptionMark 对象。
#define JNI_ENTRY_NO_PRESERVE(result_type, header) \
extern "C" {                                                         \
  result_type JNICALL header {                                       \
    JavaThread* thread=JavaThread::thread_from_jni_environment(env); \
    assert( !VerifyJNIEnvThread || (thread == Thread::current()), "JNIEnv is only valid in same thread"); \
    ThreadInVMfromNative __tiv(thread);                              \
    debug_only(VMNativeEntryWrapper __vew;)                          \
    VM_ENTRY_BASE(result_type, header, thread)
    
#define VM_ENTRY_BASE(result_type, header, thread) \
  TRACE_CALL(result_type, header)                                    \
  HandleMarkCleaner __hm(thread);                                    \
  Thread* THREAD = thread;                                           \
  os::verify_stack_alignment();      
复制代码

现在看jni_RegisterNatives函数具体的实现,逻辑为:

  • JNIWrapper 用于 debug。
  • HOTSPOT_JNI_REGISTERNATIVES_ENTRY 和 DT_RETURN_MARK 都用于 dtrace。
  • 创建 KlassHandle 对象。
  • 开始遍历方法数组,获取对应的方法名、方法签名和方法长度等信息。
  • 尝试在符号常量池中查找是否已经存在对应的方法名和方法签名,如果找不到则要抛异常,因为正常情况加载 Java 类时已经添加到常量池中了。
  • 调用register_native函数注册。
JNI_ENTRY(jint, jni_RegisterNatives(JNIEnv *env, jclass clazz,
                                    const JNINativeMethod *methods,
                                    jint nMethods))
  JNIWrapper("RegisterNatives");
  HOTSPOT_JNI_REGISTERNATIVES_ENTRY(env, clazz, (void *) methods, nMethods);
  jint ret = 0;
  DT_RETURN_MARK(RegisterNatives, jint, (const jint&)ret);

  KlassHandle h_k(thread, java_lang_Class::as_Klass(JNIHandles::resolve_non_null(clazz)));

  for (int index = 0; index < nMethods; index++) {
    const char* meth_name = methods[index].name;
    const char* meth_sig = methods[index].signature;
    int meth_name_len = (int)strlen(meth_name);

    TempNewSymbol  name = SymbolTable::probe(meth_name, meth_name_len);
    TempNewSymbol  signature = SymbolTable::probe(meth_sig, (int)strlen(meth_sig));

    if (name == NULL || signature == NULL) {
      ResourceMark rm;
      stringStream st;
      st.print("Method %s.%s%s not found", h_k()->external_name(), meth_name, meth_sig);
      THROW_MSG_(vmSymbols::java_lang_NoSuchMethodError(), st.as_string(), -1);
    }

    bool res = register_native(h_k, name, signature,
                               (address) methods[index].fnPtr, THREAD);
    if (!res) {
      ret = -1;
      break;
    }
  }
  return ret;
JNI_END
复制代码

register_native函数逻辑如下:

  • 到对应 Klass 对象中查找指定方法,如果不存在则抛异常。
  • 方法如果不是声明为 native,则先尝试查找被添加了前缀的本地方法,这个是因为可能在JVM TI agent 中设置某些 native 方法的前缀,如果还是为空则最终抛出异常。
  • 调用最重要的set_native_function函数,将 C++ 的函数绑定到该 Method 对象中。
  • 如果函数指针为空,则调用clear_native_function清理本地方法对象。
static bool register_native(KlassHandle k, Symbol* name, Symbol* signature, address entry, TRAPS) {
  Method* method = k()->lookup_method(name, signature);
  if (method == NULL) {
    ResourceMark rm;
    stringStream st;
    st.print("Method %s name or signature does not match",
             Method::name_and_sig_as_C_string(k(), name, signature));
    THROW_MSG_(vmSymbols::java_lang_NoSuchMethodError(), st.as_string(), false);
  }
  if (!method->is_native()) {
    method = find_prefixed_native(k, name, signature, THREAD);
    if (method == NULL) {
      ResourceMark rm;
      stringStream st;
      st.print("Method %s is not declared as native",
               Method::name_and_sig_as_C_string(k(), name, signature));
      THROW_MSG_(vmSymbols::java_lang_NoSuchMethodError(), st.as_string(), false);
    }
  }

  if (entry != NULL) {
    method->set_native_function(entry,
      Method::native_bind_event_is_interesting);
  } else {
    method->clear_native_function();
  }
  if (PrintJNIResolving) {
    ResourceMark rm(THREAD);
    tty->print_cr("[Registering JNI native method %s.%s]",
      method->method_holder()->external_name(),
      method->name()->as_C_string());
  }
  return true;
}
复制代码

set_native_function函数逻辑为:

  • 通过native_function_addr函数获取本地函数地址,这个函数直接return (address*) (this+1);,可以看到它是直接将 Method 对象的地址+1 作为本地函数地址的。能够这样操作是因为在创建 Method 对象时会判断是否为 native 方法,如果是则会额外留两个地址位置,分别用于本地函数地址和方法签名。
  • 判断本地函数地址是否已经等于函数指针,是的话说明已经绑定,直接返回,否则继续往下。
  • 如果 Jvmti 设置了传播绑定本地方法事件则发送事件。
  • 将函数指针赋给本地函数地址。
  • GCC 获取编译的函数代码。
void Method::set_native_function(address function, bool post_event_flag) {
  assert(function != NULL, "use clear_native_function to unregister natives");
  assert(!is_method_handle_intrinsic() || function == SharedRuntime::native_method_throw_unsatisfied_link_error_entry(), "");
  address* native_function = native_function_addr();

  address current = *native_function;
  if (current == function) return;
  if (post_event_flag && JvmtiExport::should_post_native_method_bind() &&
      function != NULL) {
    assert(function !=
      SharedRuntime::native_method_throw_unsatisfied_link_error_entry(),
      "post_event_flag mis-match");
    JvmtiExport::post_native_method_bind(this, &function);
  }
  *native_function = function;
  CompiledMethod* nm = code(); 
  if (nm != NULL) {
    nm->make_not_entrant();
  }
}
复制代码

————-推荐阅读————

我的开源项目汇总(机器&深度学习、NLP、网络IO、AIML、mysql协议、chatbot)

为什么写《Tomcat内核设计剖析》

我的2017文章汇总——机器学习篇

我的2017文章汇总——Java及中间件

我的2017文章汇总——深度学习篇

我的2017文章汇总——JDK源码篇

我的2017文章汇总——自然语言处理篇

我的2017文章汇总——Java并发篇

跟我交流,向我提问:

《向JVM注册本地方法是怎么实现的》

欢迎关注:

《向JVM注册本地方法是怎么实现的》

    原文作者:JVM
    原文地址: https://juejin.im/post/5b8c7f54e51d4538b63d3db8
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞