JDK中动态库加载路径问题,一文讲清

jdk,动态,加载,路径,问题,一文,讲清 · 浏览次数 : 229

小编点评

**加载第一层so** * **classloader**去加载 * **sun.boot.library.path** * **java.library.path** **加载第二层so** * **java.library.path**默认值 * **PATH**环境变量 **加载第三层so** * **java.library.path**默认值 * **LD_LIBRARY_PATH**环境变量

正文

前言

本周协助测试同事对一套测试环境进行扩容,我们扩容很原始,就是新申请一台机器,直接把jdk、resin容器(一款servlet容器)、容器中web应用所在的目录,全拷贝到新机器上,servlet容器和其中的应用启动没问题。以为ok了,等到测试时,web应用报错,初始化某个类出错。报错的类长下面这样:

com.thinkive.tbascli.TBASCli

static {
        String os = "win";
        String pathSep = System.getProperty("path.separator");
        if (pathSep.equalsIgnoreCase(":")) {
            os = "linux";
        }

        try {
            // 1
            System.loadLibrary("TBASClientJNI");
        } catch (SecurityException var5) {
            var5.printStackTrace();
            System.out.println("Load TBASClientJNI Library Failed");
        } catch (UnsatisfiedLinkError var6) {
            URL url = TBASCli.class.getClassLoader().getResource("");
            String path = (new File(URI.create(url.toExternalForm()))).getAbsolutePath();
            if (os.equalsIgnoreCase("win")) {
                // 2
                loadDLL(path);
            } else if (os.equalsIgnoreCase("linux")) {
                // 3
                loadSO(path);
            } else {
                loadDLL(path);
            }
        }
    	...
}

简单来说,就是处理请求的代码用到这个类,然后类加载,执行static,结果执行System.loadLibrary失败了。

失败了也没啥,问题是,这个类是个底层框架里的类,然后失败原因也不打日志。

当时已经心里骂过人了,现在就不说啥了,说说当时处理过程。

处理经过

arthas使用watch查看方法执行上下文

当时以为是System.loadLibrary("TBASClientJNI");失败,抛了异常,进了catch分支,以为会进

loadDLL(path);
loadSO(path);
private static void loadSO(String path) throws SecurityException, UnsatisfiedLinkError {
    String sep = System.getProperty("file.separator");
    System.load(path + sep + "libTBASClient.so");
    System.load(path + sep + "libTBASClientJNI.so");
}

这里看到最终会调System.load方法,就想用arthas的watch观察下参数:类似于下面这样:

watch java.lang.System load '{params, target, returnObj, throwExp}' -x 2  

结果工具报错:

[arthas@110269]$ watch java.lang.System load
Affect(class count: 0 , method count: 0) cost in 31 ms, listenerId: 1
No class or method is affected, try:
1. Execute `sm CLASS_NAME METHOD_NAME` to make sure the method you are tracing actually exists (it might be in your parent class).
2. Execute `options unsafe true`, if you want to enhance the classes under the `java.*` package.
3. Execute `reset CLASS_NAME` and try again, your method body might be too large.

按照工具的第二条提示,设置了,也还是报错,反正,当时这条路是没有走下去。

当时也试了去watch当前类的loadSO方法,不知道为啥,也是没观察到东西,我们用的jdk1.7,不清楚有没有影响。

覆盖框架类,增加日志

上面报错这个类,在我们的TBASClientJNI-2.2.0.jar中,我想着还是覆盖框架类,加点日志试试吧,于是在应用中,新增了一个包名类名都一致的类:com.thinkive.tbascli.TBASCli,修改了其中的代码:

image-20230812133005715

我们的应用,打出来的jar是在test-web.jar中,最终部署的时候,应用jar和依赖的框架jar是在同一个文件夹下,在同一个文件夹下的话,类加载的顺序是没法保证的,所以,我当时在开发环境验证了下,发现日志能看到,结果等我把改后的jar放到测试环境时,发现完全没生效,看不到日志,应该就是优先加载了旧的class。

当时也看了下,类加载的一个情况,利用arthas查看类来自哪个jar的哪个文件(以下截图来自开发环境,当时没截图):

image-20230812133942732

这里也扩展下,其他类加载相关的命令:

  • 查看类加载器及hash

    classloader -l

    image-20230812133603554

  • 查看类加载器的加载路径

    classloader -c 3bd3ac38

    image-20230812134258446

比较原机器和克隆机器文件差异

然后暂时也没啥好办法,看看是不是两边是不是漏复制了啥,或者不小心改到什么地方了。把两边的几个文件夹仔细对比了下,没发现啥问题。

也对比了一些环境变量,比如linux默认会

lsof立功

后面在两台机器上各种排查,命令一顿敲,后面发现,在原机器上执行lsof -p pid,查看进程打开的so文件时,发现两边不太一样。

这里还要补充解释下,前面大家以为我们只是一个so文件,其实是两个,如下,其中一个是xxxJNI.so,我们代码里也是去加载这个,而不带JNI的这个so,是xxxJNI.so的内部依赖的so。

[root@xxx-access ~]# ll /test-web/WebRoot/WEB-INF/classes |grep TBA
-rw-r--r-- 1 root root  20704 Feb 20 13:19 libTBASClientJNI.so
-rw-r--r-- 1 root root 103904 Feb 20 13:19 libTBASClient.so

所以,实际上来说,我们的jdk必须加载了这两个so,请求才能正常处理。

我在原机器上执行lsof的结果是:

[root@server172 ~]# lsof -p 28644|grep TBA
java    28644 root  mem    REG              253,1    103904 101004125 /usr/lib64/libTBASClient.so
java    28644 root  mem    REG              253,0     20704  21663079 /test-web/WebRoot/WEB-INF/classes/libTBASClientJNI.so

而在新机器执行的结果是:

[root@xxx-access ~]# lsof -p 110269|grep \\.so |grep TBA
java    110269 root  mem       REG              253,0     20704  34821647 /test_web/WebRoot/WEB-INF/classes/libTBASClientJNI.so

可以发现,原来的机器虽然正常运行,但是,加载的so竟然在不同文件夹下,带JNI的这个libTBASClientJNI.so,确实用的是项目路径下的;而那个libTBASClient.so,居然是/usr/lib64下的,我们确实没拷贝/usr/lib64下的那个so到新机器,估计就是这个原因了。

新机器上呢,只加载了一个so,少了一个so,估计这也就是问题原因了。

我在新机器上,试了两种改法,都有效果:

  1. 在/usr/lib64下放上那个libTBASClient.so

  2. 修改/etc/profile,设置:

    export   LD_LIBRARY_PATH=/test-web/WEB-INF/classes/:$LD_LIBRARY_PATH
    

为啥改了有效果呢,下面看看原理。

加载第一层so的原理剖析

回到报错的那行代码:

System.loadLibrary("TBASClientJNI");

了解它的最好的办法,还是在本地debug。

java.lang.System#loadLibrary
public static void loadLibrary(String libname) {
    Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
java.lang.Runtime#loadLibrary0
synchronized void loadLibrary0(Class fromClass, String libname) {
    ClassLoader.loadLibrary(fromClass, libname, false);
}

内部实现如下:

static void loadLibrary(Class fromClass, String name,
                            boolean isAbsolute) {
    	// 1 获取classloader
        ClassLoader loader =
            (fromClass == null) ? null : fromClass.getClassLoader();
    	// 2 用系统property的值来初始化field:usr_paths/sys_paths
        if (sys_paths == null) {
            usr_paths = initializePath("java.library.path");
            sys_paths = initializePath("sun.boot.library.path");
        }
    	// 3 外部传参为false,进不去本分支,不管
        if (isAbsolute) {
            if (loadLibrary0(fromClass, new File(name))) {
                return;
            }
            throw new UnsatisfiedLinkError("Can't load library: " + name);
        }
        if (loader != null) {
            // 4 由调用本方法的类的classloader来负责查找library,由具体classloader覆写
            String libfilename = loader.findLibrary(name);
            if (libfilename != null) {
                File libfile = new File(libfilename);
                if (!libfile.isAbsolute()) {
                    throw new UnsatisfiedLinkError(
    "ClassLoader.findLibrary failed to return an absolute path: " + libfilename);
                }
                if (loadLibrary0(fromClass, libfile)) {
                    return;
                }
                throw new UnsatisfiedLinkError("Can't load " + libfilename);
            }
        }
    	// 5 根据上文初始化处,这里即是从sun.boot.library.path 这个变量中加载
        for (int i = 0 ; i < sys_paths.length ; i++) {
            File libfile = new File(sys_paths[i], System.mapLibraryName(name));
            if (loadLibrary0(fromClass, libfile)) {
                return;
            }
        }
    	// 6 根据上文初始化处,这里即是从java.library.path 这个变量中加载
        if (loader != null) {
            for (int i = 0 ; i < usr_paths.length ; i++) {
                File libfile = new File(usr_paths[i],
                                        System.mapLibraryName(name));
                if (loadLibrary0(fromClass, libfile)) {
                    return;
                }
            }
        }
        // Oops, it failed
        throw new UnsatisfiedLinkError("no " + name + " in java.library.path");
    }

可以看到上面注释,加载也就是从3个地方加载;

  • 4处,首先从classloader加载

    [arthas@110269]$ classloader -c 3bd3ac38
    file:/test-web/WebRoot/WEB-INF/classes/ 
    ...
    
  • 5处,从sun.boot.library.path加载

    [arthas@110269]$ sysprop  sun.boot.library.path 
     KEY                               VALUE                           
     sun.boot.library.path             /usr/local/java/jdk1.7.0_80/jre/lib/amd64 
    
  • 6处,从java.library.path加载

    sysprop java.library.path

    image-20230812144226302

所以,我们就能解释如下的结果了:

[root@xxx-access ~]# lsof -p 110269|grep \\.so |grep TBA
java    110269 root  mem       REG              253,0     20704  34821647 /test_web/WebRoot/WEB-INF/classes/libTBASClientJNI.so

应该就是走了4处的逻辑,才加载到这个JNI的so。

那么,为啥又没加载到libTBASClient.so呢,我在网上看到的解释是,so内部加载其他依赖的so,这时候,内部已经不是java代码了,不可能走这段java.lang.ClassLoader#loadLibrary逻辑,所以,此时,就不是这段逻辑了。

加载so中依赖的so的加载逻辑

那么,对于libTBASClientJNI.so依赖的so,又是去哪里加载呢,这块呢,我的理解不是很深入,我的理解是,在windos机器,会去PATH环境变量中加载;在linux,会去环境变量LD_LIBRARY_PATH中指定的路径加载。

但根据我这边的现象看,比如最终是在/usr/lib64中找到了libTBASClientJNI.so,但我的LD_LIBRARY_PATH并没有设置/usr/lib64,所以,jvm的实现中估计还会根据java.library.path这个属性中的路径去查找。因为我程序中,查看arthas的sysprop,只有它下面有/usr/lib64这个路径。

image-20230812144226302

java.lirary.path的初始值来自哪里

arthas查看

sysprop java.library.path即可看到。

cmd下查看(windows)

java -XshowSettings:properties

image-20230812151126871

在windows下,java.library.path初始值来自PATH环境变量。

linux下java命令

image-20230812151254735

linux下,有默认值,如上面这几个路径;另外,如果有设置LD_LIBRARY_PATH环境变量,那么java.library.path的值就等于默认的几个路径(/usr/lib64、/lib64、/lib、/usr/lib) + LD_LIBRARY_PATH的值。

总结

java加载第一层的so,主要是根据classloader去加载、其次是 sun.boot.library.path 、再其次是java.library.path。

加载第一层so依赖的so,在jdk中貌似也是根据java.library.path;如果是非jdk,应该是根据LD_LIBRARY_PATH环境变量。

而java.library.path的默认值(不显示设置的情况下),在windows下就是来源于PATH,在linux下来源于LD_LIBRARY_PATH和几个默认路径(/usr/lib64、/lib64、/lib、/usr/lib),具体可以执行java -XshowSettings:properties查看。

参考:

https://stackoverflow.com/questions/29968292/what-is-java-library-path-set-to-by-default

https://stackoverflow.com/questions/20038789/default-java-library-path

https://stackoverflow.com/questions/16227045/how-to-add-so-file-to-the-java-library-path-in-linux

通过arthas的vmtool查看内存对象

https://arthas.aliyun.com/doc/vmtool.html

https://blog.csdn.net/qq_40911404/article/details/121741268

与JDK中动态库加载路径问题,一文讲清相似的内容:

JDK中动态库加载路径问题,一文讲清

# 前言 本周协助测试同事对一套测试环境进行扩容,我们扩容很原始,就是新申请一台机器,直接把jdk、resin容器(一款servlet容器)、容器中web应用所在的目录,全拷贝到新机器上,servlet容器和其中的应用启动没问题。以为ok了,等到测试时,web应用报错,初始化某个类出错。报错的类长下

【Java】JDK动态代理实现原理

代理模式 代理模式一般包含三个角色: Subject:主题对象,一般是一个接口,定义一些业务相关的基本方法。 RealSubject:具体的主题对象实现类,它会实现Subject接口中的方法。 Proxy:代理对象,里面包含一个RealSubject的引用,外部会通过这个代理对象,来实现RealSu

还在stream中使用peek?不要被这些陷阱绊住了

简介 自从JDK中引入了stream之后,仿佛一切都变得很简单,根据stream提供的各种方法,如map,peek,flatmap等等,让我们的编程变得更美好。 事实上,我也经常在项目中看到有些小伙伴会经常使用peek来进行一些业务逻辑处理。 那么既然JDK文档中说peek方法主要是在调试的情况下使

京东云开发者|深入JDK中的Optional

Optional最早是Google公司Guava中的概念,代表的是可选值。Optional类从Java8版本开始加入豪华套餐,主要为了解决程序中的NPE问题,从而使得更少的显式判空,防止代码污染,另一方面,也使得领域模型中所隐藏的知识,得以显式体现在代码中。Optional类位于java.util包下,对链式编程风格有一定的支持。实际上,Optional更像是一个容器,其中存放的成员变量是一个T类

如何创建一个线程池,为什么不推荐使用Executors去创建呢?

我们在学线程的时候了解了几种创建线程的方式,比如继承Thread类,实现Runnable接口、Callable接口等,那对于线程池的使用,也需要去创建它,在这里我们提供2种构造线程池的方法: 方法一: 通过ThreadPoolExecutor构造函数来创建(首选) 这是JDK中最核心的线程池工具类,

什么是ForkJoin?看这一篇就能掌握!

摘要:ForkJoin是由JDK1.7之后提供的多线程并发处理框架。 本文分享自华为云社区《【高并发】什么是ForkJoin?看这一篇就够了!》,作者: 冰 河。 在JDK中,提供了这样一种功能:它能够将复杂的逻辑拆分成一个个简单的逻辑来并行执行,待每个并行执行的逻辑执行完成后,再将各个结果进行汇总

Java 21新特性:Sequenced Collections(有序集合)

在JDK 21中,Sequenced Collections的引入带来了新的接口和方法来简化集合处理。此增强功能旨在解决访问Java中各种集合类型的第一个和最后一个元素需要非统一且麻烦处理场景。 下面一起通过本文来了解一下不同集合处理示例。 Sequenced Collections接口 Seque

[转帖]【技术剖析】12. 毕昇 JDK 8 中 AppCDS 实现介绍

https://bbs.huaweicloud.com/forum/thread-169622-1-1.html 作者:伍家华 > 编者按:笔者通过在 Hive 的场景发现 AppCDS 技术存在的价值,然后分析了 AppCDS 的工作原理,并将 JDK 11 中的特性移植到毕昇 JDK 8,在移植

Java 21 新特性:Record Patterns

Record Patterns 第一次发布预览是在JDK 19、随后又在JDK 20中进行了完善。现在,Java 21开始正式推出该特性优化。下面我们通过一个例子来理解这个新特性。 record Point(int x, int y) {} static void printSum(Object o

Java 21 新特性:switch的模式匹配

在之前的Java 17新特性中,我们介绍过关于JEP 406: switch的模式匹配,但当时还只是关于此内容的首个预览版本。之后在JDK 18、JDK 19、JDK 20中又都进行了更新和完善。如今,在JDK 21中,该特性得到了最终确定!下面,我们就再正式学习一下该功能! 在以往的switch语