一、程序计数器
1、定义:Program Counter Register
2、作用:记住下一条JVM指令的执行地址
3、特点:
- 线程私有,每个线程都有自己的PCR,生命周期与线程的生命周期保持一致
- 在内存结构中,唯一不存在内存溢出
- 它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域
4、常见问题:
使用PC寄存器存储字节码指令地址有什么用呢?
- 因为CPU需要不停的切换各个线程,这时候切换回来之后,就可以知道执行到哪里了
二、虚拟机栈
1、定义:
- 每个线程运行时所需要的内存,称为虚拟机栈,用来存储当前线程运行Java方法所需的数据,指令,返回地址
- 每个栈由于多个栈帧组成,每个栈帧对应每个方法调用时所占用的内存空间
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
- 每个栈帧中存储着:
局部变量表(局部变量数组或本地变量表)
1:定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用,以及returnAddress类型
2:由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
3:局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中,在方法运行期间是不会改变局部变量表大小的
操作数栈(或表达式栈)
1:在方法执行过程中,根据字节码指令,往栈(操作数栈)中写入数据或提取数据,即入栈/出栈
2:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
3:每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值
4:栈中的任何一个元素都是可以任意的Java数据类型
32bit的类型占用一个栈单位深度
64bit的类型占用两个栈单位深度
5:操作数栈并非采用访问索引的方式进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
6:如果被调用的方法有返回值,其返回值将会被压入当前栈帧的操作数栈中
动态链接(或指向运行时常量池的方法引用)
1:动态链接,方法返回地址,一些附加信息也统称为帧数据区
2:在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在字节码文件的常量池中,动态链接的作用就是将这些符号引用转换为调用方法的直接引用
方法返回地址(或方法正常退出或异常退出的定义)
1.作用:存放调用该方法的pc寄存器的值
2.一个方法的结束,有两种方式:正常执行完成,出现未处理的异常,非正常退出
3.无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息
4.正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值
5.在字节码指令中,返回指令包含ireturn(当返回值是boolean,byte,char,int,和short类型时使用)、lreturn,freturn,dreturn,以及areturn,另外还有一个return指令供声明为void的方法,实例初始化方法,类和接口的初始化方法使用
6.方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码
一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息
2、常见问题:
-
垃圾回收是否涉及栈内存?
由于方法调用完毕,对应栈帧会弹出栈,内存自动回收,所以不需要 -
栈内存分配越大越好么?
可以通过-Xss size进行分配栈内存,由于总的内存有限,栈内存分配越大,线程数就会减少,所以不建议分配过大 -
方法内的局部变量是否线程安全
如果方法内的局部变量没有逃离方法的作用范围,它是线程安全的
如果是局部变量引用了对象,并且逃离了方法的作用范围,需要考虑线程安全问题
3、栈内存溢出:
java.lang.StackOverflowError
- 栈帧过多导致溢出,比如无穷递归可能会导致
- 栈帧过大
OutOfMemoryError
如果剩余内存只有500M,此时设置虚拟机栈内存为1M,然后同时启动1000个线程,就会造成 OutOfMemoryError,可能导致死机
4、线程运行诊断
-
cpu占用过多?
i:首先使用top命令查看是哪个进程占用cpu过多,获取进程号
这里获取到32655占用最多
ii:然后用 ps H -eo pid,tid,%cpu | grep 32655 筛选出进程id,线程id,和cpu占用情况
获取到32665线程id,这里的是十进制,换算成十六进制 0x7f99
iii:再使用 jstack 进程id,可以列出改进程下所有的线程
找到thread1的nid 刚好是0x7f99,仔细观察,发现第八行有问题 -
程序运行很长时间没有结果?
可能是因为线程发生了死锁导致
使用 jstack 进程id,进行查看
PS: jstack用于显示指定进程内线程信息
5、优化:
- 资源紧张的时候,减少 -Xss
- 减少方法的调用次数,即减少了入栈出栈次数
6、作用:
主管Java程序的运行,它保存方法的局部变量、部分结果、并参与方法的调用和返回
7、特点(优点):
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
JVM直接对Java栈的操作只有两个:
每个方法的执行,伴随着进栈(入栈,压栈)
执行结束后的出栈工作
对于栈来说不存在垃圾回收问题
8、栈中可能出现的异常:
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定,如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机就会抛出StackOverFlowError异常
如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出OutofMemeoryError
9、栈运行原理:
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧
Java方法有两种返回函数的方法:
一种是正常的函数返回,使用return指令
另外一种是抛出异常,不管使用哪种方式,都会导致栈帧被弹出
10、关于slot的理解:
1:参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束
2:局部变量表,最基本的存储单元是Slot(变量槽)
局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量
3:在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(Long和double)占用两个slot
4:JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
5:当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上
6:如果当前帧是由构造方法或者实例方法创建的,那么改对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列
7:如果需要访问局部变量表中一个64位的局部变量值时,只需要使用前一个索引即可
8:Slot的重复利用,栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的
三、本地方法栈
1、定义
用于描述本地方法运行时的线程内存模型
本地方法是由非Java语言编写的,编译成和处理器相关的机器代码
OTS:
- 栈是运行时的单位,而堆是存储的单位
- 查看字节码指令的方式
javap -v **.class用于对字节码文件进行反编译
或者javap -verbose 字节码文件名
使用jclasslib,可以在idea中view->show bytecode with jclasslib
- byte,short,char,boolean存储都是转成int存储,char用ASCII,boolean中true为1,false为0
- 字节码指令解析案例
public void test(){ byte i = 15; int j = 8; int k = i + j;}
0:bipush 15 根据PC寄存器中指令地址,执行改行指令,将15压入操作数栈,更新PC寄存器地址为2
2:istore_1 根据PC寄存器中指令地址,执行改行指令,操作数栈出栈操作,将15存入局部变量表中索引地址为1处(此时0存储为this),更新PC寄存器地址为3
3:bipush 8 根据PC寄存器中指令地址,执行改行指令,将8进行入栈操作,更新PC寄存器地址为5
5:istore_2 根据PC寄存器中指令地址,执行改行指令,操作数栈出栈操作将8存入局部变量表索引为2处,更新PC寄存器地址为6
6:iload_1 根据PC寄存器中指令地址,执行改行指令,从局部变量表中加载15到操作数栈中,更新PC寄存器地址为7
7:iload_2 根据PC寄存器中指令地址,执行改行指令,将8压入操作数栈,更新PC寄存器地址为8
8:iadd 根据PC寄存器中指令地址,执行改行指令,将操作数栈中的两数进行add操作,更新PC寄存器地址为9
9:istore_3 根据PC寄存器中指令地址,执行改行指令,将23出栈存入局部变量表中索引地址为3处,更新PC寄存器地址为10
10:进行return操作
虽然i为byte类型,但是实际存储的时候还是以int类型存储,j虽然为int类型,但入栈的时候还是以byte类型入栈
- 方法的调用:
静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此就被称之为动态链接
1:对应的方法的绑定机制为:早期绑定和晚期绑定,绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次
2:早期绑定:就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目的方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用
3:晚期绑定:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种方式也就被称之为晚期绑定
虚方法与非虚方法
非虚方法:如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法是非虚方法
静态方法,私有方法,final方法,实例构造器,父类方法都是非虚方法
其他方法称为虚方法
虚拟机中提供了一下几条方法调用指令:
普通调用指令:
1.invokestatic:调用静态方法,解析阶段确定唯一方法版本
2.invokespecial:调用类的构造器,私有及父类方法,解析阶段确定唯一方法版本
3.invokevirtual:调用所有虚方法
4.invokeinterface:调用接口方法
5.invokedynamic:动态解析出需要调用的方法,然后执行
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本,其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法