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

回调与并发: 通过实例剖析WCF基于ConcurrencyMode.Reentrant模式下的并发控制机制

对于正常的服务调用,从客户端发送到服务端的请求消息最终会被WCF服务运行时分发到相应的封装了服务实例的InstanceContext上。而在回调场景中,我们同样...

1797
来自专栏法海无边

Spring-data-jpa 让数据访问更简单、更优雅

JPA不属于ORM框架,只是一套持久化API使用规范,能够更加灵活方便的管理数据库操作。从一定意义上来讲,吸取了Hibernate和Mybatis各自的优缺点,...

1386
来自专栏Spark学习技巧

Kafka源码系列之Broker的IO服务及业务处理

Kafka源码系列之Broker的IO服务及业务处理 一,kafka角色 Kafka源码系列主要是以kafka 0.8.2.2源码为例。以看spark等源码的经...

24010
来自专栏恰同学骚年

.NET基础拾遗(5)多线程开发基础

  下面的一些基本概念可能和.NET的联系并不大,但对于掌握.NET中的多线程开发来说却十分重要。我们在开始尝试多线程开发前,应该对这些基础知识有所掌握,并且能...

982
来自专栏orientlu

FreeRTOS 软定时器实现

考虑平台硬件定时器个数限制的, FreeRTOS 通过一个 Daemon 任务(启动调度器时自动创建)管理软定时器, 满足用户定时需求. Daemon 任务会在...

782
来自专栏Spark学习技巧

必会:关于SparkStreaming checkpoint那些事儿

spark Streaming的checkpoint是一个利器,帮助在driver端非代码逻辑错误导致的driver应用失败重启,比如网络,jvm等,当然也仅限...

632
来自专栏AILearning

Apache Spark 2.2.0 中文文档 - Spark Streaming 编程指南 | ApacheCN

Spark Streaming 编程指南 概述 一个入门示例 基础概念 依赖 初始化 StreamingContext Discretized ...

4599
来自专栏wOw的Android小站

[Java] CountDownLatch 与 CyclicBarrier

A synchronization aid that allows one or more threads to wait until a set of ope...

401
来自专栏华章科技

Spark知识体系完整解读

Spark简介 Spark是整个BDAS的核心组件,是一个大数据分布式编程框架,不仅实现了MapReduce的算子map 函数和reduce函数及计算模型,还...

762
来自专栏美图数据技术团队

Spark Streaming | Spark,从入门到精通

欢迎阅读美图数据技术团队的「Spark,从入门到精通」系列文章,本系列文章将由浅入深为大家介绍 Spark,从框架入门到底层架构的实现,相信总有一种姿势适合你,...

792

扫码关注云+社区