三、运行时数据区
0.概述:
不同的JVM对于内存的划分方式和管理机制存在着部分差异
运行时数据区包括堆,方法区,程序计数器,本地方法栈,虚拟机栈
堆和方法区是线程共享的,随着虚拟机的创建而创建,随着虚拟机销毁而销毁
程序计数器,虚拟机栈,本地方法栈是线程私有的,每个线程都有一份
关于线程:
JVM是支持多线程的
在Hotspot JVM中,每个线程都与操作系统中的本地线程直接映射:
当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。一旦本地线程初始化成功,他就会调用Java线程中的run()方法。Java线程终止后,本地线程也会回收
1、程序计数器(PC寄存器)
存储指向下一条指令的地址,由执行引擎读取下一条指令。控制分支,循环等程序运行的流程。
占用内存非常小,运行速度快,线程私有
程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;如果是在执行native方法,则是未指定值(undefined)
不会出现内存溢出。没有垃圾回收。
2、虚拟机栈
0).概述
Java的指令都是根据栈来设计的,不同平台CPU架构不同,所以不能基于寄存器
优点是跨平台,指令集小,编译容易实现,缺点是性能下降,实现同样的功能指令集多
栈是运行时的单位,而堆是存储的单位
即栈解决程序如何执行,如何处理数据。堆解决了数据的存储,数据怎么放,放在哪儿
每个线程都有一个虚拟机栈,里面保存了一个个的栈帧,一个栈帧对应着java中的一个方法。
作用:主管java程序的运行,保存方法的局部变量(8中基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回
栈的特点(优点):
效率较高,访问速度仅次于程序计数器
JVM对栈的操作只有两种:方法执行,入栈,执行结束后出栈
不存在垃圾回收问题(GC、OOM)
面试题:开发中遇到的异常有哪些?
答:Java栈的大小是动态的或者是固定不变的,如果采用固定不变的,当一个线程请求的栈容量超过虚拟机栈允许的最大容量,会报栈溢出异常(StackOverflowError)
如果是动态扩展的,当虚拟机栈扩展的时候没有申请到足够的内存空间,或者是没有足够的内存空间去创建虚拟机栈的时候,就会抛出内存溢出异常(OutOfMemoryError)
设置栈内存大小:使用参数-Xss设置。例-Xss256k
栈的存储单位:
栈中存储什么?
栈中存储着许多的栈帧,线程上的每个方法都各自对应一个栈帧
对栈的操作只有入栈和出栈,规则是先进后出
一个线程在一个时间点上只有一个活动的栈帧,即当前执行的方法的栈帧,称为当前栈帧,当前栈帧对应的方法叫当前方法,当前方法所在的类叫做当前类
执行引擎运行的所有字节码指令只针对当前栈帧
当程序正常结束return或者抛出未处理的异常都会导致栈帧被弹出
栈帧的内部结构:
局部变量表
操作数栈(表达式栈)
动态链接(指向运行时常量池的方法引用)
方法返回地址(方法正常退出或者异常退出的定义)
一些附加信息
1).局部变量表(local variables)
定义一个数组,主要用于存储方法参数和定义在方法体内的局部变量。包括基本数据类型、对象引用、returnAddress类型
线程私有,不存在安全问题。
局部变量表所需的容量大小是在编译期间确定下来的,保存在方法的Code属性的maximum local variables数据项中。在运行期间不会改变.
一个方法中的局部变量越多,局部变量表越大,栈帧就越大,虚拟机栈固定大小的情况下,可存放的栈帧就越少,方法调用的嵌套次数就越少。
局部变量表中的变量只在当前方法调用中有效。方法调用结束后,栈帧销毁,局部变量随之销毁。
slot(变量槽):局部变量表的最基本单元在局部变量表中,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot
byte、short、char在存储前被转换为int,boolean也被转换为int,0-false 1-true
当一个实例方法被调用时,他的参数和局部变量将会按照顺序被复制到局部变量表中的每一个slot上
如果要访问局部变量表中一个64bit的局部变量表时只需使用前一个索引即可(例:占用了4号坑和5号坑 那么索引就是4号坑)
构造方法和实例方法的index为0处的slot存放的都是this。所以我们能够直接试用,静态方法的局部变量表中没有this,所以不能使用this.
2)操作数栈(Operand Stack) 又名表达式栈
使用数组来实现,不是采用访问索引的方式来进行数据访问的 ,主要用于保存计算过程的中间结果。
进入操作数栈的时候,
-1~5 采用 iconst 指令
-128~127 采用 bipush 指令
-32768~32767 采用 sipush 指令
-2147473648~2147483647 采用 ldc指令
ps:byte,short,char,int进入操作数栈之后都会转换成int
字节码的角度分析i++和++i的区别
i++是先从局部变量表中取出之前的值,然后再自增,++i是先自增,再从局部变量表中取出这个值
栈顶缓存技术:将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
3).动态链接(指向运行时常量池的方法引用)
桢数据区:包含动态链接,方法返回地址,一些附加信息
类加载之后所有的类或者方法,变量等信息都会作为符号被放在运行时常量池中
动态链接的作用就是把这些符号引用转换为调用方法的直接引用
为什么需要运行时常量池呢?
因为可以节省空间,我们可以把类加载后的东西都放在常量池中,然后需要的时候直接使用一个指针指向它。
方法的调用:
静态链接和动态链接
如果需要调用的方法在编译期间可知,则为静态链接。为早期绑定。(例如在构造方法中使用super())
如果无法被确定下来,就是动态链接。是晚期绑定。(例如在方法中传一个接口或者父类,在方法内部调用其方法)
链接--->符号引用转换为直接引用
虚方法和非虚方法:
非虚方法:如果编译期间就确定了具体的调用版本,并且在运行时是不变的,这种方法就是非虚方法(类似于上面的静态链接和早期绑定的概念)
静态方法,私有方法,final修饰的方法,实例构造器,父类方法都是非虚方法。其他方法称为虚方法
调用方法的指令:
普通调用指令:
invokestatic:调用静态方法,解析阶段确定唯一方法版本
invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本
invokevirtual:调用所有虚方法
invokeinterface:调用接口方法
动态调用指令:
invokedynamic:动态解析出需要调用的方法,然后执行.
其中invokestatic指令和invokespecial指令调用的方法为非虚方法,其余的(final修饰的除外)为虚方法。
动态类型语言和静态类型语言
区别:对类型的检查是在编译期还是在运行期
例:java中--->String nam = "zhangsan";
js中--->var name = "zhangsan";
立即推--->java是静态类型语言
jdk1.8引入的ambda表达式是采用的动态类型的
4).方法返回地址
存储调用该方法的pc寄存器(程序计数器)的值(下一条执行指令)
主要是用于方法的正常退出时。方法的异常退出的返回地址试要通过异常表来确定的。
5).一些附加信息
存储一些与虚拟机的实现相关的附加信息(不重要)
栈相关面试题
1.举例栈溢出的情况(StackOverflowError)
如果虚拟机栈是固定大小的话,如果存储的内容超过这个栈的大小,就会出现栈溢出的情况,可以通过-Xss设置虚拟机栈的大小
如果虚拟机栈的大小是自动扩容的话,如果虚拟机栈分配不到足够大的内存,就会出现内存溢出(OOM)的情况。
2.调整栈的大小,就能保证不出现溢出吗?
不能。如果写一个死循环,那多大的大小就不够用。
3.分配的栈内存越大越好吗?
不是,因为每一个线程都会有一个虚拟机栈,虚拟机栈分配的空间过大了,就可能会线程数量减少而且其他资源能够分配的空间就减少了。
4.虚拟机栈GC吗
不存在,因为在栈中执行完相关操作之后就直接从栈中弹出了,所以不存在GC的情况。
5.方法中定义的局部变量是否线程安全?
具体问题具体分析。单线程下线程安全,多线程下线程不安全。
4.本地方法栈
本地方法接口:
native关键字修饰的方法,不能用abstarct修饰
为什么要使用 Native Method?
1.与Java环境外交互
java诞生的时候C/C++非常厉害,所以尽量的和他们融合
2.与操作系统交互
因为操作系统的底层基本上都是使用C实现的
3.Sun's Java
Sun的解释器是用C实现的
本地方法栈:
参考虚拟机栈,本地方法栈是用于管理本地方法的
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的市街。它和虚拟机拥有同样的权限:
本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
它甚至可以直接使用本地处理器中的寄存器
直接从本地内存的对中分配任意数量的内存