Java 动态加载 so 的解决方案

作者:张文波

导语 : 在一些混编系统中,我们使用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接口:

// ...
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呢?

三、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无明显差异。

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

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

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大内老A

Dora.Interception: 一个为.NET Core度身定制的AOP框架

多年从事框架设计开发使我有了一种强迫症,那就是见不得一个应用里频繁地出现重复的代码。之前经常Review别人的代码,一看到这样的程序,我就会想如何将这些重复的代...

2225
来自专栏柠檬先生

Angularjs基础(四)

AngularJS过滤器     过滤器可以使用一个管道符(|)添加到表达式和指令中。       AngularJS过滤器可用于转换数据:    ...

1849
来自专栏Java帮帮-微信公众号-技术文章全总结

【大牛经验】Java9的新特性

Java 9 包含了丰富的特性集。虽然Java 9没有新的语言概念,但是有开发者感兴趣的新的API和诊断命令。

703
来自专栏PHP技术

了解PHP中Stream(流)的概念与用法

Stream是PHP开发里最容易被忽视的函数系列(SPL系列,Stream系列,pack函数,封装协议)之一,但其是个很有用也很重要的函数。Stream可以翻译...

3225
来自专栏Java工程师日常干货

【随笔】JVM核心:JVM运行和类加载

本篇博客将写一点关于JVM的东西,涉及JVM运行时数据区、类加载的过程、类加载器、ClassLoader、双亲委派机制、自定义类加载器等,这些都是博主自己的一点...

603
来自专栏逍遥剑客的游戏开发

GameEngineArchitecture读书笔记(三)

1533
来自专栏非典型技术宅

Swift多线程之Operation:异步加载CollectionView图片1. Operation 设置依赖关系2. 前置知识点内容3. CollectionView中图片进行异步加载

1487
来自专栏葡萄城控件技术团队

程序员级别鉴定书(.NET面试问答集锦)

作为一个.NET程序员,应该知道的不仅仅是拖拽一个控件到设计时窗口中。就像一个赛车手,一定要了解他的爱车 – 能做什么不能做什么。 本文参考Scott Hans...

2467
来自专栏CSDN技术头条

Java 9的14个新特性总结

Java 9 包含了丰富的特性集。虽然Java 9没有新的语言概念,但是有开发者感兴趣的新的API和诊断命令。 我们将快速的,着重的浏览其中的几个新特性: ? ...

1965
来自专栏owent

protobuf-net的动态Message实现

这本来是个早就可以写的分享。因为代码几周前就迁移并准备好了。而且这也是之前项目的工具,因为可以抽离出来通用化所以单独整理出来。

771

扫码关注云+社区