JVM学习笔记之四

问题和知识点

        • 1.操作数栈代码追踪分析
        • 2.栈顶缓存技术(Top-of-Stack Cashing)技术(了解即可)
        • 3.动态链接
        • 4.为什么需要常量池?
        • 5.方法的调用
        • 6.
        • 7.动态类型语言和静态类型语言
        • 8.Java语言中方法重写的本质
        • 9.非虚方法
        • 10.虚方法表
        • 11.方法返回地址
        • 12.栈的面试题
        • 13.何为线程安全?
        • 14.栈帧的一些附加信息

1.操作数栈代码追踪分析

样例代码:

public void testAddOperation(){
	byte i=15;
	int j=8;
	int k=i+j;
}

样例分析示意图:
JVM学习笔记之四_第1张图片

2.栈顶缓存技术(Top-of-Stack Cashing)技术(了解即可)

基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必须需要使用更多的入栈和出栈指令,这同时也意味着将需要更多的指令分派次数和内存读写次数。
由于操作数是存储在内存中的,因此频繁的执行内存读写操作必然会影响执行速度,为了解决这个问题。Hot spot JVM的设计者提出了栈顶缓存(ToS,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU继承器中,以降低对内存的读/写次数,提升执行引擎的执行效率。

3.动态链接

JVM学习笔记之四_第2张图片

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(invokevirtual)。
在Java源文件被编译到字节码文件中时,所有的变量和方法引用的作为符号引用保存在class文件的常量池中,比如:描述一个方法调用了另外的其他的方法时,就是通过常量词中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转化为调用方法的直接引用。

4.为什么需要常量池?

常量池的作用,就是为了提供一些符号和常量,便于指令的识别。

5.方法的调用

在JVM中将符号引用转化为调用方法的直接引用与方法的绑定机制有关。
静态链接
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译器可知,且运行期保持不变时,这种情况下将调用方法的符号引用转化为直接引用的过程称之为静态连接。
动态链接
如果被调用的方法在编译器无法被确定下来,也就是说只能够在程序运行期将调用方法的符号引用转化为直接引用,由于这种引用转化过程具备动态性,因此也称之为动态链接。
早期绑定
早期绑定就是指调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一种,因此也就可以使用静态链接的方式将符号引用转化为直接引用。
晚期绑定
如果被调用的方法在编译期无法确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式就被称之为晚期绑定。

package com.atguigu.java2;

/**
 * 说明早期绑定和晚期绑定的例子
 * @author shkstart
 * @create 2020 上午 11:59
 */
class Animal{

    public void eat(){
        System.out.println("动物进食");
    }
}
interface Huntable{
    void hunt();
}
class Dog extends Animal implements Huntable{
    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }

    @Override
    public void hunt() {
        System.out.println("捕食耗子,多管闲事");
    }
}

class Cat extends Animal implements Huntable{

    public Cat(){
        super();//表现为:早期绑定
    }

    public Cat(String name){
        this();//表现为:早期绑定
    }

    @Override
    public void eat() {
        super.eat();//表现为:早期绑定
        System.out.println("猫吃鱼");
    }

    @Override
    public void hunt() {
        System.out.println("捕食耗子,天经地义");
    }
}
public class AnimalTest {
    public void showAnimal(Animal animal){
        animal.eat();//表现为:晚期绑定
    }
    public void showHunt(Huntable h){
        h.hunt();//表现为:晚期绑定
    }
}

6.

虚拟机中提供了以下几条方法调用指令
普通调用指令:
1.invokestatic:调用静态方法,解析阶段确定唯一的方法版本。
2.invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本。
3.invokevirtual:调用所有虚方法。
3.invokeinterface:调用接口方法
动态调用指令:
5.invokedynamic:动态解析出需要调用的方法,然后执行。
前4条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokeddynamic指令则支持用户确定方法版本,其中invokestatic指令和invokespecial调用的方法被称为非虚方法,其余的(final修饰的除外)称为虚方法。
Java字节码指令一直比较稳定。一直到Java7中增加了一个invokedynamic指令,这是Java为了实现动态类型语言支持而做的一种改进。
但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来生成invokedynamic之内,直到Java8的Lambda表达式出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。
Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟之中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。

7.动态类型语言和静态类型语言

动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者的就是静态语言,反之是动态语言类型。
静态类型语言是判断变量自身的类型信息,动态类型语言是判断变量值的类型

8.Java语言中方法重写的本质

1.找到操作数栈顶的第1个元素所执行的对象实际类型,记作C。
2.如果在类型c中找到常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过则返回java.lang.IllegalAccessError错误。
我:
3.否则,按照继承关系,从下往上依次对c的各类父类进行第2步的搜索和验证过程。
4.如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
java.lang.IllegalAccessError介绍:
程序试图访问或修改一个属性或调用一个方法,这个属性和方法你没有权限访问,一般的这个会引起编译器异常,这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
在面向对象的编程中,会很频繁的使用到动态分派,如果每次动态分配的过程都要重新在类的方法元数据中描述合适的目标的话,就可能影响到执行效率,因此为了提高性能,jvm采用在类的方法去建立一个虚方法表(virtual method table)(非虚方法不会出现在表中),来实现使用索引来代替查询。
每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
那么虚方法表什么时候被创建?
虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完成。

9.非虚方法

如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法被称为非虚方法。
静态方法,私有方法,final方法,实例构造器,父类方法都是非虚方法,其他方法成为虚方法。

10.虚方法表

JVM学习笔记之四_第3张图片

11.方法返回地址

存放调用该方法的PC寄存器的值。
一个方法的结束有两种方式
1.正常执行完成
2.出现未处理的异常,非正常退出。
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作返回地址,即调用该方法的指令的下一条指令的地址,而通过异常退出的,返回地址是通过异常表来确定的,战争中一般不会保存这种部分信息。
通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
3.当一个方法开始执行后,只有两种方式可以退出这个方法
执行引擎遇到任何一个方法返回的字段指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口。
一个方法在正常调用成之后,究竟需要使用哪一个方法返回指令,还需要根据方法返回值的实际数据类型而定
在字节码指令中,返回指令包含ireturn(当返回值是boolean、byte、char、short和int类型时使用),lreturn、freturn、dreturn以及areturn,另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。
在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只有在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口。
方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常时找到处理异常的代码。
图片7
本质上,方法的退出就是当前栈帧出栈的过程,此时,需要恢复上层方法的局部变量表,操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC继承器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上曾调用者产生任何的返回值。

12.栈的面试题

1.举例栈溢出的情况?
StackOverflowError
不停的调用main方法
通过-Xss设置栈的大小:OOM
2.调整栈大小,就能保证不出现溢出吗?
不能,定义的稍微大一点
3.分配的栈的内存越大越好吗?
不是
4.垃圾回收是否涉及到虚拟机栈?
不涉及
5.方法中定义的局部变量是否线程安全?
是线程安全的

13.何为线程安全?

如果只有一个线程可以操作此数据,则必是线程安全的。
如果有多个线程操作数据,则此数据是共享数据,如果不考虑同步机制的话,则存在线程安全问题。

14.栈帧的一些附加信息

栈桢中还允许携带与Java虚拟机实现相关的一些附加信息,例如:对程序调试提供支持的信息。

你可能感兴趣的:(JVM学习笔记之四)