首页 > 编程知识 正文

java静态链接和动态链接的区别,java静态链接和动态链接

时间:2023-05-06 20:27:01 阅读:178059 作者:1156

为了实际对比概念,首先明确概念有很大的帮助。 那么,为什么要使用动态链接呢? 动态链路是为了解决静态链路的维护和资源利用问题而出现的。 那么,什么是静态链接呢? 静态链接是一种将元件从库中复制到目标产物的链接方法。 那么,链接是什么意思?

大多数模块、符号和链接日常使用高级语言。 为了便于管理和关注点分离(Separation of Concern ),具有一定规模的程序通常分为多个部分(文本)来编写,并编译为多个不同的模块。 这些模块不一定存在于同一个文件中,但可以通过符号相互引用。 一个模块通常包含三种符号。

其他模块可以引用的公共符号(符号定义)本地或专用符号定义)对其他模块中的公共符号的引用如果一个程序由多个模块组成,则链接器在实际执行之前对多个模块进行组合因为只要为符号的引用指定符号定义,程序就可以完全合并。 通过对符号的引用来查找符号的定义,并用符号定义的直接引用替换符号的引用称为链接。 正文中还出现了别的词,符号分析。 我的理解是,分析和链接在很多情况下意义相同,但分析更具体,链接可能涵盖更多内容。 例如,通过Java类加载流程链接的步骤实际上可以分为三个小步骤:验证、准备和分析。 另外,分析有时用于表达符号定义检索,但不进行实际的置换。

静态链接和动态链接

与静态绑定和动态绑定之间的关系非常相似,静态链接发生在编译时,而动态链接会在运行时推迟链接,以提高灵活性。

这里需要注意的是,编译时发生的所有链接都不是静态链接。 此外,链接看起来像一个动作,但实际上可以分成多个小动作来执行。

语言层面的对比上只是从概念层面介绍了链接的相关概念,语言在实现符号链接时存在很多选题。 另外,与我们的常识不同,Java的世界中也存在编译时的静态链接,同时c的动态链接也需要静态编译器参与。 所以,我的第一个想法是比较动态链接机制,但编译期也需要涉及一点。

以下,为了便于说明,将程序从源代码到目标产物的全过程称为编译期间,将从目标产物最终到达进程可执行代码区域的过程称为加载期间。

由于重点在于比较Java和c链接机制之间的差异,因此本文将省略相关但与链接关系不太直接的信息,如实际编译过程、验证逻辑和类加载机制。 请谅解。

为了简化说明,以下默认以Linux为运行环境。

编译时的C/C

无论如何,我们编写代码的目的是让代码在系统中执行。 但是,如果没有静态链接器的参与,源代码将被编译并只能获得Object文件。 通常,Object文件只包含当前特定体系结构的一组机器指令,因此无法直接运行Object文件。 如果Object使用外部符号,编译器不会尝试分析。

当静态链接器加入时,可以链接两个产物:可执行文件(Executable )和共享库。 通常,这两个产物内部的格式相同,如Linux的可执行文件和链接格式(elf )文件。 其中主要包含符号信息和代码。 与共享库相比,可执行文件主要有更多的默认入口定义。 但是,另一方面,也可以认为共享库有多个定制的入口。

除了Object文件和共享库外,链接器还可以从静态链接库分析元件。 使用静态链接库(.a,archive )和ar命令将Object文件组合在一起,以便于使用静态链接器。

在分析代码中引用的元件时,静态链接器将按顺序从Object文件或库中进行搜索。 但是,检测到的符号的处理因源而异。 静态链接库和Object文件中包含的元件直接打包在生成的文件中。 这就是前面提到的静态链接。 共享库中的元件不会打包为生成的内容。 详细情况要等到执行期的部分再详细说。

另一点是,可执行文件和共享库对链接的要求不同。 链接将生成可执行文件,所有元件都必须在Object文件或库中找到(resolved )。 共享库必须为-Wl,--no-undefined。 否则,允许存在unresolved符号。

静态链接库和共享库通常认为静态链接库有两个缺点。

1 .如果需要更新任何链接的库,则必须从一开始就重新链接产物。 例如,如果需要升级patch或链接的库。

2 .如果被参照的符号固化为生成物,则文件系统和内存都将被浪费。 如果使用C标准库实现的所有程序都在内部打包C库,则可以想象在文件系统和内存中存在多少重复代码。

通过将链接推迟到运行期,共享库和动态链接解决了这两个问题,还引入了其他问题。 但是,静态链接不是到处都有。 至少静态链接的产物非常容易使用,不需要在目标计算机上安装依赖项,启动时间也相对缩短。

编译时的Java通常在编译时只接触Javacompiler(JavaC )。 它吃了Java源代码,吐出的是Class文件。 Class文件类似于ELF,它以固定格式存储元件信息、代码和相关信息。 此外,由于Java是面向对象的,因此Class文件还包含类型信息。 Jar包类似于静态链接库,但、

是作为Class文件的归档存在的(倒是也能把一些资源文件打包到其中)。但不管是Class文件还是Jar包,都无法直接被OS执行:必须通过JVM这个虚拟机来执行(也需要定义好入口)。

为了拿到可执行文件,就要祭出AOT(Ahead of Time)Compiler了。其输入也是普通的Class文件或Jar包,但输出却是OS可识别的可执行文件,比如有入口的ELF文件。虽然我对AOT了解不多,但是这个编译器应该是是完成了一些Java层面的静态链接的工作,再加上Bytecode到机器指令的翻译,才使得直接执行成为了可能。最后可能还是会用到动态链接,只不过非Java平台中的动态链接罢了。

我觉得,AOT Compiler和Hotspot JVM的区别主要在于目标Runtime不一样,不同的平台所支持的链接模型也会有所不同。这样说起来,目标平台是Dalvik VM的Android编译器应该是类似的,只是不知道是否需要,了解的同学请不吝赐教。

编译期对比

抛开Java使用bytecode来承载逻辑这一点,从链接的层面来看,两种语言在这个阶段最大的区别(使用方式和运行机制)是:在编译链接可执行文件或共享库时,需要显示声明(如-lpthread)依赖的共享库,而且这部分信息需要静态链接器写入到ELF文件中,以便动态链接器在运行时使用(有不用手动配置的办法么?)。反观Java,我们只需要在编译时将依赖的Jar包或Class文件放到ClassPath下即可(Maven也帮我们做好了)。

我猜这与模块加载的机制和符号组织方式有关系。在C中,当链接器要链接某一个符号时,链接器是不知道符号存在于哪个库中的。确实也可以去挨个儿扫描,但是效率不高,语言的实现者选择让语言的使用者来提供这个信息。而Java中,符号必然是附带了Class的信息(字段或方法属于哪个类),由于名称也是对应的,这样类加载器就知道应该加载哪一个类对应的Class文件,去文件系统拿就好了(或者是网络或内存中)。

运行期的C/C++

当我们触发可执行文件执行时,程序就进入了运行期。


在这个阶段,操作系统会先加载并执行动态链接器,而并非直接运行我们的第一行代码。动态链接器会扫描由静态链接器嵌入到可执行文件中的共享库依赖列表,把可执行文件所依赖的所有共享库都加载到内存中(这里省略了对查找的描述)。若库已经加载到了内存中,则只需要内存映射一下。如果出现某个依赖的库找不到,会出现错误而停止执行。如果共享库依赖了其他共享库,也会触发其他共享库的加载(加载流程是一个广度优先的遍历)。

加载完毕后,动态链接器也不会立刻开始解析符号。鉴于大多数时候并非所有代码都会使用到,也为了使startup时间尽量短(因为每次运行都需要链接),链接器采用了延迟绑定符号(lazy symbol binding)的策略。

为了实现延迟符号绑定,静态链接器会帮忙对共享库中的符号进行特殊处理。静态链接器在链接可执行文件时,会构造一个名为procedure-linking table(PLT)的跳表,并包含到可执行文件中。然后,静态链接器让代码中所有解析到的对共享库中符号(函数)的引用都指向PLT中特定的entry。

回到运行时,动态链接器会把PLT中所有entries都指向自己内部一个特殊的符号绑定函数。当任意库函数被第一次触发时,动态链接器会恢复控制,然后执行实际的符号绑定:定位到一个符号然后将对应的PLT entry替换为对符号的直接引用。这样之后的请求都会直接调用到对应的符号。这就是C中的动态链接的基本工作机制。

共享库之所以能成为共享库,首先是因为有动态链接这种机制,可以使得符号解析推迟到运行期,这样共享库中的符号就不需要打包到可执行文件中。同时,鉴于库中主要包含系统指令,类似于只读数据,基于内存映射实现库中符号在可执行文件之间共享就顺理成章了。为此,共享库通常也称动态链接共享库(Dynamically linked shared library),或者就称之为动态链接库。

现在大多数情况都会选择使用共享库,因为相对静态链接库,使用动态链接共享库还有一个好处:可以直接升级共享库而达到动态patch的目的,而不用重新链接整个可执行文件。

运行期的Java

Java中符号是按照类来组织的,所以Java的链接模型的核心,就是JVM对类的操作。

java -cp . Main复制代码

执行命令之后,JVM首先会去AppClassLoader加载启动类Main,目的是为了执行其中的main方法(见sun.launcher.LauncherHelper#checkAndLoadMain)。与其他类一样,这个启动类会经过加载、链接、初始化、使用和卸载几个阶段,其中链接又可分为验证、准备和解析三个子阶段。

Java中加载一个类,意味着通过类加载器定位到这个类的Class文件(或对应格式的内容),将其中的信息存储起来,还会构建一个Class类的对象来提供对这些信息的编程式访问。接下来还会有对这些信息的验证和准备,然后才会开始具体的解析动作。

Java中的符号,一开始存储在Class文件中的常量池,在加载完成后则进入JVM为每个类单独维护的运行时常量池。符号的解析,则是指将运行时常量池中的符号引用替换为直接引用。

C中符号解析是延迟化的,按需的。那Java中符号的解析呢?

就虚拟机规范来说,并没有规定解析实际发生的时机。虚拟机实现可以选择在加载完成后就对类中所有的符号进行解析,也可以当类中某个符号实际被使用到时,才触发真正的链接过程。实际上,JVM规范只要求在用于操作符号引用的字节码执行之前,对这些指令所使用的符号引用完成解析就行了,LinkageError这样的链接错误也只能在符号被使用时才能抛出。换句话说,规范要求JVM实现对外表现出来是按需解析符号的。那实际呢?

我看过的多数讲解类生命周期或加载机制的文章都是从类加载讲起的,”当我们主动使用某个类的时候,会触发类加载器这个类的加载”,并且还归纳出六种主动使用类的方式:

访问某个类或接口的静态变量,或者对该静态变量赋值调用类的静态方法用new创建类的实例初始化某个类的子类,则其父类也会被初始化反射(如Class.forName(“com.xxx.xxx”))Java虚拟机启动时被标明为启动类的类,就如我们的Main

前边两种方式是我们平时写Java代码使用得很频繁的,由于相应字节码需要使用符号(getstatic,setstatic,invokestatic),到了符号必须链接的时候了。如果需要的符号不存在在内存中(因为对应的类尚未加载),会触发类加载流程。所以,前两种方式实际上是由链接触发类的加载,加载完成后接着就要进行链接,完成所需符号的解析。

第三和第四种是普通的类加载流程及其副作用。第五种方式是另外一种特殊情况,后边再讲。第六种情况是实际上就是用第五种方式实现的。

前文提到过,C是在一开始就确保了所有依赖的模块(共享库)加载到了内存中,但链接却是按需的。Java中对类(符号)的加载是按需的,而链接则分两种情况:

当前执行的字节码需要解析符号,如果所需要的类尚未加载,则先进入类加载流程,然后完成链接。这种情况下,解析是按需的先触发了类加载,JVM可以选择直接完成类中所引用符号的链接,也可以选择按需链接 动态加载

前文提到,Java中模块的加载可能是由符号链接触发的,也可能是因为需要创建类实例导致的。另外还有一种情况,那就是由动态加载机制(其实我觉得用主动加载更能表达目的)触发的,即在运行时由程序决定加载和链接什么符号(以动态链接为基础)。这种能力让语言具备了获取在编译时尚未存在的模块和符号,以支持程序的动态扩展和实现插件机制。

在C中也支持动态加载,我们可以通过动态加载系统函数来加载指定的共享库并使用其中的符号。JNI中对共享库的加载就是基于dlopen函数实现的。但针对这些动态加载的符号的具体链接过程还有有一些小的差别,详见The inside story on shared libraries and dynamic loading ,这里就不展开说了。

实际的使用过程中,Java提供了ClassLoader.loadClass或Class.forname(前文提到过的反射)两种方式来完成Class文件的动态加载。

因为是先触发类加载,类中符号解析的时机就不确定了。JVM可以选择提前,也可以选择在符号即将被使用时再完成链接。

动态链接的对比

最后,回到本文的主题。符号解析的前置条件是符号(模块)已经完成了加载,所以其实加载和链接是一个整体(先后顺序不定),都是语言链接模型的一部分。从加载和链接的角度,Java和C中动态链接的不同点有以下这些:

对于模块加载,C以共享库为单位,而Java则是以Class文件为单位C的动态链接依赖于静态链接器在编译期写入的共享库依赖列表,而Java不需要C中可执行文件依赖的所有共享库会在启动时完成加载,而Java的Class是按需加载的C只支持从本地文件系统中加载共享库(有一套既定的查找规则),而Java的类加载体系除了支持从本地文件系统查找类,还支持自定义类加载器,从而支持程序从任意自定义位置加载类,比如网络、数据库甚至是动态生成类Java中的Class文件最多只能被称为动态链接库,因为它加载到内存中之后无法在多个JVM间共享 总结

从后往前看,我觉得我对链接的困惑,源自Java的编程模型没有(显示)包含链接这一环,以至于我对链接的概念太陌生了(也是理解得还不够好)。而且,我感觉自己在很长一段时间内都是假装知道链接是什么意思,同时也模模糊糊地把Library等同于了Jar包。

不懂装懂真可怕,特别是自己。

Ref

The Linking Model, Chapter 8 of Inside the Java Virtual Machine,本文的基础,非常详细,但是不好get到big picture ↩

Library (computing) - Wikipedia↩

c++ - Static linking vs dynamic linking ↩

Linker (computing) - Wikipedia 概念上很全面 ↩

Static, Shared Dynamic and Loadable Linux Libraries 具体细节足够详细 ↩

The 101 of ELF Binaries on Linux: Understanding and Analysis↩

c - Shared libraries vs executable↩

c++ - Force GCC to notify about undefined references in shared libraries↩

The inside story on shared libraries and dynamic loading↩

What is the difference in byte code like Java bytecode and files and machine code executables like ELF?↩

How do Java AOT compilers work?↩

PLT and GOT - the key to code sharing and dynamic libraries↩

Java is Dynamic(ly linked)↩



版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。