一、运行时数据区
1.1 概述
内存是十分重要的系统资源,是硬盘和 CPU 的两头仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM 内存布局规定了 Java 在运行过程中内存申请、调配、治理的策略,保障了 JVM 的高效稳固运行。不同的 JVM 对于内存的划分形式和管理机制存在着局部差别。联合 JVM 虚拟机标准,来探讨一下经典的 JVM 内存布局。
JVM 内存共分为 本地办法栈、程序计数器、虚拟机栈、堆、办法区 五个局部。这些区域有各自的用处和创立与销毁的工夫。有的区域随着虚拟机过程的启动而始终存在,有些区域则是依赖用户线程的启动和完结而建设和销毁。
在上图中,灰色局部为线程隔离的数据区域,其余局部为线程共享的区域。
运行时数据区 | 是否线程共享 | 是否存在内存溢出 | 是否存在 GC |
---|---|---|---|
本地办法栈 | 否 | 是 | 否 |
虚拟机栈 | 否 | 是 | 否 |
程序计数器 | 否 | 否 | 否 |
堆区 | 是 | 是 | 是 |
办法区 | 是 | 是 | 是 |
1.2 JVM 零碎线程
JVM 容许一个利用有多个线程 并行 的执行。在 Hotspot JVM 里,每个线程都与操作系统的本地线程间接映射。
- 当一个 Java 线程筹备好执行当前,此时一个操作系统的本地线程也同时创立。Java 线程执行终止后,本地线程也会回收。操作系统负责所有线程的安顿调度到任何一个可用的 CPU 上。一旦本地线程初始化胜利,它就会调用 Java 线程中的 run()办法。如果应用
jconsole
或者是其余调试工具,都能看到在后盾有许多线程在运行。这些后盾线程不包含调用public static void main(String[] args)
的 main 线程以及所有 main 线程创立的线程。
这些后盾零碎线程在 Hotspot JVM 里次要是以下几个:
- 虚拟机线程:这种线程的操作是须要 JVM 达到平安点才会呈现。这些操作必须在不同的线程中产生的起因是他们都须要 JVM 达到平安点,这样堆才不会变动。这种线程的执行类型包含 ”stop-the-world” 的垃圾收集,线程栈收集,线程挂起以及偏差锁撤销。
- 周期工作线程:这种线程是工夫周期事件的体现(比方中断),他们个别用于周期性操作的调度执行。
- GC 线程:这种线程对在 JVM 里不同品种的垃圾收集行为提供了反对。
- 编译线程:这种线程在运行时会将字节码编译成到本地代码。
- 信号调度线程:这种线程接管信号并发送给 JVM,在它外部通过调用适当的办法进行解决。
二、程序计数器
2.1 概述
程序计数器 (Program Counter Register)是一块较小的内存空间,是 运行速度最快 的存储区域。它能够看作是以后线程所执行的字节码的行号指示器。Register 的命名源于 CPU 的寄存器,存储指令相干的现场信息。这里,并非指狭义上所指的物理寄存器,或者将其翻译为PC 寄存器(或指令计数器)会更加贴切(也称为程序钩子)。JVM 中的程序计数器是对物理 PC 寄存器的一种形象模仿。
2.2 作用
程序计数器用来寄存下一条指令的地址(将要执行的字节码指令地址)。在 JVM 标准中,每个线程都有它本人的程序计数器,是线程公有的,生命周期与线程的生命周期保持一致。
任何工夫一个线程都只有一个办法在执行,也就是所谓的以后办法。程序计数器会存储以后线程正在执行的 Java 办法的 JVM 指令地址;或者,如果是在执行 native 办法,则是未指定值(undefned)。
它是程序控制流的指示器,分支、循环、跳转、异样解决、线程复原等根底性能都须要依赖这个计数器来实现。字节码解释器工作时就是通过扭转这个计数器的值来选取下一条须要执行的字节码指令。
程序计数器中不存在内存溢出。
代码演示
public class PCRegisterTest {public static void main(String[] args) {
int i = 10;
int j = 20;
int k = i + j;
}
}
而后将代码进行编译成字节码文件,查看 发现在字节码的右边有一个行号标识,它其实就是 指令地址。
0 bipush 10
2 istore_1
3 bipush 20
5 istore_2
6 iload_1
7 iload_2
8 iadd
9 istore_3
10 return
通过程序计数器,咱们就能够晓得以后程序执行到哪一步了。
应用程序计数器存储字节码指令地址有什么用呢?
因为 CPU 须要不停的切换各个线程,在线程切换回来当前,就得晓得接着从哪开始继续执行。JVM 的字节码解释器就须要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令。
PC 寄存器为什么被设定为线程公有的?
多线程在一个特定的时间段内只会执行其中某一个线程的办法,CPU 会不停地做工作切换,这样必然导致常常中断或复原,如何保障分毫无差呢?为了可能精确地记录各个线程正在执行的以后字节码指令地址,最好的方法天然是为每一个线程都调配一个 PC 寄存器,这样一来各个线程之间便能够进行独立计算,从而不会呈现互相烦扰的状况。
因为 CPU 工夫片 轮限度,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
这样必然导致常常中断或复原,如何保障分毫无差呢?每个线程在创立后,都会产生本人的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
CPU 工夫片
CPU 工夫片即 CPU 调配给各个程序的工夫,每个线程被调配一个时间段,称作它的工夫片。
在宏观上:能够同时关上多个应用程序,每个程序并行不悖,同时运行。
但在宏观上:一个 CPU 一次只能处理程序要求的一部分。如何解决偏心,一种办法就是引入工夫片,每个程序轮流执行。
三、虚拟机栈
3.1 概述
因为跨平台性的设计,Java 的指令都是依据栈来设计的(因为不同平台 CPU 架构不同,所以基于寄存器设计)。
- 长处:跨平台,指令集小,编译器容易实现。
- 毛病:性能降落,实现同样的性能的指令更多。
常有人把 Java 内存区域抽象地划分为堆内存(Heap)和栈内存(Stack),这种划分形式间接继承自传统的 C、C++ 程序的内存布局构造,在 Java 语言里就显得有些毛糙了,理论的内存区域划分要比这更简单。“栈”通常就是指这里讲的虚拟机栈,能够明确的是——栈是运行时的单位,而堆是存储的单位。
- 栈解决程序的运行问题,即程序如何执行,或者说如何解决数据。
- 堆解决的是数据存储的问题,即数据怎么放,放哪里。
那么 Java 虚拟机栈又是什么
每个 Java 虚拟机线程都有一个公有的Java 虚拟机栈(Java Virtual Machine Stack),与该线程同时创立,其外部保留一个个的栈帧(Stack Frame),对应着每一次的 Java 办法调用。其生命周期和线程保持一致。
作用
主管 Java 程序的运行,它保留办法的局部变量(8 种根本数据类型、对象的援用地址)、局部后果,并参加办法的调用和返回。
局部变量,它是相比于成员变量来说的(或属性)
根本数据类型变量 VS 援用类型变量(类、数组、接口)
栈的特点
栈是一种疾速无效的调配存储形式,访问速度仅次于程序计数器。JVM 间接对 Java 栈的操作只有两个:
- 每个办法执行,随同着压栈(入栈、进栈)
- 执行完结后的出栈工作
对于栈来说 不存在垃圾回收的问题(存在内存溢出的问题)。
与 Java 虚拟机栈相干异样
如果采纳固定大小的 Java 虚拟机栈,那每一个线程的 Java 虚拟机栈容量能够在线程创立的时候独立选定。如果线程申请调配的栈容量超过 Java 虚拟机栈容许的最大容量,Java 虚拟机将会抛出 StackOverflowError 异样。
如果 Java 虚拟机栈能够动静扩大,并且在尝试扩大的时候无奈申请到足够的内存,或者在创立新的线程时没有足够的内存去创立对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异样。
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {System.out.println(count++);
main(args);// Exception in thread "main" java.lang.StackOverflowError 谬误
}
}
设置栈内存大小
咱们能够应用参数 -Xss
选项来设置线程的最大栈空间,栈的大小间接决定了函数调用的最大可达深度。
-Xss1m
-Xss1k
3.2 栈的存储单位
(1)栈帧
每个线程都有本人的栈,栈中的数据都是以 栈帧(Stack Frame)的格局存在。线程上正在执行的每个办法都各自对应一个栈帧(Stack Frame)。栈帧是一个内存区块,是一个数据集,维系着办法执行过程中的各种数据信息。
OOP 的基本概念:类和对象
类中根本构造:field(属性、字段、域)、method
JVM 间接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出 / 后进先出”准则。
在一条流动线程中,一个工夫点上,只会有一个流动的栈帧。即只有以后正在执行的办法的栈帧(栈顶栈帧)是无效的,这个栈帧被称为以后栈帧(Current Frame),与以后栈帧绝对应的办法就是以后办法(Current Method),定义这个办法的类就是以后类(Current Class)。
执行引擎运行的所有字节码指令只针对以后栈帧进行操作。如果在该办法中调用了其余办法,对应的新的栈帧会被创立进去,放在栈的顶端,成为新的以后帧。
例子:
public class StackFrameTest {public static void main(String[] args) {method01();
}
private static int method01() {System.out.println("办法 1 的开始");
int i = method02();
System.out.println("办法 1 的完结");
return i;
}
private static int method02() {System.out.println("办法 2 的开始");
int i = method03();
System.out.println("办法 2 的完结");
return i;
}
private static int method03() {System.out.println("办法 3 的开始");
int i = 30;
System.out.println("办法 3 的完结");
return i;
}
}
输入后果为
办法 1 的开始
办法 2 的开始
办法 3 的开始
办法 3 的完结
办法 2 的完结
办法 1 的完结
满足栈先进后出的概念,通过 DEBUG,也可能看到栈相干信息:
(2)栈运行原理
不同线程中所蕴含的栈帧是不容许存在互相援用的,即不可能在一个栈帧之中援用另外一个线程的栈帧。
如果以后办法调用了其余办法,办法返回之际,以后栈帧会传回此办法的执行后果给前一个栈帧,接着,虚构机会抛弃以后栈帧,使得前一个栈帧从新成为以后栈帧。
Java 办法有两种返回函数的形式,一种是失常的函数返回,应用 return 指令;另外一种是 抛出异样 。不论应用哪种形式, 都会导致栈帧被弹出。
(3)栈帧的内部结构
每个栈帧中存储着:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)(或表达式栈)
- 动静链接(Dynamic Linking)(或指向运行时常量池的办法援用)
- 办法返回地址(Return Address)(或办法失常退出或者异样退出的定义)
- 一些附加信息
并行每个线程下的栈都是公有的,因而每个线程都有本人各自的栈,并且每个栈外面都有很多栈帧,栈帧的大小次要由局部变量表和操作数栈决定的。
3.3 局部变量表
(1)概述
局部变量表(Local Variables),又称局部变量数组或本地变量表,是一个 数字数组 ,次要用于存储 办法参数 和定义在办法体内的 局部变量,这些数据类型包含根本数据类型(8 种)、对象援用(reference),以及 returnAddress(指向了一条字节码指令的地址)类型。
因为局部变量表是建设在线程的栈上,是线程的公有数据,因而 不存在数据安全问题。
局部变量表所需的容量大小是在编译期确定下来的,并保留在办法的 Code 属性的 Maximum local variables 数据项中。在办法运行期间是不会扭转局部变量表的大小的。
例如下面案例的办法 1:
办法嵌套调用的次数由栈的大小决定 。一般来说, 栈越大,办法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表收缩,它的栈帧就越大,以满足办法调用所需传递的信息增大的需要。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会缩小。
局部变量表中的变量只在以后办法调用中无效 。在办法执行时,虚拟机通过应用局部变量表实现参数值到参数变量列表的传递过程。 当办法调用完结后,随着办法栈帧的销毁,局部变量表也会随之销毁。
(2)对于 Slot 的了解
参数值的寄存总是在局部变量数组的 index0 开始,到数组长度 - 1 的索引完结。
局部变量表,最根本的存储单元是 Slot(变量槽)。局部变量表中寄存编译期可知的根本数据类型、援用类型、returnAddress 类型的变量。
在局部变量表里,32 位以内的类型只占用一个 slot(包含 returnAddress 类型),64 位的类型 (long 和 double) 占用两个 slot。
byte、short、char、boolean 在存储前被转换为 int。0 示意 false,非 0 示意 true。
JVM 会为局部变量表中的每一个 Slot 都调配一个拜访索引,通过这个索引即可胜利拜访到局部变量表中指定的局部变量值。
当一个实例办法被调用的时候,它的办法参数和办法体外部定义的局部变量将会依照程序被复制到局部变量表中的每一个 slot 上。如果须要拜访局部变量表中一个 64 位的局部变量值时,只须要应用前一个索引即可。(比方:拜访 long 或 doub1e 类型变量)
如果以后帧是由构造方法或者实例办法创立的,那么该对象援用 this 将会寄存在 index 为 0 的 slot 处,其余的参数依照参数表程序持续排列。
Slot 的反复利用
栈帧中的局部变量表中的槽位是能够重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的部分变就很有可能会复用过期局部变量的槽位,从而达到节俭资源的目标。
(3)动态变量与局部变量的比照
变量的分类:
- 按数据类型分:根本数据类型、援用数据类型
-
按类中申明的地位分:成员变量(类变量,实例变量)、局部变量
- 类变量:Linking 的 Prepare 阶段,给类变量默认赋值,initial 阶段给类变量显示赋值即动态代码块。
- 实例变量:随着对象创立,会在堆空间中调配实例变量空间,并进行默认赋值。
- 局部变量:在应用前必须进行显式赋值,不然编译不通过。
参数表调配结束之后,再依据办法体内定义的变量的程序和作用域调配。
咱们晓得类变量表有两次初始化的机会,第一次是在“筹备阶段 ”,执行零碎初始化,对类变量设置零值,另一次则是在“ 初始化”阶段,赋予程序员在代码中定义的初始值。
和类变量初始化不同的是,局部变量表不存在零碎初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无奈应用。
public void test(){
int i;
System.out.println(i);// 报错,局部变量没有赋值不能应用。}
在栈帧中,与性能调优关系最为亲密的局部就是后面提到的局部变量表。在办法执行时,虚拟机应用局部变量表实现办法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只有被局部变量表中间接或间接援用的对象都不会被回收。
3.4 操作数栈
(1)概述
每一个独立的栈帧除了蕴含局部变量表以外,还蕴含一个后进先出(Last – In – First -Out)的 操作数栈 ,也能够称之为 表达式栈(Expression Stack)。
操作数栈,在办法执行过程中,依据字节码指令,往栈中写入数据或提取数据,即 入栈(push)/ 出栈(pop)。
- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。应用它们后再把后果压入栈
- 比方:执行复制、替换、求和等操作
操作数栈,次要用于保留计算过程的两头后果,同时作为计算过程中变量长期的存储空间。
操作数栈就是 JVM 执行引擎的一个工作区,当一个办法刚开始执行的时候,一个新的栈帧也会随之被创立进去,这个办法的操作数栈是 空的。
这个时候数组是有长度的,数组一旦创立,长度是不可变的。
每一个操作数栈都会领有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保留在办法的 Code 属性中,为 maxstack 的值。
栈中的任何一个元素都是能够任意的 Java 数据类型
- 32bit 的类型占用一个栈单位深度
- 64bit 的类型占用两个栈单位深度
操作数栈 并非采纳拜访索引的形式来进行数据拜访,而是只能通过规范的入栈和出栈操作来实现一次数据拜访。
如果被调用的办法带有返回值的话,其返回值将会被压入以后栈帧的操作数栈中,并更新 PC 寄存器中下一条须要执行的字节码指令。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类测验阶段的数据流分析阶段要再次验证。|
另外,咱们说 Java 虚拟机的 解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
(2)代码追踪
以上面代码为例子:
public void testAddOperation() {
byte i = 15;
int j = 8;
int k = i + j;
}
应用 javap 命令反编译 class 文件:javap -v 类名.class
0 bipush 15
2 istore_1
3 bipush 8
5 istore_2
6 iload_1
7 iload_2
8 iadd
9 istore_3
10 return
从下面的代码咱们能够晓得,咱们都是通过 bipush
对操作数 15 和 8 进行入栈操作,同时应用的是 iadd 办法进行相加操作,i
-> 代表的是 int
类型的加法操作。
Tips:byte、short、char、boolean 外部都是应用 int 型来进行保留的。
执行流程如下所示:
首先执行第一条语句,PC 寄存器指向的是 0,也就是指令地址为 0,而后应用 bipush 让操作数 15 入栈。
执行完后,让 PC + 1,指向下一行代码,下一行代码就是将操作数栈的元素存储到局部变量表 1 的地位,咱们能够看到局部变量表的曾经减少了一个元素。
为什么局部变量表不是从 0 开始的呢?
局部变量表也是从 0 开始的,然而因为 0 号地位存储的是 this 指针,这里省略书写了。
而后 PC+1,指向的是下一行。让操作数 8 也入栈,同时执行 store 操作,存入局部变量表中。
而后从局部变量表中,顺次将数据放在操作数栈中。
而后将操作数栈中的两个元素执行相加操作,并存储在局部变量表 3 的地位。
最初 PC 寄存器的地位指向 10,也就是 return 办法,则间接退出办法。
(3)栈顶缓存技术
栈顶缓存技术:Top Of Stack Cashing
基于栈式架构的虚拟机所应用的零地址指令更加紧凑,但实现一项操作的时候必然须要应用更多的入栈和出栈指令,这同时也就意味着将须要更多的指令分派(instruction dispatch)次数和内存读 / 写次数。
因为操作数是存储在内存中的,因而频繁地执行内存读 / 写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM 的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全副缓存在物理 CPU 的寄存器中,以此升高对内存的读 / 写次数,晋升执行引擎的执行效率。
3.5 动静链接
每一个栈帧外部都蕴含一个指向 运行时常量池中该栈帧所属办法的援用 。蕴含这个援用的目标就是为了反对以后办法的代码可能实现 动静链接(Dynamic Linking)。比方 invokedynamic 指令。
在 Java 源文件被编译到字节码文件中时,所有的变量和办法援用都作为符号援用(Symbolic Reference)保留在 class 文件的常量池里。比方:形容一个办法调用了另外的其余办法时,就是通过常量池中指向办法的符号援用来示意的,那么 动静链接的作用就是为了将这些符号援用转换为调用办法的间接援用。
为什么须要运行时常量池?因为在不同的办法,都可能调用常量或者办法,所以只须要存储一份即可,节俭了空间。
常量池的作用:就是为了提供一些符号和常量,便于指令的辨认。
3.6 办法返回地址
当一个办法开始执行后,只有两种形式能够退出这个办法:
- 失常执行实现
- 呈现未解决的异样
执行引擎遇到任意一个办法返回的字节码指令(return),会有返回值传递给下层的办法调用者,简称“失常调用实现”(Normal Method Invocation Completion):
- 一个办法在失常调用实现之后,到底须要应用哪一个返回指令,还须要依据办法返回值的理论数据类型而定。
- 在字节码指令中,返回指令蕴含 ireturn(当返回值是 boolean,byte,char,short 和 int 类型时应用),lreturn(Long 类型),freturn(Float 类型),dreturn(Double 类型),areturn。另外还有一个 return 指令申明为 void 的办法,实例初始化办法,类和接口的初始化办法应用。
在办法执行过程中遇到异样(Exception),并且这个异样没有在办法内进行解决,也就是只有在本办法的异样表中没有搜寻到匹配的异样处理器,就会导致办法退出,这种退出办法的形式称为“异样调用实现“(Abrupt MethodInvocation Completion)。
无论通过哪种形式退出,在办法退出后都返回到该办法被调用的地位。办法失常退出时,调用者的 PC 寄存器的值作为返回地址,即调用该办法的指令的下一条指令的地址。而通过异样退出的,返回地址是要通过异样表来确定,栈帧中个别不会保留这部分信息。
实质上,办法的退出就是以后栈帧出栈的过程。此时,须要复原下层办法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置 PC 寄存器值等,让调用者办法继续执行上来。
失常实现进口和异样实现进口的区别在于:通过异样实现进口退出的不会给他的下层调用者产生任何的返回值。
3.7 附加信息
《Java 虚拟机标准》容许虚拟机实现减少一些标准里没有形容的信息到栈帧之中,例如与调试、性能收集相干的信息,这部分信息齐全取决于具体的虚拟机实现,这里不再详述。
参考
深刻了解 Java 虚拟机:JVM 高级个性与最佳实际(第 3 版)
运行时数据区 Oracle 官网介绍