前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 动态加载 so 的解决方案

Java 动态加载 so 的解决方案

原创
作者头像
serena
修改2021-08-03 14:56:08
8.3K1
修改2021-08-03 14:56:08
举报
文章被收录于专栏:社区的朋友们社区的朋友们

作者:张文波

导语 : 在一些混编系统中,我们使用Java成熟的网络/调度框架编写框架代码,使用C++编写适用于计算密集型的so,通过Java函数System.load进行全局静态的so加载/卸载。业务场景有对so实现动态加载/替换的需求,但Java并没有直接动态加载so的机制。本文将深度剖析Java加载so的实现机制,并提出一套Java动态加载so的方案。

在一些业务场景中,为了支持单点单so(动态链接库)的热更新,需要在框架层动态加载/替换so。这里动态加载so,是指当前so提供服务的时候,需要动态加载另一个同名so,并对旧的so进行替换,而不影响现有服务。

考虑开发效率和成熟的网络/调度框架,我们使用Java作为网络和调度框架;而计算密集型或者某些只能使用C/C++的场景(如GPU),我们会使用C++编写so作为算法/业务代码实现。这个过程涉及到的Java加载so,一般都是使用Java函数System.load()或者System.loadLibrary(),通过JNI调用C++动态链接库,整个流程在业界已经非常成熟。那我们如何实现Java框架中的so动态加载呢?

一、C++如何实现so动态加载

C++框架实现so的动态加载比较简单,通过dlopen得到加载的so的句柄(void *),dlsym获得函数地址。一般为:

  1. 框架中维护一个so到句柄的map,dlopen,dlsym成功后,将新的句柄替换至map;
  2. 新的流量请求通过map转发到新的so函数;
  3. 待老的so没流量之后,将老的so卸载(dlclose)即可完成so的动态加载。

二、Java加载so原理剖析

在解决Java动态加载so之前,我们跟着源码来看System.load是如何实现的(以下源码都以JDK1.8为例)。前面已经描述Java静态加载so一般都是通过System.load()或者System.loadLibrary()实现,实际两者调用的JNI代码是一致的,所以我们以System.load()为例。

a. 跟着System.load()以及ClassLoader.java看下去(略去中间步骤),我们找到了下面的native接口:

代码语言:javascript
复制
// ...
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);

public NativeLibrary(Class<?> fromClass, String name, boolean isBuiltin) {
  this.name = name;
  this.fromClass = fromClass;
  this.isBuiltin = isBuiltin;
}
// ...

b. 在JDK源码中找到ClassLoader中对应的native代码ClassLoader.c,下面是ClassLoader的JNI实现,JVM_LoadLibraray(cname)里面即是so加载的地方。

代码语言:javascript
复制
// ...
/*
 * Class:     java_lang_ClassLoader_NativeLibrary
 * Method:    load
 * Signature: (Ljava/lang/String;Z)V
 */
JNIEXPORT void JNICALL
Java_java_lang_ClassLoader_00024NativeLibrary_load
  (JNIEnv *env, jobject this, jstring name, jboolean isBuiltin)
{
    const char *cname;

    // ...
    handle = isBuiltin ? procHandle : JVM_LoadLibrary(cname);
    // ...

c. 在hotspot源码中找到JVM_LoadLibraray的实现jvm.cpp

代码语言:javascript
复制
JVM_ENTRY_NO_ENV(void*, JVM_LoadLibrary(const char* name))
  //%note jvm_ct
  JVMWrapper2("JVM_LoadLibrary (%s)", name);
  // ...
  {
    ThreadToNativeFromVM ttnfvm(thread);
    load_result = os::dll_load(name, ebuf, sizeof ebuf);
  }
  // ...

d. 跟进os::dll_load(),有三个不同实现分别对应三个平台os_linux, os_windows, os_solaris,这里只看os_linux.cpp

代码语言:javascript
复制
// ...
void * os::dll_load(const char *filename, char *ebuf, int ebuflen)
{
  void * result= ::dlopen(filename, RTLD_LAZY);
  if (result != NULL) {
    // Successful loading
    return result;
}
// ...

到这里恍然,dlopen(filename, RTLD_LAZY)即是linux下Java System.load的最终实现,其实跟C++加载动态链接库是一样的。

那我们是否可以利用dlopen返回的句柄来进行动态加载呢?答案是否定的,因为Java没法接受void *,在a的时候,JNI并没有将加载so的句柄返回给Java代码。官方文档也说明,实际上JVM load/loadLibrary都是全局加载的,没法同时加载两个同名so。

代码语言:javascript
复制
If this method is called more than once with the same library name, the second and subsequent calls are ignored.

那么我们如何实现Java动态加载so呢?

三、Java中动态加载so

我们没法通过System.load()重复加载同名so或者直接动态替换so,也没法在Java层拿到dlopen返回的句柄,所以我们没法在Java代码层实现so的动态加载。当然还有一种做法是先卸载(System.unload),再加载(System.load),但这个过程不是无损的。

最终我们设计了一套代理方案,通过System.load()加载libproxy.so,然后在libproxy.so中实现了跟文章第一节说的动态加载过程。libproxy.so中会维护一个map, key为Java框架中传入的String,value为包含dlopen返回的句柄,dlsym拿到的函数地址以及相关的上下文信息。在libproxy.so进行数据的转发并且封装了相应JNI的转换,彻底的将算法so与框架解耦开了。如图所示:

图1 Java框架、代理so与算法so解耦

图2 Java动态加载so流程

动态加载流程为:

  1. JavaServer会监听so目录,但so发生变更后,JavaServer通过JNI调用libproxy.so的reload方法,并将该so对应的key及路径传入;
  2. libproxy.so完成dlopen及dlsym之后,会将新的句柄,函数等存入map,后面所有的请求都会被指向新的so;
  3. 在一段时间后,延迟卸载旧的so

四、总结

综上,我们详细剖析了Java加载so的机制,并设计了一套在Java框架中动态加载so的方案。我们将这套机制成功应用于图像识别服务框架中从0到1打造轻量级图像识别服务框架。ProxySo是一个非常轻量级的so,实现简单并且实测下来,性能跟直接通过C++加载so无明显差异。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、C++如何实现so动态加载
  • 二、Java加载so原理剖析
  • 三、Java中动态加载so
  • 四、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档