JVM 的艺术—JAVA 内存模型
本篇文章将进入jvm领域的另一个知识点,java内存模型。彻底的了解java内存模型,是有必要的。只要掌握了java的内存模型,内存空间分为哪些区域,才能更好地理解,java是如何创建对象以及如何分配对象的空间。对后续的jvm调优打下坚实的基础。而对于现在的互联网行业来说,高并发,高可用已经必不可少,而学好jvm调优,不仅能在企业工作当中针对高并发场景下的系统进行优化,在日常对系统的错误排查、系统的优化也起着至关重要的作用。希望这篇文章能让各位读者学到真正的本领。同时也感谢大家的持续关注和认可。
一:JDK体系结构
JDK、JRE、JVM之间的关系
JDK:Java Development Kit(java开发工具包),包含JRE和开发工具包,例如javac、javah(生成实现本地方法所需的 C 头文件和源文件)。
JRE:Java Runtime Environment(java运行环境),包含JVM和类库。
JVM:Java Virtual Machine(Java虚拟机),负责执行符合规范的Class文件。
Java语言的跨平台特性
JVM所处的位置
(1)通常工作中所接触的基本是Java库和应用以及Java核心类库,知道如何使用就可以了,但是归根结底代码都是要编译成class文件由Java虚拟机装载执行,所产生的结果或者现象都可以通过Java虚拟机的运行机制来解释。一些相同的代码会由于虚拟机的实现不同而产生不同结果。
(2)在Java平台的结构中,可以看出,Java虚拟机(JVM)处在核心的位置,是程序与底层操作系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统,其中依赖于平台的部分称为适配器;JVM通过移植接口在具体的平台和操作系统上实现;在JVM的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application)和小程序(Java applet)可以在任何Java平台上运行而无需考虑底层平台,就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java的平台无关性。
(3)对JVM规范的的抽象说明是一些概念的集合,它们已经在书《The Java Virtual Machine Specification》(《Java虚拟机规范》)中被详细地描述了;对JVM的具体实现要么是软件,要么是软件和硬件的组合,它已经被许多生产厂商所实现,并存在于多种平台之上;运行Java程序的任务由JVM的运行期实例单个承担。
(4)JVM可以由不同的厂商来实现。由于厂商的不同必然导致JVM在实现上的一些不同,像国内就有著名的TaobaoVM;然而JVM还是可以实现跨平台的特性,这就要归功于设计JVM时的体系结构了。
(5)JVM在它的生存周期中有一个明确的任务,那就是装载字节码文件,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行,即Java程序被执行。因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。
Class字节码
编译后被Java虚拟机所执行的代码使用了一种平台中立(不依赖于特定硬件及操作系统的)的二进制格式来表示,并且经常(但并非绝对)以文件的形式存储,因此这种格式被称为Class文件格式。Class文件格式中精确地定义了类与接口的表示形式,包括在平台相关的目标文件格式中一些细节上的惯例,
正如概念所说,Java为了能够实现平台无关性,制定了一套自己的二进制格式,并经常以文件的方式存储,称为Class文件。这样在不同平台上,只要都安装了Java虚拟机,具备Java运行环境[JRE],那么都可以运行相同的Class文件。
上图描述了Java程序运行的一个全过程,也可以看出Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。
由Java源文件编译生成字节码文件,这个过程非常复杂,学过《编译原理》的朋友都知道必须经过词法分析、语法分析、语义分析、中间代码生成、代码优化等;同样的,Java源文件到字节码的生成也想要经历这些步骤。Javac编译器的最后任务就是调用con.sun.tools.javac.jvm.Gen类将这课语法树编译为Java字节码文件。
其实,所谓的编译字节码,无非就是将符合Java语法规范的Java代码转化为符合JVM规范的字节码文件。JVM的架构模型是基于栈的,大部分都需要通过栈来完成。
字节码结构比较特殊,其内部不包含任何的分隔符,无法人工区分段落(字节码文件本身就是给机器读的),所以无论是字节顺序、数量都是有严格规定的,所有16位、32位、64位长度的数据都将构造成2个、4个、8个-----8位字节单位来表示,多字节数据项总是按照Big-endian顺序(高位字节在地址的最低位,地位字节在地址的最高位)来进行存储。
参考《Java虚拟机规范 Java SE7版》的描述,每一个字节码其实都对应着全局唯一的一个类或者接口的定义信息。字节码文件采用的是一种类似于C语言结构体的伪结构来描述字节码文件格式。字节码文件中对应的“基本类型”u1,u2,u4,u8分别表示无符号1、2、4、8个字节。
Class文件----总体格式
值得一提的是,一个有效的class字节码文件的前4个字节为0xCAFEBABE,都是固定的,被称为“魔术”,即magic。它就是JVM用于校验所读取的目标文件是否是一个有效且合法的字节码文件。由此可见,JVM并不是通过判断文件后缀名的方式来校验,以防止人为手动修改。
JVM底层架构图
上面这张图,是本人花了很多心思总结出来的,基本涵盖了java内存模型的结构。今天奉上。这篇文章会把上面这张图讲清楚。
运行时数据区:
1,堆
Java堆在虚拟机启动的时候被创建,Java堆主要用来为类实例对象和数组分配内存。Java虚拟机规范并没有规定对象在堆中的形式。
在Java中,堆被划分成两个不同的区域:新生代( Young )、老年代( Old );这也就是JVM采用的“分代收集算法”,简单说,就是针对不同特征的java对象采用不同的 策略实施存放和回收,自然所用分配机制和回收算法就不一样。新生代( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
分代收集算法:采用不同算法处理[存放和回收]Java瞬时对象和长久对象。大部分Java对象都是瞬时对象,朝生夕灭,存活很短暂,通常存放在Young新生代,采用复制算法对新生代进行垃圾回收。老年代对象的生命周期一般都比较长,极端情况下会和JVM生命周期保持一致;通常采用标记-压缩算法对老年代进行垃圾回收。
这样划分的目的是为了使JVM能够更好的管理堆内存中的对象,包括内存的分配以及回收。
Java堆可能发生如下异常情况:如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那Java虚拟机将会抛出一个OutOfMemoryError异常。简称(OOM)。
堆大小 = 新生代 + 老年代。堆的大小可通过参数–Xms(堆的初始容量)、-Xmx(堆的最大容量) 来指定。
其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。默认的,Edem : from : to = 8 : 1 : 1 。(可以通过参数 –XX:SurvivorRatio 来设定 。
即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
java堆是GC垃圾回收的主要区域。 GC分为两种: Minor GC、Full GC(也叫做Major GC)
Minor GC(简称GC)
Minor GC是发生在新生代中的垃圾收集动作, 所采用的是复制算法。
GC一般为堆空间某个区发生了垃圾回收,
新生代(Young)几乎是所有java对象出生的地方。即java对象申请的内存以及存放都是在这个地方。java中的大部分对象通常不会长久的存活, 具有朝生夕死的特点。
当一个对象被判定为“死亡”的时候, GC就有责任来回收掉这部分对象的内存空间。
新生代是收集垃圾的频繁区域。
2,方法区(元空间)
方法区在虚拟机启动的时候被创建,它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容、还包括在类、实例、接口初始化时用到的特殊方法。
方法区可能发生如下异常情况: 如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError异常.
3,JVM栈空间
每个Java虚拟机线程都有自己的Java虚拟机栈。Java虚拟机栈用来存放栈帧,而栈帧主要包括了:局部变量表、操作数栈、动态链接。Java虚拟机栈允许被实现为固定大小或者可动态扩展的内存大小。
Java虚拟机使用局部变量表来完成方法调用时的参数传递。局部变量表的长度在编译期已经决定了并存储于类和接口的二进制表示中,一个局部变量可以保存一个类型为boolean、byte、char、short、float、reference和returnAddress的数据,两个局部变量可以保存一个类型为long和double的数据。
Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果。
每个栈帧中都包含一个指向运行时常量区的引用支持当前方法的动态链接。在Class文件中,方法调用和访问成员变量都是通过符号引用来表示的,动态链接的作用就是将符号引用转化为实际方法的直接引用或者访问变量的运行是内存位置的正确偏移量。
总的来说,Java虚拟机栈是用来存放局部变量和过程结果的地方。
Java虚拟机栈可能发生如下异常情况: 如果Java虚拟机栈被实现为固定大小内存,线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出一个StackOverflowError异常。
如果Java虚拟机栈被实现为动态扩展内存大小,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。
1.符号引用(Symbolic References):
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
2.直接引用:
直接引用可以是
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
4,本地方法栈
对于一个运行中的Java程序而言,它还可能会用到一些跟本地方法相关的数据区。当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,但不止如此,它还可以做任何它想做的事情。
本地方法本质上是依赖于实现的,虚拟机实现的设计者们可以自由地决定使用怎样的机制来让Java程序调用本地方法。
任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。
如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。当C程序调用一个C函数时,其栈操作都是确定的。传递给该函数的参数以某个确定的顺序压入栈,它的返回值也以确定的方式传回调用者。同样,这就是虚拟机实现中本地方法栈的行为。
很可能本地方法接口需要回调Java虚拟机中的Java方法,在这种情况下,该线程会保存本地方法栈的状态并进入到另一个Java栈。
下图描绘了这样一个情景,就是当一个线程调用一个本地方法时,本地方法又回调虚拟机中的另一个Java方法。
这幅图展示了JAVA虚拟机内部线程运行的全景图。一个线程可能在整个生命周期中都执行Java方法,操作它的Java栈;或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。
该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。假设这是一个C语言栈,其间有两个C函数,第一个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过本地方法接口回调了一个Java方法(第三个Java方法),最终这个Java方法又调用了一个Java方法(它成为图中的当前方法)。
Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。
5,程序计数器
程序计数器是一个记录着当前线程所执行的字节码的行号指示器。
JAVA代码编译后的字节码在未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”进行解释执行。简单的工作原理为解释器读取装载入内存的字节码,按照顺序读取字节码指令。读取一个指令后,将该指令“翻译”成固定的操作,并根据这些操作进行分支、循环、跳转等流程。
从上面的描述中,可能会产生程序计数器是否是多余的疑问。因为沿着指令的顺序执行下去,即使是分支跳转这样的流程,跳转到指定的指令处按顺序继续执行是完全能够保证程序的执行顺序的。假设程序永远只有一个线程,这个疑问没有任何问题,也就是说并不需要程序计数器。但实际上程序是通过多个线程协同合作执行的。
首先我们要搞清楚JVM的多线程实现方式。JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,在JVM中,通过程序计数器来记录某个线程的字节码执行位置。因此,程序计数器是具备线程隔离的特性,也就是说,每个线程工作时都有属于自己的独立计数器。
程序计数器的特点
1.线程隔离性,每个线程工作时都有属于自己的独立计数器。
2.执行java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址(参考上一小节的描述)。
3.执行native本地方法时,程序计数器的值为空(Undefined)。因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。
4.程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。
5.程序计数器,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。
6,线程栈
线程堆栈也称线程调用堆栈,是虚拟机中线程(包括锁)状态的一个瞬间状态的快照,即系统在某一个时刻所有线程的运行状态,包括每一个线程的调用堆栈,锁的持有情况。虽然不同的虚拟机打印出来的格式有些不同,但是线程堆栈的信息都包含:
1、线程名字,id,线程的数量等。
2、线程的运行状态,锁的状态(锁被哪个线程持有,哪个线程在等待锁等)
3、调用堆栈(即函数的调用层次关系)调用堆栈包含完整的类名,所执行的方法,源代码的行数。
因为线程栈是瞬时快照包含线程状态以及调用关系,所以借助堆栈信息可以帮助分析很多问题,比如线程死锁,锁争用,死循环,识别耗时操作等等。线程栈是瞬时记录,所以没有历史消息的回溯,一般我们都需要结合程序的日志进行跟踪,一般线程栈能分析如下性能问题:
1、系统无缘无故的cpu过高
2、系统挂起,无响应
3、系统运行越来越慢
4、性能瓶颈(如无法充分利用cpu等)
5、线程死锁,死循环等
6、由于线程数量太多导致的内存溢出(如无法创建线程等)
线程栈状态
线程栈状态有如下几种
1、NEW
2、RUNNABLE
3、BLOCKED
4、WAITING
5、TIMED_WAITING
6、TERMINATED
下面依次对6种线程栈状态进行介绍。
1、NEW
线程刚刚被创建,也就是已经new过了,但是还没有调用start()方法,这个状态我们使用jstack进行线程栈dump的时候基本看不到,因为是线程刚创建时候的状态。
2、RUNNABLE
从虚拟机的角度看,线程正在运行状态,状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等。
处于RUNNABLE状态的线程是不是一定会消耗cpu呢,不一定,像socket IO操作,线程正在从网络上读取数据,尽管线程状态RUNNABLE,但实际上网络io,线程绝大多数时间是被挂起的,只有当数据到达后,线程才会被唤起,挂起发生在本地代码(native)中,虚拟机根本不一致,不像显式的调用sleep和wait方法,虚拟机才能知道线程的真正状态,但在本地代码中的挂起,虚拟机无法知道真正的线程状态,因此一概显示为RUNNABLE。
3、BLOCKED
线程处于阻塞状态,正在等待一个monitor lock。通常情况下,是因为本线程与其他线程共用了一个锁。其他在线程正在使用这个锁进入某个synchronized同步方法块或者方法,而本线程进入这个同步代码块也需要这个锁,最终导致本线程处于阻塞状态。
真实生活例子:
今天你要去阿里面试。这是你梦想的工作,你已经盯着它多年了。你早上起来,准备好,穿上你最好的外衣,对着镜子打理好。当你走进车库发现你的朋友已经把车开走了。在这个场景,你只有一辆车,所以怎么办?在真实生活中,可能会打架抢车。 现在因为你朋友把车开走了你被BLOCKED了。你不能去参加面试。
这就是BLOCKED状态。用技术术语讲,你是线程T1,你朋友是线程T2,而锁是车。T1被BLOCKED在锁(例子里的车)上,因为T2已经获取了这个锁。
4、WAITING
这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在理解点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束,处于waiting状态的线程基本不消耗CPU。
真实生活例子:
再看下几分钟后你的朋友开车回家了,锁(车)就被释放了,现在你意识到快到面试时间了,而开车过去很远。所以你拼命地踩油门。限速120KM/H而你以160KM/H的速度在开。很不幸,一个交警发现你超速了,让你停到路边。现在你进入了WAITING状态。你停下车坐在那等着交警过来检查开罚单然后给你放行。基本上,你只有等他让你走(你没法开车逃),你被卡在WAITING状态了。
用技术术语来讲,你是线程T1而交警是线程T2。你释放你的锁(例子中你停下了车),并进入WAITING状态,直到警察(例子中T2)让你走,你陷入了WAITING状态。
5、TIMED_WAITING
该线程正在等待,通过使用了 sleep, wait, join 或者是 park 方法。(这个与 WAITING 不同是通过方法参数指定了最大等待时间,WAITING 可以通过时间或者是外部的变化解除),线程等待指定的时间。
真实生活例子:
尽管这次面试过程充满戏剧性,但你在面试中做的非常好,惊艳了所有人并获得了高薪工作。你回家告诉你的邻居你的新工作并表达你激动的心情。你的朋友告诉你他也在同一个办公楼里工作。他建议你坐他的车去上班。你想这不错。所以去阿里上班的第一天,你走到你邻居的房子,在他的房子前停好你的车。你等了他10分钟,但你的邻居没有出现。你然后继续开自己的车去上班,这样你不会在第一天就迟到。这就是TIMED_WAITING.
用技术术语来解释,你是线程T1而你的邻居是线程T2。你释放了锁(这里是停止开车)并等了足足10分钟。如果你的邻居T2没有来,你继续开车(老司机注意车速,其他乘客记得买票)。
6、TERMINATED
线程终止,同样我们在使用jstack进行线程dump的时候也很少看到该状态的线程栈。
1.局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。
在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。(最大Slot数量)
一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference和returnAddress类型的数据。reference类型表示对一个对象实例的引用。returnAddress类型是为jsr、jsr_w和ret指令服务的,目前已经很少使用了。
虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。如果Slot是32位的,则遇到一个64位数据类型的变量(如long或double型),则会连续使用两个连续的Slot来存储。
2.操作数栈
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。
操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。
当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。
3.动态连接
在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。
这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。
4.静态链接
静态链接的过程就已经把要链接的内容已经链接到了生成的可执行文件中,就算你再去把静态库删除也不会影响可执行程序的执行;而动态链接这个过程却没有把内容链接进去,而是在执行的过程中,再去找要链接的内容,生成的可执行文件中并没有要链接的内容,所以当你删除动态库时,可执行程序就不能运行。
通俗解释:静态连接库就是把(lib)文件中用到的函数代码直接链接进目标程序,程序运行的时候不再需要其它的库文件;动态链接就是把调用的函数所在文件模块(DLL)和调用函数在文件中的位置等信息链接进目标程序,程序运行的时候再从DLL中寻找相应函数代码,因此需要相应DLL文件的支持。
转载:https://www.cnblogs.com/tomakemyself/p/14147989.html
评论