JVM全文
运行时数据区简述
- 内存,是硬盘和cpu的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。jvm内存布局规定了java在运行过程中内存申请、分配、管理,保证jvm的高效稳定。
- 区域结构,jdk7前,持久代/方法区;jdk8后,元数据区
- 其中一些区域,会随着虚拟机启动而创建,随着虚拟机退出而销毁,生命周期同进程;而另一些则是与线程一一对应,这些与线程对应的数据区域会随着线程开始和结束创建和销毁
- 红色区域,进程一份
- 灰色区域,线程一份
- 每个线程,独立包括程序计数器、栈、本地栈
- 线程间共享,堆、堆外内存(永久代/元空间、代码缓存)
- 每一个jvm实例对应一个runtime实例,即运行时环境,相当于内存结构中最外层的部分,包裹所有结构
线程
- 线程,程序中的运行单元,运行一个应用有多个线程并行执行。
守护线程、普通线程 - 如果当前线程只有守护线程,虚拟机也可以停掉
- hotspot jvm中,每个线程都与操作系统的本地现成直接映射;当一个java线程准备好执行后,一个操作系统的本地线程同时创建,java线程执行终止后,本地线程也会回收
- 操作系统负责所有线程的安排调度到任何一个可用的cpu,一旦本地线程初始化成功,就会调用java线程中的
run()方法 - java线程类型
- 虚拟机线程,这种线程的操作是需要jvm达到安全点(循环的末尾/方法返回前/调用方法的call之后/抛出异常的位置)才会出现,即线程运行到某个可以安全回收对象的位置,执行类型包括
stop-the-world(垃圾回收时,涉及对象引用的更新,需要暂停其他线程)的垃圾收集、线程栈收集、线程挂起以及偏向锁撤销(允许其他线程争夺这个资源的操作权)- 周期任务线程,时间周期事件的体现,用于周期性操作的调度执行
- GC线程,对jvm里不同种类的垃圾收集行为提供支持,后台线程
- 编译线程,在运行时将字节码编译成到本地代码
- 信号调度线程,接受信号并发送给jvm,在它内部通过调用适当的方法进行处理
程序计数器
- PC寄存器,Program Counter Register,是对物理PC寄存器的一种软件模拟。CPU只有把数据装载到寄存器才能够运行,程序钩子,记录每一条程序的地址
- 是程序控制流的指示器,用来存储指向下一条指令的地址,由执行引擎读取下一条指令;分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器。既没有
GC(垃圾回收),也没有OOM(空间溢出,OutOfMemoryError) - 是一块很小的内存空间,可以忽略不计,也是运行速度最快的存储区域
- 每个线程都有自己的程序计数器,任何时间,一个线程,都只有一个方法在执行,即当前方法。程序计数器会存储当前线程正在执行的java方法的
jvm指令地址;如果在执行native本地方法,调用C/C++的方法,则是未指定的值(undefined) - 字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令
示例
- 左边一栏,为pc寄存器的计数和偏移。执行引擎根据指令地址取出指令,操作
局部变量表/操作数栈,将字节码指令翻译成机器指令。#Number指的是符号引用 - 中间一栏,操作指令
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: ldc #2
12: astore 4
14: getstatic #3
几个问题
- 并行,同时执行;串行,顺序执行
- 并发,快速切换交替执行,不同时,效果可以达到并行
- 使用PC寄存器存储字节码指令地址的作用
- cpu在不停地切换各个线程,这时候切换回来,需要知道当前线程执行到了哪个位置
- jvm的字节码解释器需要通过改变pc寄存器的值,来明确下一条应该执行哪条字节码指令
- PC寄存器为什么被设定成线程私有
- 一个cpu执行多个线程,是并发执行的,并不是同时的,需要不停地人物切换
- PC寄存器进行线程私有,可以准确地记录各个线程正在执行的当前字节码指令地址,不会出现互相干扰的情况,各个线程之前的PC寄存器相互独立,互不影响
- CPU时间片,CPU分配给各个程序的时间,每个线程被分配一个时间段,CPU一次只能处理程序要求的一部分,让每个程序轮流执行
虚拟机栈
- 集于栈的指令集结构,跨平台、指令集小,编译器容易实现;性能下降,实现同样的功能需要更多的指令
- 栈是运行时的单位,栈解决程序运行问题,即程序如何执行,如何处理数据;快速有效的分配存储方式,访问速度仅次于程序计数器
- 堆是存储时的单位,解决数据存储的问题,怎么放,放在哪
- 栈可以通过数组或链表来实现
- Java 虚拟机栈使用的内存不需要保证是连续的
简介
- 每一个线程在创建时都会创建一个虚拟机栈(线程私有),内部保存一个个的栈帧,对应一次次的java方法调用
- 生命周期和线程一致
- 主管java程序的运行,保存方法的局部变量(8种基本数据类型+对象的引用地址)、部分结果,并参与方法的调用和返回;局部变量-成员变量(类中的属性);基本数据类型-引用类型(类、数组、接口)
- jvm堆栈的的操作只有两个,方法执行,进栈;执行结束,出栈
- 不存在垃圾回收的问题
GC
开发中遇到的异常
- 采用固定大小的java虚拟机栈,每一个线程的java虚拟机栈容量可以在线程创建的时候独立选定,如果线程请求分配的栈容量,超过java虚拟机栈允许的最大容量,会抛出
StackOverFlowError异常。无限递归- 如果java虚拟机栈动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,会抛出
OutOfMemoryError异常
设置栈的大小
- run->eidt configurations->modify options->add vm options->
-Xssxxk/xxm - os默认1m
存储单位
- 虚拟机栈中的数据都是以栈帧的格式存在的;
- 在这个线程上,每个正在执行的方法都对应一个栈帧
- 栈帧是一个内存区块,一个数据集,维系着方法执行过程中的各类数据信息
- OOP,Object Oriented Programming的基本概念,
类、对象- 类中基本结构,
field(属性、字段、域),method
运行原理
- 压栈和出栈,先进后出/后进先出
- 一条活动线程中,一个时间点上,只会有一个活动的栈帧,只有当前正在执行的方法的栈帧(栈顶栈帧)有效,被称为当前栈帧,对应的就是当前方法,定义这个方法的类就是当前类
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
- 如果该方法内调用了其他方法,对应新的栈帧就会被创建出来,放在栈的顶端
- 不同线程中所包含的栈帧,不允许存在互相饮用
- 栈帧弹出,
正常的函数返回/抛出异常
栈帧的内部结构
- 局部变量表/本地变量表/局部变量数组,
local variables,每个方法都有自己的局部变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,包括基本数据类型、对象引用以及
returnAddress类型- 建立在线程的栈上,是线程的私有数据,不存在数据安全的问题
- 局部变量表所需的容量大小是在编译期确定下来,并保存在方法的
Code属性的maximum loacal variables数据项中,对应字节码文件中locals=1,在方法运行期间不会改变- 方法嵌套的次数由栈的大小决定,栈越大,方法嵌套调用的次数越多;对应一个函数而言,参数和局部变量越多,局部变量表膨胀,栈帧越大,函数调用就会占用更多的栈空间,导致其嵌套调用次数减少
- 局部变量表中的变量只在当前方法调用中有效,方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当方法调用结束后,随着方法帧的销毁,局部变量表也随之销毁
- slot,变量槽,局部变量表中最基本的存储单元。32位以内的类型只占用一个slot,64位的类型(long/double)占用两个slot。byte/short/char/boolean,都以int型来保存;序号表示槽位
- jvm会为每一个slot都分配一个访问索引,当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量会按照顺序被复制到局部变量表中的每一个slot上;如果访问64位的变量,需要访问起始索引;如果当前帧是由构造方法或者实例方法(非静态的方法,没有static修饰)创建的,该对象引用this将会放在0的slot处
- 栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用于之后声明的新的局部变量就会复用过期局部变量的槽位
- 栈帧中,与性能调优关系最为密切的部分就是局部变量表
- 在方法执行时,虚拟机使用局部变量表完成方法的传递,如果方法中声明了对象,那么局部变量表中就会有这个对象,jvm通过这个对象变量找到对应的实例类
- 局部变量表中的变量是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收,局部变量表与性能调优最为密切
问题:为什么静态方法不能调用this;答:因为当前方法的局部变量表中不存在this变量
变量分类,按照数据类型,基本数据类型/引用数据类型;按照声明位置,成员变量/局部变量
成员变量,在使用前,都经历过默认初始化赋值。分为类变量/实例变量
- 类变量,static修饰,linking的prepare阶段,隐式赋值,类型的零值->initial阶段,显式赋值,静态代码块赋值
- 实例变量,随着对象的创建,会在堆空间中分配实例变量空间,进行默认赋值(隐式)
局部变量,在使用前必须进行显式赋值,否则编译不通过
- 操作数栈/表达式栈,operand stack
- 通过数组来实现,但是并非采用访问索引的方式进行数据访问;在执行过程中,根据字节码指令,往栈中写入数据或者提取数据,出栈/入栈
- 用于保存计算过程的中间结果,作为计算过程中变量临时的存储空间
- 随着方法的调用开始,当一个新的栈帧被创建出来,这个方法的操作树栈也是空的
- 每一个操作数栈都会有一个明确的栈深度,在编译期确定,保存在
Code中max_stack,对应字节码文件stack=1- 32位,占用一个栈深度;64位,占用两个栈深度
- 如果被调用的方法有返回值,其返回值将会被压入当前栈的操作数栈中,更新PC寄存器中下一条需要执行的字节码指令
- java虚拟机基于栈的执行引擎,这里的栈指的就是操作数栈
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,编译期期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段再次验证
- 动态链接/指向运行时常量池的方法引用,dynamic linking
- java源文件被编译到字节码时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中
- 一个方法调用另外的其他方法,需要通过常量池中指向方法的符号引用来表示,动态链接则将符号引用转换为调用方法的直接引用
- 每一个栈帧当中都包含一个指向运行时常量池中该栈帧所属方法(该帧调用的方法)的引用,为了支持当前方法的代码能够实现动态链接,找到要调用的方法
- 面向对象的思想,通过引用进行调用,节省空间,提高运行速度
- 运行时常量池,为了提供一些符号和常量,便于指令的识别
- 虚拟机在class文件时才会进行动态链接,class文件中
不会保存各个方法和字段的最终内存布局信息,这些字段和方法的符号引用不经过转换无法直接被虚拟机使用;当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段,将其替换成直接引用,翻译到具体的内存地址中
- 方法返回地址/方法正常退出或者异常退出的定义,return address
- 存放调用该方法的PC寄存器的值,A方法调用B方法,B方法中的方法返回地址是
存放A调用该方法的PC寄存器的值,此时的值为下一条指令的值- 正常退出,
调用者的PC寄存器的值作为返回地址,即返回调用者调用该方法的指令的下一条指令的地址,并且调整A的PC寄存器指向调用该方法的指令的下一条指令的地址- 异常退出,返回地址是要通过异常表来确定,通过异常完成退出,不会给调用者产生任何的返回值
- 在字节码指令中,返回指令包含ireturn(当返回值是Boolean、byte、char、short和int类型时使用)、lreturn(long)、freturn(float)、dreturn(double)以及areturn(引用类型),另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用
- 在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,称为异常完成出口。方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码
- 附加信息,java虚拟机实现相关的附加信息,对程序调试提供支持的信息
- 帧数据区,包括动态链接、方法返回地址、附加信息
示例解析
- lineNumberTable PC寄存器的索引与java代码的行号的对应
- locaVariableTable,变量表。Nr.索引;起始PC,作用域的起点;length,作用域的长度(对于PC寄存器的长度);序号,占用的slot位置索引;描述符,具体的类
- 代码解析
- 原代码
byte i = 15;
int j = 8;
int k = i + j;
int m = returnTest();
- 字节码
0 bipush 15 //将15压入操作数栈
2 istore_1 //将15出栈,存入局部变量表
3 bipush 8
5 istore_2
6 iload_1 //将15 和 8 压入操作数栈
7 iload_2
8 iadd // 15 + 8 出栈,并进行加操作,再压入操作数栈
9 istore_3 //23 出栈,存入局部变量表
10 aload_0 //将this指针压入操作数栈
11 invokevirtual #2 //调用方法,并在返回值压入操作数栈
14 istore 4 //将返回值出栈,存入局部变量表
- 异常处理,起始PC,执行代码;结束PC,没有异常执行的代码;跳转PC,异常处理代码
栈顶缓存
- 基于栈式架构的虚拟机所使用的零地址(操作数据不需要地址)指令更加紧凑,但是完成一项操作需要更多的入栈和出栈指令,需要更多的指令分派次数和内存读写次数
- 由于操作数存储在内存中,频繁地执行内存读写操作影响执行速度
将栈顶元素全部缓存在物理cpu的寄存器中,降低对内存的读写次数,提升执行引擎的执行效率
方法的调用
- 将符号引用转换为调用方法的直接引用
- 静态链接,当一个字节码文件被装载进jvm中,如果
被调用的目标方法在编译期可知,且运行期保持不变,这种情况下,将调用方法的符号引用转换为直接引用的过程为静态链接 - 动态链接,如果
被调用的方法在编译期无法被确定,只能够在程序运行期将调用方法的符号引用转换为直接引用,这种引用转换过程具有动态性,则被称为动态链接 - 绑定,一个字段、方法或者类在符号引用被替换为直接引用的过程
- 早期绑定,对应静态链接,被调用的目标方法在编译期可知,且运行期保持不变,即
可将这个方法与所属类型进行绑定,明确被调用的目标方法是哪一个 - 晚期绑定,对应动态链接,被调用的方法在编译期无法被确定,
只能在程序运行期根据实际的类型绑定相关的方法 - 面向过程的语言,为早期绑定
- 如果java中不希望某个方法拥有虚函数的特征,用
关键字final(不能被重写)修饰
虚方法与非虚方法
- 非虚方法,编译期就确定了具体的调用版本,运行时不可变,静态方法/私有方法/finla方法/实例构造器/父类方法,不涉及多态的形式
- 虚方法,涉及多态,不能明确是哪个类的方法,除了非虚方法的所有方法
- 子类对象多态性的使用前提,类的继承关系/方法的重写
- jvm调用指令,前四种固化在虚拟机内部,不可人为干预;
invokedynamic由用户确定方法版本
//非虚方法
invokestatic 调用静态,解析阶段确定唯一方法版本
invokespecial 调用<init>方法、私有以及父类方法,解析阶段确定唯一方法版本
//虚方法 (final修饰的方法除外,也使用invokevirtual)
invokevirtual 调用所有虚方法
invokeinterface 调用接口方法
//动态调用指令,java8的lambda表达式,可以直接生成invokedynamic指令
invokedynamic 动态解析需要调用的方法,然后执行
- 动态类型语言,在运行期进行类型检查,判断变量值的类型信息,变量没有类型信息,变量值才有类型信息
- 静态类型语言(java),在编译期进行类型检查,判断变量自身的类型信息
调用重写方法的本质
- 找到操作数栈顶的第一个元素所执行的对象的实际类型,记为类型C
- 如果该类型C中找到与常量池中的描述都相同的方法,进行访问权限校验,如果通过则返回这个方法的直接引用;如果不通过,则返回
IllegalAccessError异常,没有权限访问 - 否则,按照继承关系从下向上依次对类型C的各个父类进行上一步的搜索和验证
- 如果始终没有找到合适的方法,则抛出
AbstractMethodError异常,表明调用的是接口中的方法,没有被重写 - 为了提高性能,每次都要搜索,jvm在类的方法区建立了一个
虚方法表(virtual method table),使用索引表来代替查找,存放各个方法的实际入口;会在类加载的链接阶段(解析环节,将符号引用转换为直接引用)被创建并开始初始化,类的变量初始化值准备完成之后,jvm也会把该类的方法表初始化完毕
面试题
- 举例栈溢出的情况
StackOverflowError(固定空间)/OutOfMemoryError(动态申请),一个个加栈帧
- 调整栈的大小,只能出现的时间更晚一些,不能保证不出现溢出,无限递归
- 分配的栈内存是否越大越好
一定时间的error概率减小了,会影响其他部分的空间,内存是固定的,线程数会变少
- 垃圾回收是否会涉及到虚拟机栈空间
不会,虚拟机栈负责保存方法的局部变量(8种基本数据类型+对象的引用地址)、部分结果,并参与方法的调用和返回,并不涉及到对象实例化
- 方法中定义的局部变量是否线程安全
StringBuffer线程安全;StringBuilder线程不安全- 保证一个参数,同一时刻只被一个线程操作、内部产生、内部消亡等,才能保证线程安全
- 如果只有一个线程操作此数据,则为线程安全;如果有多个线程操作此数据,则此数据是共享数据,如果不考虑同步机制,会存在线程安全问题
- 操作方法参数时,这个参数有可能并发得被其他线程操作,存在线程不安全
- 作为返回值时,返回后,同时被多个线程操作,存在线程不安全