本文共 10121 字,大约阅读时间需要 33 分钟。
在JVM中表示两个class对象是否为同一个类存在两个必要条件
JVM必须直到一个类型是由启动类加载器加载还是由用户类加载器加载。如果由用户类加载器加载,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载是相同的
类的主动使用和被动使用
不同的JVM对于内存的划分方式和管理机制存在着部分差异
进程是随着虚拟机的启动而创建,退出而销毁
每个线性独立享有:虚拟机栈、本地方法栈、程序计数器
多个线程共享:堆,堆外内存(永久代或元空间、代码缓存)
每个JVM只有一个Runtime实例
线程是一个程序里的运行单元,JVM中的多个线程可以并发执行,在Hotspot JVM中的线程与操作系统中的本地线程有之间直接映射的关系
后台线程中不包含main线程以及其自己创建的线程
在Hotspot JVM中的后台线程主要包括
JVM中的程序技计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息,CPU只有把数据装载到寄存器才能够运行。JVM中的PC寄存器是对物理PC寄存器的一种抽象
作用
PC寄存器用来存储指向下一条指令的地址,也即要执行的指令代码。由执行引擎读取吓一条指令
介绍
OOM(OutOtMemory):堆、方法区、本地方法栈、虚拟机栈
GC:堆、方法区
使用PC寄存器存储字节码指令地址有什么用?为什么使用PC寄存器记录当前线程的执行地址?
PC寄存器为什么会被设定为线程私有?
CPU时间片
优点:跨平台,指令集小,编译器容易实现;缺点:性能下降,实现同样的功能需要更多的指令
栈是运行时的单位,而堆是存储的单位
面试题:开发中遇到的异常有哪些?
Java虚拟机允许Java栈的大小存在固定不变或者是动态的
通过-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
每个栈帧存储着:
JVM会为局部变量表中的每一个slot分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量
当一个实例方法被调用时,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot中
如果要访问64位的局部变量时,只需要访问第一个索引即可
如果当前正在是由构造函数或者实例方法创建的,那么该对象引用this将会存放在index=0的slot处,其余参数的索引依次排列
栈帧中局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么在其后声明的变量很有可能会利用过期局部变量的槽位,从而达到节省资源的目的
静态变量与局部变量的对比
在执行方法时,虚拟机栈使用局部变量表完成方法的传递
局部变量表的变量也是重要的垃圾回收的根节点,只要被局部变量表直接或者间接引用的对象都不会被回收
每一个独立的栈帧中处理包含局部变量表以外,还包含一个后进先出的操作数栈,也可以称之为表达式栈
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或者提取数据,即入栈/出栈
如果被调用的方法带有返回值的话,其返回值会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令
操作数栈中的字节码指令的类型必须要与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载器加载过程中的类检验阶段的数据流分析阶段要再次验证
另外我们说的Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之创建出来,这个方法的操作数栈是空的
每一个操作数栈都有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值
栈的任何一个元素都可以是任意的Java数据类型
操作数栈并非采用访问索引的方式来进行数据访问,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据的访问
程序员面试过程中,常见的i++和++i区别
将栈顶元素全部缓存在物理CPU寄存器中,以此降低对内存的读/写,提升执行引擎的执行效率
帧数据区
常量池的作用:就是为了通过一些符合和常量,便于指令识别
在JVM中将符号引用转为调用方法的直接引用与方法的绑定机制相关
对应的方法的绑定机制为:早期绑定和晚期绑定。绑定是一个字段,方法或者类在符合引用被替换为直接引用的过程,这仅仅发生一次
非虚方法与虚方法
非虚方法
虚拟机中提供了以下几条方法的指令:
普通调用指令
动态调用指令
invokedynamic:动态解析需要调用的方法,然后执行
interface Func { boolean func(String s);}public class Lambda { public void lambda(Func func) { return; } public static void main(String[] args) { Lambda lambda = new Lambda(); // invokedynamic Func func = s -> { return true; }; lambda.lambda(func); lambda.lambda(s -> { return false; }); }}
前四条指令固化在虚拟机内部,不可认为干涉,而invokedynamic指令则支持由用户确定方法版本。其中,invokestatic指令和invokespecial指令调用调用的方法称为非虚方法,其余的(除final修饰的)称为虚方法【final无法被子类重写,虽然是invokevirtual,但其实是非虚方法,当显示通过super调用时是invokespecial】
说白了虚方法就是可以实现多态的即重写的方法,因为多态在编译期间无法确定,只能在运行期间确定
public class Son extends Father { public Son() { // invokestatic super();// System.out.println("son"); } public Son(int age) { // invokestatic this();// System.out.println("son age is " + age); } //不是重写父类的方法,因为静态方法不能重写 public static void showSta(String s) { System.out.println("son static " + s); } private void showPri(String s) { System.out.println("son private " + s); } public void show() { //编译时期就是确定的 // invokestatic showSta("look!"); // invokestatic super.showSta("look!"); // invokespecial showPri("pri!!"); //invokespecial super.showComm(); //编译期间无法确定的,即会被重写的方法 //invokevirtual showFin();//因为此方法被final修饰,因此无法被重写,因此在编译期间是可以确定的,所以也认为是虚方法 //invokevirtual showComm(); //invokevirtual info(); Method method = null;//是由实现接口的类进行重写的方法 // invokeinterface method.method(); } public void info() { System.out.println("info"); } public static void main(String[] args) { Son son = new Son(); son.show(); }}class Father { public Father() { System.out.println("father"); } public static void showSta(String s) { System.out.println("father static " + s); } public final void showFin() { System.out.println("father final"); } public void showComm() { System.out.println("father common"); }}interface Method { void method();}
为了解决每次动态分配的问题,提高性能,JVM采用在类的方法区建立一个虚方法表(非虚方法不会出现在表中)来实现,使用索引表来代替查找
每一个类中都有一个虚方法表,表中存放着各个方法的实际入口
虚方法表会在类被加载的链接阶段被创建和初始化,类的变量初始值准备完毕之后,JVM会把该类的方法表也初始化完毕
动态语言和静态语言的区别就在于两者对于类型的检查是在编译期间还是在运行期间,满足前者就是静态语言,满足后缀就是动态语言
存放调用该方法的pc寄存器
一个方法的结束,有两种方式
无论哪种方式的退出,在方法推出后都返回到该方法的调用的位置,方法正常退出时,调用者的pc寄存器的值作为返回地址值,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址值是由通过异常表来确定,栈帧中一般不会保存这部分信息
当一个方法开始执行后,只要两种方式可以退出这个方法:
执行引擎遇到任意一个方法返回的字节码指令,会有返回值传递给上层的方法调用者,简称正常完成出口
在方法执行过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口
方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便再发生异常时候找到处理异常的代码
实际上,方法的退出实际就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何返回值
StackOverflowError
通过-Xss设置栈的大小
不能,只能延缓
不是
不会
名称 | Error | GC |
---|---|---|
程序计数器 | × | × |
本地方法栈 | √ | × |
虚拟机栈 | √ | × |
堆 | √ | √ |
方法区 | √ | √ |
具体问题具体分析
以StringBuilder为例:
一个native方法就是一个Java调用非Java代码的接口,该方法的实现是由非Java语言实现的,其作用是融合不同的编程语言为Java所用初衷是融合c/c++,其次因为操作系统大多数也是由c实现的,如果用c编写的代码,其运行效率也高,这就是使用本地方法的主要原因(与外界环境交互)
标识符native可以与所有其他的Java标识符连用,但是abstract除外
Java虚拟机是用来管理Java方法的调用,而本地方法栈是用来管理本地方法的调用
本地方法栈是线程私有的
运行本地方法栈实现成固定大小或者动态扩展(与虚拟机栈相同)
本地方法是由c语言实现的
具体做法是通过在本地方法栈中登录本地方法,在执行引擎执行时加载本地方法库
当某个线程调用本地方法时,它就进入了一个全新的且不受虚拟机限制的世界,它和虚拟机拥有同样的权限
并不是所有的虚拟机都支持本地方法,因为Java没有明确要求本地方法栈的使用语言,具体实现方式,数据结构等,如果不支持本地方法也就没有要本地方法栈的必要了
在HotSpot中,将本地方法栈和虚拟机中合二为一
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dUctquOK-1618969670225)(JVM.assets/image-20210420132751120.png)]
转载地址:http://mrgh.baihongyu.com/