JVM原理解析

1. JVM结构图

2. 虚拟机栈

启动一个新的线程,jvm虚拟机都会分配一个java栈,用于存储当前线程的运行状态。单位:栈帧,以栈帧为单位的入栈和出栈,一个方法对应一个栈帧

每当线程调用一个Java方法时,虚拟机都会在该线程的Java栈中压入一个新帧。而这个新帧自然就成为了当前帧。在执行这个方法时,它使用这个帧来存储参数、局部变量、中间运算结果等数据。

Java方法可以以两种方式完成。一种通过return返回的,称为正常返回;一种是通过抛出异常而异常终止的。不管以哪种方式返回,虚拟机都会将当前帧弹出Java栈然后释放掉,这样上一个方法的帧就成为当前帧了。

Java帧上的所有数据都是此线程私有的。任何线程都不能访问另一个线程的栈数据,因此我们不需要考虑多线程情况下栈数据的访问同步问题。当一个线程调用一个方法时,方法的的局部变量保存在调用线程Java栈的帧中。只有一个线程能总是访问那些局部变量,即调用方法的线程。

2.1 局部变量数组,包含参数和局部变量

堆栈帧的局部变量部分被组织为基于零的单词阵列。

它包含该方法的所有参数和局部变量。

阵列中的每个插槽或条目都是4个字节。

扫描二维码关注公众号,回复: 5813092 查看本文章

int,float和reference类型的值在数组中占用1个入口或插槽,即4个字节。

double和long的值占用阵列中的2个连续条目,即总共8个字节。

字节,short和char值将在存储并占用1个时隙(即4个字节)之前转换为int类型。

但是,存储布尔值的方式从jvm到jvm不等。但是大多数jvm在本地变量数组中为布尔值提供了1个槽。参数首先按照声明的顺序放置在局部变量数组中。

以下程序的局部变量表

public class StackDemo {
    
    //静态方法
    public static int runStatic(int i, long l, float f, Object o, byte b) {
        return 0;
    }

    //实例方法
    public int runInstance(char c, short s, boolean b) {
        return 0;
    }

}

2.2 操作数栈

jvm没有寄存器的概念,使用操作数栈来完成基本的数学操作

public static int add(int a,int b){
        int c=0;
        c=a+b;
        return c;
    }

操作的指令如下

  0:   iconst_0 // 0压栈

  1:   istore_2 // 弹出int,存放于局部变量2

  2:   iload_0  // 把局部变量0压栈

  3:   iload_1 // 局部变量1压栈

  4:   iadd      //弹出2个变量,求和,结果压栈

  5:   istore_2 //弹出结果,放于局部变量2

  6:   iload_2  //局部变量2压栈

  7:   ireturn   //返回

2.3 动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。

2.4 返回地址

当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。

无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调

用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

3. 方法区

4. 堆

5. 程序计数器

6. 本地方法栈

7. JVM运行的流程

8. 方法调用

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。

Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于之前说的直接引用)。

java虚拟机提供的方法调用字节码指令

  invokestatic:调用静态方法。

  invokespecial:调用实例构造器方法、私有方法和父类方法。

  invokevirtual:调用所有的虚方法。还有final修饰的方法。

  invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。

  invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而- - invokedynamic指令的分派逻辑是由用户所设定

的引导方法决定的。

分派调用:是“重载”和“重写”在虚拟机中的实现过程。

分派调用可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派。这两类分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况。

静态分派:重载,overload,编译阶段的选择过程

编译器在重载时是通过参数的静态类型(Human)而不是实际类型(Man或Woman)作为判定依据的。并且静态类型是编译期可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个

重载版本,所以选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里的两条invokevirtual指令的参数中。

注意:静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。另外,编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一的”,往往只能确定一个“更加合适

的”版本。

public class StaticDispatch {

        static abstract class Human{
        }
        static class Man extends Human{
        }
        static class Woman extends Human{
        }
        public void sayHello(Human guy){
            System.out.println("hello,guy!");
        }
        public void sayHello(Man guy){
            System.out.println("hello,gentleman!");
        }
        public void sayHello(Woman guy){
            System.out.println("hello,lady!");
        }
        public static void main(String[]args){
            Human man=new Man();
            Human woman=new Woman();
            StaticDispatch sr=new StaticDispatch();
            sr.sayHello(man);
            sr.sayHello(woman);
        }

}

运行结果:

hello,guy!
hello,guy!

修改:

sr.sayHello((Man)man);

sr.sayHello((Woman)woman);

输出结果:

hello,gentleman!
hello,lady!

动态分派:重写,override,运行阶段的选择过程

public class DynamicDispatch {

    static abstract class Human{
        protected abstract void sayHello();
    }
    static class Man extends Human{
        @Override
        protected void sayHello(){
            System.out.println("man say hello");
        }
    }
    static class Woman extends Human{
        @Override
        protected void sayHello(){
            System.out.println("woman say hello");
        }
    }
    public static void main(String[]args){
        Human man=new Man();
        Human woman=new Woman();
        man.sayHello();
        woman.sayHello();
        man=new Woman();
        man.sayHello();
    }
}

单分派与多分派:

方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少宗量,可以将分派划分为单分派和多分派。

Java语言是一门静态多分派、动态单分派语言:

Java在静态分派时选择目标方法的依赖有两点:一是静态类型,二是方法参数,所以Java的静态分派属于多分派。

Java在动态分派时,由于编译期已经确定了目标方法的签名,所以此时虚拟机不会关心方法参数是什么,而只会关心方法的接收者的实际类型。因为只有一个宗量作为选择依据,所以Java的动态分派属于

单分派。

public class Dispatch {

    static class QQ{}
    static class _360 {}
    public static class Father{
        public void hardChoice(QQ arg){
            System.out.println("father choose qq");
        }
        public void hardChoice(_360 arg){
            System.out.println("father choose 360");
        }
    }
    public static class Son extends Father{
        public void hardChoice(QQ arg){
            System.out.println("son choose qq");
        }
        public void hardChoice(_360 arg){
            System.out.println("son choose 360");
        }
    }
    public static void main(String[]args){
        Father father=new Father();
        Father son=new Son();
        father.hardChoice(new _360());
        son.hardChoice(new QQ());
    }
}

动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正地进行如此频繁的搜

索。面对这种情况,最常用的“稳定优化”手段就是为类在方法区中建立一个虚方法表(Vritual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表——Inteface Method Table,简

称itable),使用虚方法表索引来代替元数据查找以提高性能。

9. 类的加载过程

9.1. 加载

简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例

(Java虚拟机规范并没有明确要求一定要存储在堆区中,只是hotspot选择将Class存储在哪个方法区中),这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。

9.2. 链接

链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段。

9.2.1 验证

验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。

格式验证:验证是否符合class文件规范

语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法视频被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)

操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过富豪引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰

符是否允许访问等)

9.2.2 准备

为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内

被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值

9.2.3 解析

将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。

可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)

9.3. 初始化

将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会

执行static代码块中定义的所有操作。

所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>方法,即类/接口初始化方法。该方法的作用就是初始化一个中的变量,使

用用户指定的值覆盖之前在准备阶段里设定的初始值。任何invoke之类的字节码都无法调用<clinit>方法,因为该方法只能在类加载的过程中由JVM调用。

如果父类还没有被初始化,那么优先对父类初始化,但在<clinit>方法内部不会显示调用父类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的父类<clinit>方法已经被执行。

JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正

在等待的其他线程。

 参考博客:https://blog.csdn.net/wutao1155/article/details/79340135

https://blog.csdn.net/xyh930929/article/details/84067186

https://www.cnblogs.com/wade-luffy/p/5753057.html#_label1

https://blog.csdn.net/csdnliuxin123524/article/details/81303711

猜你喜欢

转载自www.cnblogs.com/feng-ying/p/10671816.html
今日推荐