作者:张文波
导语 : 在一些混编系统中,我们使用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的动态加载比较简单,通过dlopen得到加载的so的句柄(void *),dlsym获得函数地址。一般为:
在解决Java动态加载so之前,我们跟着源码来看System.load是如何实现的(以下源码都以JDK1.8为例)。前面已经描述Java静态加载so一般都是通过System.load()或者System.loadLibrary()实现,实际两者调用的JNI代码是一致的,所以我们以System.load()为例。
a. 跟着System.load()以及ClassLoader.java看下去(略去中间步骤),我们找到了下面的native接口:
// ...
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加载的地方。
// ...
/*
* 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
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
// ...
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。
If this method is called more than once with the same library name, the second and subsequent calls are ignored.
那么我们如何实现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流程
动态加载流程为:
综上,我们详细剖析了Java加载so的机制,并设计了一套在Java框架中动态加载so的方案。我们将这套机制成功应用于图像识别服务框架中从0到1打造轻量级图像识别服务框架。ProxySo是一个非常轻量级的so,实现简单并且实测下来,性能跟直接通过C++加载so无明显差异。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。