乐趣区

关于spring:JVM的艺术JAVA内存模型

喜爱文章,动动手指导个赞

引言

敬爱读者你们好,对于 jvm 篇章的连载,后面三章讲了类加载器,本篇文章将进入 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, 而锁是车。T1BLOCKED 在锁(例子里的车)上,因为 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 文件的反对。

这篇内容次要介绍一下图中的概念。下篇文章我会把这些概念串起来,比如说创建对象的过程,内存空间是怎么工作的。感激大家的继续关注。

另外我在我的公众号内,针对 JVM 写了一个系列介绍内容,想要获取更多内容,请关注公众号:奇客工夫

退出移动版