目录
1、概述
2、线程
3、PC寄存器(程序计数器)
4、虚拟机栈
4.1、概述
4.2、栈的存储单位
4.2.1、局部变量表(local variables)
4.2.2、操作数栈(表达式栈、Operand Stack)
4.2.3、动态链接(指向运行时常量池的方法引用)
4.2.4、方法返回地址(方法正常退出或者异常退出的定义)
4.2.5、附加信息
5、本地方法栈
5.1、概述
5.2、本地方法库
5.3、本地方法栈
6、面试问题
PDF版笔记:JVM的学习笔记PDF版-互联网文档类资源-CSDN下载
1、概述
与进程同在(即线程公共区):方法区、堆、堆外内存(永久代或元空间、代码缓存)
与线程同在(即线程私有区):虚拟机栈、PC寄存器、本地方法栈
Runtime:一个JVM只有一个Runtime
2、线程
- 线程是程序的运行单元,JVM允许一个应用有多个线程并行执行。
- 在Hotspot JVM中,每个线程都与操作系统的本地线程直接映射。当一个Java线程准备好执行以后,此时,一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收。
- 操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run()方法
- 守护线程、普通线程
3、PC寄存器(程序计数器)
英文:Program Counter Register
JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟
作用:PC寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。
特点:
- 内存空间小到几乎可以忽略不计,也是运行速度最快的存储区域
- 在JVM规范中,每一个线程都有属于它自己的PC寄存器,是线程私有的,生命周期与线程生命周期保持一致
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。PC寄存器会存储当前线程正在执行的Java方法的JVM指令地址;如果是在执行native方法,则是未指定指令(undefned)
- 它是程序控制流的指示器,字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
- 它是唯一一个在JVM规范中没有规定任何OOM(内存溢出)的区域。同时,也没有GC(垃圾回收)
面试问题:
Q:使用PC寄存器存储字节码指令地址有什么用?/为什么使用PC寄存器记录当前线程的执行地址呢?
A:因为CPU在执行多条线程的时候,需要在不同的线程之间转换。当转换到一条线程时,需要通过PC寄存器知道该线程下一条需要执行的字节码指令。
Q:PC寄存器为什么会被设定为线程私有的?
A:为了能够准确记录各个线程正在执行的当前字节码指令地址,最好的方法就是为每一个线程分配一个PC寄存器,这样每个线程之间才能进行独立计算。
4、虚拟机栈
4.1、概述
基于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台的CPU架构不同,所以不能设计为基于寄存器架构的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现相同的功能需要更多的指令。
栈是运行时的单位,堆时存储时的单位
Java虚拟机栈是什么?
- Java 虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧(Stack Frame),对应一次次的Java方法调用。是线程私有的,生命周期与线程保持一致
- 作用:主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回
优点:
- 栈是一种快速有效的分配存储方式,访问速度仅次于PC寄存器
- JVM直接对Java栈的操作只有两个:方法执行的入栈,方法执行结束后的出栈
- 对于栈来说,不存在垃圾回收(GC)问题,但是存在内存溢出(OOM)问题
栈中可能出现的异常:
- JVM规范允许JVM栈的大小是动态或者是固定不变的
- 如果采用固定大小的JVM栈,那么每个线程的JVM栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超出JVM栈允许的最大容量,JVM将会抛出栈溢出(StackOverflowError)异常
- 如果JVM栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的JVM栈,那么JVM将会抛出一个内存溢出(OutOfMemoryError )异常
4.2、栈的存储单位
栈中存储什么:
- 栈里面的数据都是以栈帧的格式存在
- 线程上每个正在执行的方法都对应一个栈帧
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
栈运行原理:
- JVM直接对栈的操作只有两个,出栈、入栈,遵循先进后出,后进先出的原则
- 一条活动的线程中,一个时间点上,只会有一个活动的栈帧,即当前栈帧,其对应的方法为当前方法,定义这个方法的类就是当前类
- 执行引擎的所有字节码指令只对当前方法有效,调用方法时压入栈帧,方法结束时弹出栈帧
- 不同线程所包含的栈帧是不允许相互引用的
- Java方法有两种返回函数的方式:正常的使用return指令返回;抛出异常。不管哪一种方式,都会导致栈帧被弹出。
栈帧的内部结构:局部变量表、操作数栈、动态链接、方法返回地址、附件信息
4.2.1、局部变量表(local variables)
- 定义为一个数字数组,主要用于存储方法参数和定义在本方法体内部的局部变量,这些数据类型包括基本数据类型、对象引用以及返回地址(returnAddress)类型。
- 由于是线程私有数据,不存在数据安全问题。
- 局部变量表所需的容量大小是在编译期确定下来的,并保存在Code属性的maxumun local variable数据项中。在方法运行期间是不会改变局部变量表的大小的。
- 方法嵌套调用的次数由栈的大小决定。
- 局部变量表中的变量只在当前方法调用中有效。
关于Slot的理解
- 局部变量表最基本的存储单位就是Slot(槽)
- 32位以内的类型只占一个slot,64位的类型占用两个slot
- JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个访问索引,即可成功访问到局部变量表中指定的局部变量
- 局部变量按照顺序被复制到局部变量表中的每一个Slot上
- 如果访问的是64bit的局部变量值时,访问其前置索引即可
- 如果当前方法是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的Slot处,其余参数照常排序放置
- Slot重复利用,如果一个局部变量过了其作用域,那么在其之后申明的局部变量就很可能复用过期局部变量的槽位,以达到节省资源的目的
静态变量与局部变量的对比:局部变量没有默认值,必须要显示赋值;成员变量和静态变量有默认值。
补充说明:
- 在栈帧中,与性能调优最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
4.2.2、操作数栈(表达式栈、Operand Stack)
- 操作数栈在方法执行过程中,根据字节码指令,进行入栈、出栈操作
- 主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间
- 操作数栈是由数组结构实现的,有确定的长度,其长度在编译期就定义好了,保存在Code属性中,为max_stack的值
- 32bit的占一个栈单位,64bit的占用两个栈单位
- 如果调用的方法带有返回值,其返回值将会被压入下一个栈帧的操作数栈中,并更新PC寄存器中下一条要执行的字节码指令
栈顶缓存技术:由于JVM基于栈式架构开发,导致其需要更多的指令分派次数和内存读写次数,其影响程序执行速度。通过栈顶缓存技术,即将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读写次数,提高执行引擎的执行效率。
4.2.3、动态链接(指向运行时常量池的方法引用)
- 每一个栈帧中都包含一个指向运行时常量池中该栈帧所属方法的引用。包含该引用的目的就是为了支持当前方法的代码能够实现动态链接。
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池中。动态链接的作用就是为了将这些符号引用转化为调用方法的直接引用
方法的调用
在JVM中,将符号引用转化为调用方法的直接引用与方法的绑定机制相关
静态链接:当字节码文件被转载进JVM的内部时,如果被调用的目标方法在编译期可知,且运行期保存不变。这种情况下将调用的方法的符号引用转化为直接引用的过程称之为静态链接。
动态链接:当字节码文件被转载进JVM的内部时,如果被调用的目标方法在编译期无法确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
方法的绑定
绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次
早期绑定:被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法,因此就可以使用静态链接的方法将符号引用转换为直接引用
晚期绑定:如果被调用的方法在编译期无法被确定下来,只能在程序运行期根据实际的类型绑定相关的方法,这种绑定方式被称之为晚期绑定。(Java语言的多态性的原理)
非虚方法:如何在编译期就确定了静态的调用版本,这个版本在运行时是不可变的这样的方法被称为非虚方法。如:静态方法、私有方法、final方法、实例构造器、父类方法
虚方法:其余方法被称为虚方法。
invokedynamic指令:使Java语言具备了一定的动态性语言的特性。主要由Java 8中的Lambda表达式展现。有利于其在JVM上运行动态性的语言。
方法重写的本质:
- 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C
- 如果在类型C中找到与常量中描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接语言,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常(访问权限不足异常)
- 如果没有找到对应方法,则按照继承关系从下往上依次对C的各个父类进行第二步的操作。
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常(调用虚拟方法异常)
虚方法表:在面向对象的编程中,会频繁使用动态分派,多次的查找会影响执行效率。为了提高性能,JVM采用在类的方法区建立一个虚方法白哦来实现索引表来代替查找。虚方法表在类加载的链接阶段解析环节被创造并开始初始化。
4.2.4、方法返回地址(方法正常退出或者异常退出的定义)
- 存放调用该方法的PC寄存器的值(即从被调用的方法哪个地方跳转出的)
- 异常退出则不会存在该值
4.2.5、附加信息
- 栈帧中还允许携带JVM实现相关的一些附加信息。例如对程序调试提高支持的信息。
5、本地方法栈
5.1、概述
5.2、本地方法库
定义:那些被Java调用,而不是通过Java实现的方法被称为本地方法
作用:
- 有时Java应用需要与Java外面环境交互,这是本地方法存在的主要原因
- 与操作系统交互
- 融合其余语言的方法为Java所用
- Sun‘s Java:Sun的解释器是使用C实现的,这使得它能够像普通的C一样与外部交互
- 现状:处理与硬件有关的应用,目前该方法使用得越来越少了
实现:一个方法被native修饰,则其为本地方法。其没有显示方法体,但是其不是虚拟方法。可以用除abstract以外的标识符连用。
5.3、本地方法栈
定义:JVM栈用于管理Java方法的调用,本地方法栈就是管理本地方法的JVM栈。
- 本地方法栈为线程私有
- 可以为固定大小,也可以为动态大小
- 异常和CG与栈相同
- 被调用的本地方法与JVM同级,所有JVM不能限制本地方法,与JVM权限相同。
- JVM规范并没有规定JVM实现使用特定的语言,所有其余的JVM不一定支持本地方法栈,该处特指Hotspot VM
6、面试问题
Q:举例栈溢出的情况
A:如果栈为指定的大小,当压入的栈帧超过栈的内存空间时,即会发生栈溢出。如果栈为动态大小,当压入栈帧时,整个JVM的剩余的内存空间都不足时,则会发生内存溢出。
Q:调整栈的大小,就能保证不出现溢出吗?
A:不能。栈空间的大小总归是有限的,如果出现无尽递归,则一样会发生栈溢出,只时发生溢出的时间会晚一些。
Q:分配的栈内存越大越好吗?
A:不一定。过大的栈会影响到其余区域的空间的大小。导致JVM整体的性能可能会下降。
Q:垃圾回收是否会涉及虚拟机栈?
A:不会。
Q:方法中定义的局部变量是否线程安全?
A:如果该局部变量只能被一个线程调用,则其是线程安全的。比如一个局部变量在方法区内部产生和消亡。
但是如果该局部变量能够被多个线程调用,则存在线程安全问题。比如该局部变量有外部通过参数传入,或者被当作返回值传出。
Q:为什么需要常量池?
A:jvm 在栈帧(frame) 中进行操作数和方法的动态链接(link),为了便于链接,jvm 使用常量池来保存跟踪当前类中引用的其他类及其成员变量和成员方法。每个栈帧(frame)都包含一个运行常量池的引用,这个引用指向当前栈帧需要执行的方法,jvm使用这个引用来进行动态链接。