1. 总体结构

2. 基本内容

2.1. 线程独立

每个线程对应各自的虚拟机栈,线程独立

2.2. 大小设置

3. 栈帧

3.1. 基本内容

3.2. 运行原理

3.3. 本地变量表

3.3.1. 基本内容

用来存储形参、局部变量、返回值等内容

3.3.2. 作用影响

主要影响栈帧大小,进而影响栈帧个数,以及是否容易发生溢出

3.3.3. Slot 运行原理

本地变量表 (局部变量表) 是重要的垃圾回收根节点

3.4. 操作数栈

3.5. 动态链接⭐️🔴

3.5.1. class 文件常量池和运行时常量池

性能调优专题-基础-3、JVM-方法区和3个池子

3.5.2. 静态绑定和动态绑定

将所调用方法的符号引用转为直接引用的方式有 2 种:静态绑定和动态绑定

3.5.3. 虚方法和非虚方法

静态方法不能重写: https://blog.csdn.net/gao_zhennan/article/details/72892946

JDK7新增加invokedynamic方法

invokestatic, invokespecial 指令调用的方法为非虚方法
invokevirtual, invokeinterface, invokedynamic 指令调用的方法为虚方法

3.5.3.1. 版本变化

但是!作为私有方法的 private Method 方法,却在字节码中被编译为使用 invokevirtrual 指令来调用。这是为什么呢?

笔者查阅资料后,发现在 JEP181 中,对方法调用字节码指令进行了一定程度上的修改。在 Java11 版本及以后,嵌套类之间的私有方法的访问权限控制,就从编译期转移到了运行时,从而这样的私有方法也被使用 invokevirtual 指令来调用,

总而言之,在 Java11 及以后,类中的私有方法往往用 invokevirtual 来调用,接口中的私有方法往往用 invokeinterface 调用,invokespecial 往往仅用于实例构造器方法和父类中的方法。

3.6. 方法出口

异常处理参考

4. 多态的本质⭐️🔴

4.1. 动态分派

方法重写的本质
[[../../../../cubox/006-ChromeCapture/JVM是如何实现多态的 个人博客]]

invokevirtual 指令的运行时解析过程大致分为以下几个步骤:

  • 找到操作数栈顶的第一个元素指向的对象的实际类型,记为 C。
  • 到类型 C 的方法元数据中进行搜索,如果找到与常量池中描述符和简单名称一样的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,返回 java.lang.IllegalAccessError 异常。
  • 否则,按照继承关系从下到上依次对 C 的各个父类进行搜索和验证。
  • 如果还没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常。

由于 invokevirtual 指令执行的第一步就是在运行期间确定接收者的实际类型,所以两次调用中的 invokevirtual 指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是 Java 语言中方法重写的本质。这种在运行期根据实际类型确定方法执行版本的分派过程叫做动态分派。❕%%
0917-🏡⭐️◼️invokevirtual 方法在寻找方法过程中,可以将常量池中的符号引用,根据实际情况解析到不同的直接引用上,这就是多态的本质◼️⭐️-point-202301300917%%

案例 :[[内功心法专题-设计模式-22、访问者模式#4 双分派]]

4.2. JVM 实现

虚方法表中存放着各个方法的 实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口 和父类相同方法的地址入口 是一致的,都指向父类的实现入口。如果子类重写了父类的方法,子类方法表中的地址会替换为 指向子类实现版本的入口地址

为了程序实现上的方便,具有相同签名的方法,在父类和子类的虚方法表中都应该具有一样的索引号,这样当类型变换时,仅仅需要 变更查找的方法表,就可以从不同的虚方法表中按索引转换出所需的入口地址。

4.2.1. 方法表

4.2.1.1. 方法表 (Method Table)

介绍了虚分派,接下来介绍是它的一种实现方法—方法表。类似于 C++ 虚函数表 vtbl。

在有的 JVM 实现中,使用了方法表机制实现虚分派,而有时候,为了节省内存可能不采用方法表的实现。

不要被方法表这个名字迷惑,它并不是记录所有方法的表。它是为虚分派服务,不会记录用 invokestatic 调用的静态方法和用 invokespecial 调用的构造函数和私有方法。

JVM 会在链接类的过程中,给类分配相应的方法表内存空间。每个类对应一个方法表。这些都是存在于 method area 区中的。这里与 C++ 略有不同,C++ 中每个对象的第一个指针就是指向了相应的虚函数表。而 Java 中每个实例对象 (InstanceOopDesc) 通过对象头中的 MarkWord中的Klass Pointer 索引到对应的类 (InstanceKlass),在对应的类数据中对应一个方法表 (vtable)。 对象创建-1、对象内存

4.2.1.2. 方法表的一种实现

父类的方法比子类的方法先得到解析,即父类的方法相对于子类的方法位于表的前列。

表中每项对应于一个方法,索引到实际方法的实际代码上。如果子类重写了父类中某个方法的代码,则该方法第一次出现的位置的索引更换到子类的实现代码上,而不会在方法表中出现新的项。

JVM 运行时,当代码索引到一个方法时,是根据它在方法表中的偏移量来实现访问。(第一次执行到调用指令时,会执行解释,将符号索引替换为对应的直接索引)。

4.2.1.3. invokeinterface 与 invokevirtual 的比较

当使用 invokeinterface 来调用方法时,由于不同的类可以实现同一 interface,我们无法确定在某个类中的 interface 中的方法处在哪个位置。于是,也就无法解释 CONSTANT_interfaceMethodref-info 为直接索引,而必须每次都执行一次在 methodtable 中的搜索了。所以,在这种实现中,通过 invokeinterface 访问方法比通过 invokevirtual 访问明显慢很多。

4.3. C++ 实现

4.3.1. 虚函数

虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为 动态链接,或 后期绑定

编译器在编译的时候,发现 Father 类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表 (即 vtable),该表是一个一维数组,在这个数组中存放每个虚函数的地址,

每个包含虚函数的类创建一个虚表,而 Java 中万物皆对象,总不能每个对象都对应一个虚表,会浪费很多空间,所以 Java 中虚函数表是放在类型信息中的

%%
▶6.🏡⭐️◼️Java 中虚函数表放在哪里 ?🔜MSTM📝 方法区中的类型信息中,准确的说是方法区中的 InstanceKlass 中◼️⭐️-point-20230226-2134%%

那么如何定位虚表呢?编译器另外还为每个对象提供了一个虚表指针 (即 vptr),这个指针指向了对象所属类的虚表,在程序运行时,根据对象的类型去初始化 vptr,从而让 vptr 正确的指向了所属类的虚表,从而在调用虚函数的时候,能够找到正确的函数,对于第二段代码程序,由于 pFather 实际指向的对象类型是 Son,因此 vptr 指向的 Son 类的 vtable,当调用 pFather->Son() 时,根据虚表中的函数地址找到的就是 Son 类的 Say() 函数.

5. 实战经验

5.1. 调整参数 -Xss

当我们定义的方法参数和局部变量过多,字节过大,考虑到可能会导致栈深度变小,所以要把线程栈大小调大,就能支持更多的方法调用,也就是能存储更多的栈帧

https://www.cnblogs.com/rocky-fang/p/8367018.html

https://www.itzhai.com/jvm/how-stack-frame-can-a-thread-hold.html

java -Xss2m -cp “C:\Users\rocky fang\Documents\mycode” JavaStackTest

结论:

  • 随着线程栈的大小越大,能够支持越多的方法调用,也即是能够存储更多的栈帧;
  • 局部变量表内容越多,那么栈帧就越大,栈深度就越小。

我们在评审写代码的时候,发现了堆栈溢出,可以查看下对应类的本地变量表,是不是太多了,可不可以优化下代码,比如,去掉不必要的参数、变量,以及变量是否太大。或者加大下线程栈的大小,以增加栈的深度。

image-20200130215912480

因为物理内存是一定的,所以单个栈太大,导致并发线程数变少

典型案例:

部门里有员工 list 属性,员工里有部门属性,没有加单向引用设置@JsonIgnore,导致循环引用,出现栈内存溢出

image-20200409182716136

5.2. 线程诊断

cup 占用居高不下

1.使用 top 查看进程号

image-20200130221806323

2.查看线程号

image-20200130221910911

image-20200130222121176

3.将线程号换算成 16 进制找到代码行数

image-20200130222654058

image-20200130222722187

5.3. 死锁

使用 jstack 分析死锁

6. 参考与感谢

6.1. 尚硅谷 - 宋红康

https://www.bilibili.com/video/BV1PJ411n7xZ?p=57&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204

对象创建-7、JVM-堆

6.2. 网络笔记

c++ 教程: https://www.runoob.com/cplusplus/cpp-polymorphism.html
[[../../../../cubox/006-ChromeCapture/C++ 多态的实现及原理 - evilsnake - 博客园]]