美文网首页
2020-10-12---深入理解java虚拟机总结

2020-10-12---深入理解java虚拟机总结

作者: 李霖神谷 | 来源:发表于2020-10-13 09:28 被阅读0次

    一、jvm数据区域
    1.Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。


    image.png

    程序计数器:
    (1) java多线程中是通过线程切换的来实现的,在切换到下一个线程过程中需要记录当前线程的正在执行的虚拟机字节码指令地址(执行Native方法时计数器为Undefined),CPU切换回来时会按照计数器记录的行数继续执行。
    (2) 每一个线程都有自己独自的程序计数器、并且是独享、互不影响的。
    (3) 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

    java虚拟机栈
    (1)虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame[插图])用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

    (2)在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

    本地方法栈:
    本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

    java堆:
    (1) 对象的实例、数组都是存储在此内存区域。
    (2) 它是java垃圾收集器管理的区域。
    (3) 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

    方法区:
    方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

    运行时常量池:
    常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

    对象访问:
    简单的Object o=new Object();首先“Object obj”这部分的语义将会反映到Java栈的本地变量表中,作为一个reference类型数据出现。而“newObject()”这部分的语义将会反映到Java堆中,形成一块存储了Object类型所有实例数据值的结构化内存。


    image.png

    二、垃圾回收
    1..判断是否是垃圾的算法:
    *引用计数法:
    给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。
    缺点:不能解决循环引用的问题。

    *根搜索算法:
    在主流的商用程序语言中(Java和C#,甚至包括前面提到的古老的Lisp),都是使用根搜索算法(GC Roots Tracing)判定对象是否存活的。这个算法的基本思路就是通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
    2.方法区的垃圾回收:
    方法区(永久代)的垃圾回收分为“废弃的常量、无用的类”。回收废弃的常量和回收堆中的对象类似、如果没有对这个常量的引用之后就会被回收。无用的类回收起来很苛刻如下:
    (1) 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
    (2) 加载该类的ClassLoader已经被回收。
    (3) 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

    3.垃圾收集算法:
    3.1标记清除算法:
    标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。


    image.png

    3.2复制算法:
    它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
    很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。


    image.png
    3.3标记整理算法:
    该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动
    image.png
    *分代收集算法:

    根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法

    jvm将新生代分为三个区域、一个end区和两个suvivor区。新生代的采用复制算法。新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

    4.垃圾回收策略:
    4.1. 对象优先在 Eden 分配
    大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。

    4.2. 大对象直接进入老年代
    大对象是指 需要连续内存空间的对象 ,最典型的大对象是那种很长的字符串以及数组。
    经常出现大对象会 提前触发垃圾收 集以获取足够的连续空间分配给大对象。

    4.3. 长期存活的对象进入老年代
    为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活, 将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
    -XX:MaxTenuringThreshold 用来定义年龄的阈值。

    4.4. 动态对象年龄判定
    虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代, 如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半, 则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

    4.5. 空间分配担保
    在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

    三、虚拟机性能监控与故障处理工具
    虚拟机进程状况工具:

    1. jps命令行工具:
      jps是jdk提供的一个查看当前java进程的小工具, 可以看做是JavaVirtual Machine Process Status Tool的缩写。
      示例:
      jps –l:输出主类或者jar的完全路径名:


      image.png

    jps –v :输出jvm参数:


    image.png

    2.jstat:虚拟机统计信息监视工具:
    是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或远程[插图]虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。

    假设需要每250毫秒查询一次进程2764垃圾收集的状况,一共查询20次,那命令应当是:
    jstat -gc 12008 250 20


    image.png

    S0C:第一个幸存区的大小
    S1C:第二个幸存区的大小
    S0U:第一个幸存区的使用大小
    S1U:第二个幸存区的使用大小
    EC:伊甸园区的大小
    EU:伊甸园区的使用大小
    OC:老年代大小
    OU:老年代使用大小
    MC:方法区大小
    MU:方法区使用大小
    CCSC:压缩类空间大小
    CCSU:压缩类空间使用大小
    YGC:年轻代垃圾回收次数
    YGCT:年轻代垃圾回收消耗时间
    FGC:老年代垃圾回收次数
    FGCT:老年代垃圾回收消耗时间
    GCT:垃圾回收消耗总时间

    堆内存统计:
    jstat -gccapacity 12008

    image.png

    NGCMN:新生代最小容量
    NGCMX:新生代最大容量
    NGC:当前新生代容量
    S0C:第一个幸存区大小
    S1C:第二个幸存区的大小
    EC:伊甸园区的大小
    OGCMN:老年代最小容量
    OGCMX:老年代最大容量
    OGC:当前老年代大小
    OC:当前老年代大小
    MCMN:最小元数据容量
    MCMX:最大元数据容量
    MC:当前元数据空间大小
    CCSMN:最小压缩类空间大小
    CCSMX:最大压缩类空间大小
    CCSC:当前压缩类空间大小
    YGC:年轻代gc次数
    FGC:老年代GC次数

    3.jmap:(Memory Map for Java)用于生成堆转储快照,即Dump文件;还能查询finalize执行队列、java堆和永久代详细信息,比如空间使用率、当前用的是哪种收集器等。
    *jstasck:用于生成虚拟机当前时刻的线程快照(一般称为threaddump或javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。

    jdk可视化工具:
    JDK中除了提供大量的命令行工具外,还有两个功能强大的可视化工具:JConsole和VisualVM,这两个工具是JDK的正式成员,没有被贴上“unsupported andexperimental”的标签。

    4.JConsole:
    它是一款基于JMX的可视化监视和管理的工具。直接在jdk bin目录下启动JConsole程序


    image.png image.png

    四、类文件结构
    1.class文件结构
    1.1.Class文件是一组以8个字节为基础单位的二进制流(可能是磁盘文件,也可能是类加载器直接生成的),各个数据项目严格按照顺序紧凑地排列,中间没有任何分隔符;
    1.2.Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,其中只有两种数据类型:无符号数和表;
    1.3.无符号数属于基本的数据类型,以u1、u2、u4和u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值;
    表是由多个无符号数获取其他表作为数据项构成的复合数据类型,习惯以“_info”结尾;
    1.4.无论是无符号数还是表,当需要描述同一个类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。

    2.具体的类文件结构(下面是简单的打印语句的class文件)
    2.1、魔数和版本
    2.1.1. Class文件的头4个字节,唯一作用是确定文件是否为一个可被虚拟机接受的Class文件,固定为“0xCAFEBABE”。
    2.1.2 第5和第6个字节是次版本号,第7和第8个字节是主版本号(0x0034为52,对应JDK版本1.8);Java的版本号是从45开始的,JDK1.1之后的每一个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容低版本的JDK。

    对应到class文件中就是:


    这里写图片描述

    2.2常量池
    2.2.1常量池的组成:
    常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近Java语言的常量概念,如文本字符串、声明为final的常量等。而符号引用则属于编译原理方面的概念,它包括三方面的内容:
    类和接口的全限定名(Fully Qualified Name);
    字段的名称和描述符(Descriptor);
    方法的名称和描述符;
    Java代码在进行javac编译的时候并不像C和C++那样有连接这一步,而是在虚拟机加载class文件的时候进行动态连接。也就是说,在class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,虚拟机也就无法使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中。
    常量池中的每一项都是一个表,这14个表的开始第一个字节是一个u1类型的tag,用来标识是哪一种常量类型。这14种常量类型所代表的含义如下:


    image.png

    2.3、访问标志
    常量池结束后紧接着的两个字节代表访问标志,用来标识一些类或接口的访问信息,包括:这个Class是类还是接口;是否定义为public;是否定义为abstract;如果是类的话,是否被声明为final等。具体的标志位以及含义如下表:


    这里写图片描述

    2.4.类索引、父类索引与接口索引集合
    在访问标志access_flags后接下来就是类索引(this_class)和父类索引(super_class),这两个数据都是u2类型的,而接下来的接口索引集合是一个u2类型的集合,class文件由这三个数据项来确定类的继承关系。

    2.5.字段表集合
    字段表集合,顾名思义就是Java类中的字段,字段又分为类字段(静态属性)和实例字段(对象属性),那么,在Class文件中是如何保存这些字段的呢?我们可以想一想保存一个字段需要保存它的哪些信息呢?
    答案是:字段的作用域(public、private和protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、是否可被序列化(transient修饰符)、字段的数据类型(基本类型、对象、数组)以及字段名称。

    五、虚拟机加载机制
    上一节我们已经知道了类文件结构,在class文件中描述的各种信息最终都需要加载到虚拟机中之后才能运行和使用。
    1.类的加载过程:
    包括加载、验证、准备、解析、初始化。
    1.1加载:
    在加载阶段,虚拟机需要完成以下三件事情:
    1)通过一个类的全限定名来获取定义此类的二进制字节流。
    2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

    1.2验证:
    这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
    1.2.1.为什么需要验证:
    Class文件并不一定要求用Java源码编译而来,可以使用任何途径,包括用十六进制编辑器直接编写来产生Class文件。在字节码的语言层面上,上述Java代码无法做到的事情都是可以实现的,至少语义上是可以表达出来的。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。
    1.2.2.过程:
    大致上都会完成下面四个阶段的检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

    2.准备:
    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段

    1. 解析:
      解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
      3.1.符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

    3.2. 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

    4.初始化:
    4.1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
    4.2.使用java.lang.reflect包的方法对类进行发射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
    4.3当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    4.4当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

    对于这四种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这四种场景中的行为称为对一个类进行主动引用。除此之外所有引用类的方式,都不会触发初始化,称为被动引用。下面举三个例子来说明被动引用,分别见代码清单7-1、代码清单7-2和代码清单7-3。
    7-1:

    package com.shuai.Util;
    
    class  father{
    static {
        System.out.println("father");
    }
     public  static  String name="shuai";
    
    }
    class  children extends  father{
     static {
         System.out.println("children");
     }
    
    
    
    }
    
    
    public class test3 {
     public static void main(String[] args) {
         System.out.println(children.name);
     }
    
    }
    
    

    上面运行的结果是father shuai不会运行children。当通过子类调用父类的静态代码时不会初始化本身的静态代码块。

    7-2:

    public class test3 {
        public static void main(String[] args) {
            children[] c=new children[10];
        }
    }
    

    使用数组的时候不会初始化。

    7-3:

    class  father{
       static {
           System.out.println("father");
       }
        public  static  final  String HELLOWORD="hello world";
    
    }
    
    
    public class test3 {
        public static void main(String[] args) {
            System.out.println(father.HELLOWORD);
        }
    
    }
    

    上述代码运行之后,也没有输出“father”,这是因为虽然在Java源码中引用了father类中的常量HELLOWORLD,但是在编译阶段将此常量的值“hello world”存储到了thest类的常量池中,对常量father.HELLOWORLD的引用实际都被转化为test类对自身常量池的引用了。也就是说实际上father的Class文件之中并没有father类的符号引用入口,这两个类在编译成Class之后就不存在任何联系了。

    5..双亲委派:
    站在Java虚拟机的角度讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现[插图],是虚拟机自身的一部分;另外一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
    5.1 启动类加载器(Bootstrap ClassLoader):前面已经介绍过,这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
    5.2扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.LauncherExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。 5.3 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc. LauncherAppClassLoader来实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

    5.4 过程:
    双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

    六、虚拟机执行引擎
    在Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各种虚拟机执行引擎的统一外观(Facade)。在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,甚至还可能包含几个不同级别的编译器执行引擎。但从外观上看起来,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。本章将主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行。
    1.运行时栈帧结构
    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)[插图]的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
    1.1局部变量表:
    局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有“导向性”地说明每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。
    局部变量表中的Slot是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那么这个变量对应的Slot就可以交给其他变量使用。这样的设计不仅仅是为了节省栈空间,在某些情况下Slot的复用会直接影响到系统的垃圾收集行为。
    示例:

    public class Test4 {
       public static void main(String[] args) {
               byte [] a=new byte[64*1024*1024];
               System.gc();
       }
    }
    

    通过设置虚拟机参数-verbose:gc看运行结果:


    image.png

    这是因为当执行system.gc的时候a参数还在当前作用域,此时无法进行垃圾回收。

     public static void main(String[] args) {
           {
               byte[] a = new byte[64 * 1024 * 1024];
    
           }
           int b=0;
           System.gc();
           }
    
    image.png

    此时可以看到,垃圾会被回收。因为slot被b复用并且清空。
    1.2操作数栈:
    操作数栈也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。
    1.3动态链接
    每个栈帧都包含一个指向运行时常量池[插图]中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
    1.4方法返回地址
    当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者
    另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。
    一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
    2.方法调用:
    方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。
    2.1.解析:
    在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。
    在Java语言中,符合“编译期可知,运行期不可变”这个要求的方法主要有静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法都不可能通过继承或别的方式重写出其他版本,因此它们都适合在类加载阶段进行解析。与之相对应,在Java虚拟机里面提供了四条方法调用字节码指令[插图],分别是:□ invokestatic:调用静态方法。□ invokespecial:调用实例构造器<init>方法、私有方法和父类方法。□ invokevirtual:调用所有的虚方法。□ invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器和父类方法四类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法,与之相反,其他方法就称为虚方法。

    2.2分派:
    解析调用是一个静态的过程,在编译期间就完全确定,不会延迟到运行期再去完成。
    分派调用则可能是静态的也可能是动态的。
    根据分派宗数量可分为单分派和多分派。这两类分派又可两两组合成:静态单分派,静态多分派,动态单分派和动态多分派4中分派组合。
    分派体现了Java的多态性,如“重载”和“重写”。
    静态分派:
    所有依赖静态类型(类型的引用)来定位方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
    动态分派:
    我们把在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

    相关文章

      网友评论

          本文标题:2020-10-12---深入理解java虚拟机总结

          本文链接:https://www.haomeiwen.com/subject/mznupktx.html