精华内容
下载资源
问答
  • 前言 有一天逛知乎的时候,遇到了这样的问题 public static void main(String[] ...在思考一会之后,决定还是通过字节码指令来看看这两行代码是怎么运行的。 将两行代码拷贝到Test.java中,切换到此文件目录,执行以

    前言

    有一天逛知乎的时候,遇到了这样的问题:

    public static void main(String[] args) {
    	int i = 1;
    	i += i += ++i + 2.6 + i;
    }
    // 为什么i最后的结果是8?
    

    很简单的两行代码,如果是你遇到这样的问题,你会怎样去把问题解释清楚?是利用Java运算符顺序将式子拆解,然后一步步运算,还是其他什么办法?
    在思索一会儿之后,决定还是通过字节码指令来看看这两行代码是怎么运行的。
    将两行代码拷贝到Test.java中,执行以下指令输出字节码:

    javac Test.java
    javap -c Test.class
    

    字节码输出结果如下:
    在这里插入图片描述
    如果是之前对字节码没有了解的话,可以去搜一下字节码指令的资料,或者去《深入理解Java虚拟机》这本书去找附录b 字节码指令表
    接下来翻译一下字节码:

    public static void main(java.lang.String[]);
        Code:
           0: iconst_1   // 将1放入操作数栈顶
           1: istore_1   // 将操作数栈顶的i出栈并存放到局部变量表中slot中
           2: iload_1    // 从slot中取出i并放入操作数栈顶,此时栈内容为1
           3: iload_1    // 从slot取出i再次放入操作数栈顶,此时栈内容为1 1
           4: i2d        // 将操作数栈顶i的int转换为double类型,此时栈内容为1.0 1
           5: iinc       // ++i自增,此时slot中的i的值为2,记住,是2
           8: iload_1    // 从slot取出i放入栈顶,此时栈内容为2 1.0 1
           9: i2d        // 将栈顶的int类型转换为double类型
          10: ldc2_w     // 将2.6放入栈顶,此时栈内容为2.6 2.0 1.0 1
          13: dadd       // 将栈顶的两个double相加,并把结果放入栈顶,此时栈内容为 4.6 1.0 1 
          14: iload_1   // 将slot中的i放入栈顶,此时栈内容为 2 4.6 1.0 1 
          15: i2d       // 将栈顶的int类型转换为double类型,此时栈内容 2.0 4.6 1.0 1
          16: dadd      // 将栈顶的两个double相加,并把结果放入栈顶,此时栈内容为 6.6 1.0 1
          17: dadd     // 将栈顶的两个double相加,并把结果放入栈顶,此时栈内容为 7.6 1
          18: d2i       // 将栈顶的double转换为int类型7.6变成7,此时栈内容为7 1
          19: dup       // 复制栈顶数值并压栈,此时栈内容为 7 7 1
          20: istore_1  // 将i= i + (++i + 2.6 + i)的结果,i的值即7放入slot中,并出栈,此时栈内容7 1
          21: iadd      // 将栈顶两个int相加,此时栈内容为8
          22: istore_1  // i = i + (i + (++i + 2.6 + i))结果,即i的值即8放入slot,并出栈
          23: return    // 返回8
    

    上面的字节码注释就是我的答案,一步一步的将运算步骤进行了拆解。

    栈桢

    上面提到的局部变量表和slot是什么?
    这里就不得不提栈桢了。当我们执行一个方法的时候,虚拟机就会在线程私有的虚拟机栈栈顶创建一个栈桢来对应此方法。所以栈桢是方法调用和执行时的数据结构,包括局部变量表、操作数栈、动态连接等。一个方法从开始调用到执行完成,对应了一个栈桢在虚拟机栈中入栈和出栈的过程。
    在这里插入图片描述

    局部变量表

    局部变量表是用于存放方法参数和方法局部变量的空间,里面由一个个Slot组成。代码在编译成字节码文件的时候,就可以确定局部变量表的大小。除了64位的long和double类型占用2个slot外,其他的数据类型占用1个slot。

    操作数栈

    在方法执行过程中,通过各种字节码指令往操作数栈中写入和读取数据,即入栈和出栈。数据的运算基于操作栈进行,例如iadd可以将栈顶的两个int类型进行加法运算。

    动态连接

    每个栈桢都会包含一个指向运行时常量池中该栈桢对应方法的符号引用,持有这个引用是为了支持方法调用过程的动态连接。将符号引用在运行期解析成直接引用的过程,叫做动态连接。

    方法返回地址

    方法会在以下两种情况进行退出:当遇到方法返回字节码指令时,根据方法逻辑决定是否会有返回值返回给调用者,然后正常退出方法;当遇到异常时,并且没有使用try来捕获异常,导致代码异常退出。不论怎么样退出,都要返回到调用方法时的位置,栈桢中会保存方法返回时的一些信息,来恢复上层方法的执行状态。

    扩展应用

    最近网上比较流行的一个问题,为什么Integet类型的100 == 100返回true,200 == 200返回false?众所周知,==比较的是两个对象的地址,为什么两个对象的地址能一样的,有一点amazing。那就让我们来探索一下:
    源码如下:

     public static void main(String[] args) {
            Integer a = 100;
            Integer b = 100;
            Integer c = 200;
            Integer d = 200;
            System.out.println(a == b);
            System.out.println(c == d);
        }
    

    输出结果:
    在这里插入图片描述
    字节码如下:

    public static void main(java.lang.String[]);
        Code:
           0: bipush        100
           2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
           5: astore_1
           6: bipush        100
           8: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
          11: astore_2
          12: sipush        200
          15: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
          18: astore_3
          19: sipush        200
          22: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
          25: astore        4
          27: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
          30: aload_1
          31: aload_2
          32: if_acmpne     39
          35: iconst_1
          36: goto          40
          39: iconst_0
          40: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V
          43: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
          46: aload_3
          47: aload         4
          49: if_acmpne     56
          52: iconst_1
          53: goto          57
          56: iconst_0
          57: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V
          60: return
    

    从字节码中可以看到a、b、c、d赋值的时候都是通过invokestatic字节码指令调用了Integer.valueOf()方法。但是不同的是,在给a、b赋值时候字节码指令是bipush,是将单字节的整型常量值(-128 - 127)压入操作数栈顶;给c、d赋值时候字节码指令是sipush,是将int类型的常量值压入操作数栈顶。为什么同样是Integer类型,一个是1个字节,一个是4个字节呢?
    那我们来探索一下Integer的valueOf()方法:
    在这里插入图片描述
    这个方法调用了重载的valueOf(),代码如下:
    在这里插入图片描述
    这个IntegerCache是Integer的一个静态内部类,会对你初始化的Integer的值进行判断,当这个值在low和high之间,即-128 ~ 127,不会重新在堆中分配内存创建Integer对象,会直接从cache数组中返回一个Integer对象,所以a == b。
    IntegerCache源码如下:
    在这里插入图片描述

    结语

    文章可能对栈桢描述的并没有那么详细,主要还是让大家大致了解一下栈桢基本的功能作用,普及一下字节码的作用。当我们对一些代码无法理解的时候,换个角度去理解可能会豁然开朗。

    在这里插入图片描述

    展开全文
  • 以标准 JDK 中的 HotSpot 虚拟机为例,从虚拟机以及底层硬件两个角度来看Java 虚拟机具体是怎么运行 Java 字节码的。 虚拟机视角 从虚拟机视角来看,执行 Java 代码首先需要将它编译而成的 class 文件加载到 ...

    以标准 JDK 中的 HotSpot 虚拟机为例,从虚拟机以及底层硬件两个角度来看Java 虚拟机具体是怎么运行 Java 字节码的。

     

    虚拟机视角

    从虚拟机视角来看,执行 Java 代码首先需要将它编译而成的 class 文件加载到 Java 虚拟机中。

    加载后的 Java 类会被存放于方法区(Method Area)中。实际运行时,虚拟机会执行方法区内的代码。

    在运行过程中,每当调用进入一个 Java 方法,Java 虚拟机会在当前线程的 Java 方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。

    这个栈帧的大小是提前计算好的,而且 Java 虚拟机不要求栈帧在内存空间里连续分布。 

    当退出当前执行的方法时,不管是正常返回还是异常返回,Java 虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。

     

    硬件视角

    从硬件视角来看,Java 字节码无法直接执行。因此,Java 虚拟机需要将字节码翻译成机器码。

    在 HotSpot 里面,上述翻译过程有两种形式:第一种是解释执行,即逐条将字节码翻译成机器码并执行;

    第二种是即时编译(Just-In-Time compilation,JIT),即将一个方法中包含的所有字节码编译成机器码后再执行。

    前者的优势在于无需等待编译,而后者的优势在于实际运行速度更快。

     HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。

    它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。

     

    即时编译

    即时编译建立在程序符合二八定律的假设上,也就是百分之二十的代码占据了百分之八十的计算资源。

    对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;

    另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。

     

    为了满足不同用户场景的需要,HotSpot 内置了多个即时编译器:C1、C2 和 Graal。

    Graal 是 Java 10 正式引入的实验性即时编译器,这里暂不做讨论。

     

    之所以引入多个即时编译器,是为了在编译时间和生成代码的执行效率之间进行取舍。

     

    C1 又叫做 Client 编译器,面向的是对启动性能有要求的客户端 GUI 程序,采用的优化手段相对简单,因此编译时间较短。

    C2 又叫做 Server 编译器,面向的是对峰值性能有要求的服务器端程序,采用的优化手段相对复杂。

    因此编译时间较长,但同时生成代码的执行效率较高。

    从 Java 7 开始,HotSpot 默认采用分层编译的方式:热点方法首先会被 C1 编译,而后热点方法中的热点会进一步被 C2 编译。

    为了不干扰应用的正常运行,HotSpot 的即时编译是放在额外的编译线程中进行的。

    HotSpot 会根据 CPU 的数量设置编译线程的数目,并且按 1:2 的比例配置给 C1 及 C2 编译器。

    在计算资源充足的情况下,字节码的解释执行和即时编译可同时进行。

    编译完成后的机器码会在下次调用该方法时启用,以替换原本的解释执行。

     

    展开全文
  • Java号称是一门“一次编译到处运行”的语言,从我们写的java文件到通过编译器编译成java字节码文件(.class文件),这个过程是java编译过程;而我们的java虚拟机执行的就是字节码文件。不论该字节码文件来自何方,由...

    什么是Java字节码?

    它是程序的一种低级表示,可以运行于Java虚拟机上。将程序抽象成字节码可以保证Java程序在各种设备上的运行

    Java号称是一门“一次编译到处运行”的语言,从我们写的java文件到通过编译器编译成java字节码文件(.class文件),这个过程是java编译过程;而我们的java虚拟机执行的就是字节码文件。不论该字节码文件来自何方,由哪种编译器编译,甚至是手写字节码文件,只要符合java虚拟机的规范,那么它就能够执行该字节码文件。

    JAVA程序的运行

    因为Java具有跨平台特性,为了实现这个特性Java执行在一台虚拟机上,这台虚拟机也就是JVM,Java通过JVM屏蔽了不同平台之间的差异,从而做到一次编译到处执行。JVM位于Java编译器和OS平台之间,Java编译器只需面向JVM,生成JVM能理解的代码,这个代码即字节码,JVM再将字节码翻译成真实机器所能理解的二进制机器码。

    字节码是怎么产生的?

    我们所编写的程序都是.java格式,通常在执行的时候也许点击一下eclipse的运行键就可以在控制台看到运行结果,但是也可以更酷一些,如果你装了JDK,那就可以直接在以命令行的方式编译运行你的.java文件,编译后会形成.class文件,这个.class文件即字节码。

    字节码怎么解读?

    上图是编译好的字节码文件,即一堆16进制的字节,如果使用IDE去打开,也许看到的是已经被反编译的我们所熟悉的java代码,但这才是纯正的字节码

    这里只介绍字节码由哪些部分组成, 具体的意思自行百度或者看文尾的连接, 有较为详细的讲解

    上图即字节码文件的组成部分, Class文件的结构不像XML等描述语言那样松散自由。由于它没有任何分隔符号

    所以,以上数据项无论是顺序还是数量都是被严格限定的。哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改

    变,  如上图左侧即每一部分规定的长度

    • 魔数(Magic Number)
      魔数是用来区分文件类型的一种标志,一般都是用文件的前几个字节来表示。
      比如0XCAFE BABE表示的是class文件,那么有人会问,文件类型可以通过文件名后缀来判断啊?是的,但是文件名是可以修改的(包括后缀),那么为了保证文件的安全性,将文件类型写在文件内部来保证不被篡改。
      至于为什么是CAFE BABE估计大家也能猜到, 程序员与咖啡的不解之缘
    • 版本号(Version)
      版本号含主版本号和次版本号,都是各占2个字节。在此Demo种为0X0000 0033。其中前面的0000是次版本号,后面的0033是主版本号。通过进制转换得到的是次版本号为0,主版本号为51。高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式未发生任何变化.  这就是target参数的用处,可以在使用JDK 1.7编译时指定-target 1.5
    • 常量池(Constant Pool)
      常量池是Class文件中的资源仓库, 量池中主要存储2大类常量:字面量和符号引用。字面量如文本字符串,java中声明为final的常量值等等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符。常量池是一个表结构,在表的内容前有一个类型的计数器,表示常量池的长度
      上面的表中描述了11中数据类型的结构,其实在jdk1.7之后又增加了3种(CONSTANT_MethodHandle_info,CONSTANT_MethodType_info以及CONSTANT_InvokeDynamic_info)。这样算起来一共是14种
    • 访问标志(Access_Flag)
      访问标志信息包括该Class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被声明成final。通过上面的源代码,我们知道该文件是类并且是public。

      0x 00 21:是0×0020和0×0001的并集。其中0×0020这个标志值涉及到字节码指令
    • 类索引(This Class Name)
      类索引用于确定类的全限定名
      0×00 03 表示引用第3个常量,同时第3个常量引用第19个常量,查找得”com/demo/Demo”。#3.#19
    • 父类索引(Super Class Name)
      0×00 04 同理:#4.#20(java/lang/Object)
    • 接口索引(Interfaces)
      通过上边字节码图可以看到,这个接口有2+n个字节,前两个字节表示的是接口数量,后面跟着就是接口的表。我们这个类没有任何接口,所以应该是0000。果不其然,查找字节码文件得到的就是0000。
    • 字段表集合(fields)
      字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。接下来就是2+n个字段属性。我们只有一个属性a,所以应该是0001。查找文件果不其然是0001。
      该区域含有字段的访问标志, 访问权限, 字段的名称索引, 字段的描述符索引, 属性表 
      描述符
      的作用就是用来描述字段的数据类型、方法的参数列表和返回值。而属性表就是为字段表和方法表提供额外信息的表结构。对于字段来说,此处如果将字段声明为一个static final msg = "aaa"的常量,则字段后就会跟着一个属性表,其中存在一项名为ConstantValue,指向常量池中的一个常量,值为的"aaa"。
       
    • 方法(methods)
      包含访问标志表, 方法名索引 , 方法描述符索引, 属性表数量,等
    • Attribute
      0×0001 :同样的,表示有1个Attributes了。
      0x000f : #15(“SourceFile”)
      0×0000 0002 attribute_length=2
      0×0010 : sourcefile_index = #16(“Demo.java”)
      SourceFile属性用来记录生成该Class文件的源码文件名称。

    持续更新中…
    文章参考学习地址:https://blog.csdn.net/q5706503/article/details/84204747

    参考以下博主:

    https://www.cnblogs.com/beautiful-code/p/6425376.html

    http://www.importnew.com/24088.html

    https://www.cnblogs.com/mar-q/p/7295088.html

    展开全文
  • class文件结构、类加载机制、类加载器、运行时数据区这四个java技术体系中非常重要的知识,学习完了这些以后,我们知道一个类是通过类加载器加载到虚拟机,存储到运行时数据区,而且我们也知道了我们方法体内的代码...

    前言:

    class文件结构、类加载机制、类加载器、运行时数据区这四个java技术体系中非常重要的知识,学习完了这些以后,我们知道一个类是通过类加载器加载到虚拟机,存储到运行时数据区,而且我们也知道了我们方法体内的代码被编译成字节码保存在方法表中的code属性中,那么虚拟机又是怎么执行这些代码的,得出方法输出结果的呢?这一节我们就要来学习,关于虚拟机字节码执行引擎的相关知识。通过这章节的学习,我们要掌握一下知识点:

    1.运行时栈帧结构

    2.方法调用

    3.基于栈的字节码执行引擎

    运行时栈帧结构

    栈帧是用于支持方法调用和方法执行的数据结构。他是虚拟机运行时数据区中虚拟机栈中的栈元素。栈帧存储了:方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法调用从开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

    b34bb069c179483d7e4be4535596012f.png

    局部变量表

    局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。

    操作数栈

    操作数栈也常被称为操作栈,它是一个后入先出栈。跟局部变量表一样,操作数栈的最大深度也在编译的时候被写入Code属性的max_stacks数据项中。当方法开始执行的时候,这个方法的操作栈是空的,在方法执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈和出栈操作。比如说做算术运算的时候通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。

    动态连接

    每一个栈帧都包含一个指向运行时常量池的该栈帧所属的方法引用,持有这个引用是为了支持方法调用过程中的动态连接。通过Class文件结构我们知道,常量池中存在大量的符号引用,字节码中方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候转化为直接引用,这种转化叫做静态解析。而另外一部分将在每一次运行期间转化为直接引用,这部分叫做动态连接。

    方法返回地址

    当一个方法被执行后,有两种方式退出该方法,一种是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会返回值给上层调用者,这种方式叫做:正常完成出口。

    另外一种方式就是方法执行遇到异常,并且异常在方法体内没有被处理,这时候就会导致退出方法,这种方式叫做:异常完成出口。异常完成出口方式是不会给上层调用者任何返回值的。

    方法返回地址等同于当前栈帧出栈,恢复上层栈帧的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈,pc计数器加1,执行pc计数器的值指向的方法调用指令。

    方法调用

    方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(调用哪个方法),还没有涉及到方法体内的具体运行过程。那么我们都知道java有方法重载和方法重写,那么如何确定调用方法的版本呢?一切方法调用在Class文件里面存储的只是符号引用,而不是方法在实际运行时内存布局中的入口地址。这个特性给java带来了更强大的动态扩展能力,但也使得java方法调用过程变得相对复杂起来,需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。

    在类加载的解析阶段,会将一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个确定的调用版本,并且这个方法的调用版本在运行期间不可改变的,这种调用被称为解析调用。符合“编译期可知,运行期不可变"的方法主要有静态方法和私有方法两大类。这两种方法都不可能通过继承或者别的方式重写出其他版本,因此他们都适合在类加载阶段进行解析。

    解析调用一定是个静态过程,在编译期间就完全确定,在类装载的解析阶段就会涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。

    与之相对应,在JAVA虚拟机中提供了四条方法调用字节码指令,分别是:

    invokestatic:调用静态方法

    invokespecial:调用实例构造方法,私有方法和父类方法

    invokevirtual:调用所有的虚方法

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

    只要能被invokestatic \invokespecial指令调用的方法,都可以在解析阶段确定唯一调用版本,比如说静态方法、私有方法、实例构造器和父类方法四类,在类加载的时候就会把符号引用转化为直接引用,这类方法又称为非虚方法。final方法虽然是用invokevirtual调用的,但是它无法覆盖,没有其他版本,在java语言规范中明确说明了final方法是一种非虚方法。

    因为java具备面向对象的三大基本特征:继承、封装、多态。多态的基本体现就是重载和重写,那么重载和重写的方法在虚拟机如何确定正确的目标方法?

    分派调用

    分派调用可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派,两类组合就构成了静态单分派、静态多分派、动态单分派、动态多分派。我们来看一下下面这段代码。

    import com.sun.deploy.net.proxy.StaticProxyManager;

    import java.util.Map;

    /**

    * @Author:Administrator.

    * @CreatedTime: 2018/8/13.

    * @EditTime:2018/8/13.

    * @Version:

    * @Description:

    * @Copyright:

    */

    public class StaticDispatch {

    static abstract class Human {

    }

    static class Man extends Human {

    }

    static class Women 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(Women guy) {

    System.out.println("Hello,lady!");

    }

    public static void main (String[] args) {

    Human women = new Women();

    Human man = new Man();

    StaticDispatch sd = new StaticDispatch();

    sd.sayHello(women);

    sd.sayHello(man);

    }

    }

    执行结果是:

    Hello,guy!

    Hello,guy!

    有经验的开发者,一看就能看出结果来,那为什么虚拟机会调用参数为Human的sayHello方法呢?在说明这个之前,我们先来理解两个概念:

    Human man = new Man();

    Human是变量的静态类型或者叫外观类型,而Man是变量的实际类型。静态类型和实际类型在程序中都可以发生一些变化,区别在于静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变。并且最终的静态类型是在编译期可知的,而实际类型变化的结果在运行期才可以确定,编译器在编译程序的时候并不知道一个对象实际类型是什么。

    所以回到上面代码,main方法中两次调用sayHello方法,使用哪一个版本就完全取决于传入参数的数量和数据类型。代码中刻意定义了两个静态类型相同,实际类型不同的变量,但是虚拟机(具体的说应该是编译器)在重载时是通过参数的静态类型而不是实际类型作为判断依据的。并且静态类型是在编译期可知的,所以在编译阶段,javac编译器就根据参数的静态类型决定使用哪一个版本,所以选择了sayHello(Human)作为调用目标。

    静态分派

    所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派。静态分派最典型的例子就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是虚拟机来执行的。更多时候重载的版本不是唯一的,往往只能确定一个更合适的版本。看下面代码:

    /**

    * @Author:Administrator.

    * @CreatedTime: 2018/8/13.

    * @EditTime:2018/8/13.

    * @Version:

    * @Description:

    * @Copyright:

    */

    public class StaticDispatch {

    public static void sayHello(Object obj) {

    System.out.println("Hello,object!");

    }

    public static void sayHello(int c) {

    System.out.println("Hello,int!");

    }

    public static void sayHello(double c) {

    System.out.println("Hello,double!");

    }

    public static void sayHello(float c) {

    System.out.println("Hello,float!");

    }

    public static void sayHello(long c) {

    System.out.println("Hello,long!");

    }

    public static void sayHello(Character c) {

    System.out.println("Hello,Character!");

    }

    public static void sayHello(char c) {

    System.out.println("Hello,char!");

    }

    public static void sayHello(char... c) {

    System.out.println("Hello,char...!");

    }

    public static void sayHello(Serializable c) {

    System.out.println("Hello,Serializable!");

    }

    public static void main (String[] args) {

    sayHello('c');

    }

    }

    执行结果是:Hello,char!,然后我们把sayHello(char c)注释再执行,发现结果是:Hello,int!

    这里发生了自动转换的过程,‘c’->65;我们继续注释掉这个方法继续执行,输出结果为Hello,long!这里就发生了两次自动转换:c->65->65L.这一的方式自动转换可以持续多次:char->int->long->float->double.注释掉double参数重载方法后执行:Hello,Character!这里就存在一次自动装箱的过程。那么我们继续把这个方法注释掉,继续执行,发现:Hello,Serializable!这里跟序列化有什么关系呢?自动装箱后,发现还是找不到匹配的参数类型,却找到了装箱类实现的接口Serializable,那么就继续自动转型,注意这里封装类型Character是不能够转换成Integer的,它只能安全的转换为它实现的接口或者父类。Character还实现了一个java.lang.comparable接口,如果同时出现Serializable、comparable的方法重载时,它的优先级是一样的,这个时候编译器就会报错:类型模糊,编译不通过。这个时候必须指定对应的接口才能通过编译(如sayHello(comparable 'c'))。继续注释掉Serializable参数的重载方法,执行!这个时候就是Hello,object!自动装箱后转为父类类型,如果有多重继承,那么由下往上找,越往上优先级越高。继续注释,最后就执行char...变长参数的重载方法,由此可见变长参数的匹配优先级是最低的。这个例子就是java实现方法重载的本质,这个例子是个极端例子,通常工作中是几乎没有用的,一般都是放到面试题里“为难”一下面试者。

    动态分派

    我们了解了静态分派后,我们继续看下动态分派是如何实现的.动态分派是多态特性的另一个重要体现重写(override).看如下代码:

    /**

    * @Author:Administrator.

    * @CreatedTime: 2018/8/13.

    * @EditTime:2018/8/13.

    * @Version:

    * @Description:

    * @Copyright:

    */

    public class StaticDispatch {

    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 Women extends Human {

    @Override

    protected void sayHello() {

    System.out.println("Women Say Hello!");

    }

    }

    public static void main (String[] args) {

    Human man = new Man();

    Human women = new Women();

    man.sayHello();

    women.sayHello();

    man = new Women();

    man.sayHello();

    }

    }

    运行结果:

    man Say Hello!

    Women Say Hello!

    Women Say Hello!

    相信这个运行结果肯定都在你们预料之中的,因为习惯了面向对象编程的你们来说这是理所当然的了。但虚拟机怎么知道该调用哪一个方法的呢?显然这里是无法通过参数静态类型来确定!

    Human man = new Man();

    Human women = new Women();

    这两行在内存中分配了man和women的内存空间,调用man和women的实例构造器,把两个实例放到局部变量表的第一和第二个slot位置上。

    4909e9a715b91a27d504060f9527294c.png

    在运行期将符号引用转化为直接引用,所以man和women被解析到不同的直接引用上,这过程就是方法重写的本质。我们把运行期根据实际类型确定方法版本的分派过程叫做动态分派。

    基于栈的字节码解释执行引擎

    上面已经把java虚拟机是如何调用方法讲完了,那么接下来就是虚拟机是怎么执行这些字节码指令的.虚拟机在执行代码时都有解释执行和编译执行两种选择。

    解释执行

    java语言刚开始的时候被人们定义为解释执行的语言,在jdk1.0来说是比较准确的,但随着虚拟机的发展,虚拟机中开始包含了即时编译器后,class文件中的代码到底是解释执行还是编译执行恐怕只有虚拟机自己才能判断了。

    不过不管是解释还是编译,不管是物理机还是虚拟机,对于应用程序,机器肯定是无法像人一样阅读和理解,然后获得执行能力。大部分的程序代码到物理机或者虚拟机可执行的字节码指令集,都需要经历多个步骤,如下图,而中间那条就是解释执行的过程。

    120e76dc8d0af3c9429875222e92dcd1.png

    Java语言中,Javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历树生成线性的字节码指令流的过程。因为一部分在虚拟机外,而解释器在虚拟机的内部,所以java程序的编译就是半独立的实现。

    基于栈的指令集与基于寄存器的指令集

    Java编译器输出的指令流,基本上是一种基于栈的指令集架构,指令流里面大部分都是零地址指令看,他们依赖操作数栈进行工作。与之相对的另外一套常用指令集架构是基于寄存器的指令集。

    两者优缺点:

    1.基于栈的指令集主要优点就是可移植性,但因为相同的动作该指令集需要频繁操作内存,且多于寄存器指令集,速度就慢。

    2.基于寄存器指令集主要优点就是速度快,操作少。但是因为寄存器是依赖于硬件的,所以它的移植性受到影响。

    基于栈的解释器执行过程

    这一内容通过一个一个四则运算进行讲解,下面是代码:

    public int calc() {

    int a = 100;

    int b = 200;

    int c = 300;

    return (a+b) * c;

    }

    下面是字节码执行过程图(包含字节码指令、pc计数器、操作数栈、局部变量表):

    47f6da911ea5b69eca8c431450249bf8.png

    de8e56eec5ccfc9b2cfc242ff18c2370.png

    6d84eac63ba8d8f10a855441081d14bb.png

    a9c7b80dd96fad1f43a499bd783c6d8f.png

    ea2ab4b0c2af4914609372bbe221c436.png

    上面的演示,是一个概念模型,实际上肯定不会跟这个一样的,因为虚拟机中的解释器和即时编译器都会对输入的字节码进行优化。

    总结:

    学到这里,我们已经把Java程序是如何存储(Class文件结构),如何加载(类加载机制、类加载器),运行时数据区、以及如何执行的相关知识都学习完了。接下来我们应该进行学习的章节就是垃圾收集器和内存分配策略。怎么判断该对象所持有的内存可以回收了?回收主要在运行时数据区的那些区域?以及内存分配与回收策略。

    展开全文
  • JVM进阶 | Java字节码

    2020-12-01 11:31:41
    Java字节码是由(.Java)文件编译成(.class)的文件。之所以叫字节码是因为(.class)文件是由十六进制组成的。而JVM以两个十六进制值为一组,即以字节为单位进行读取。java之所以能够做到一次编译、到处运行,就是因为...
  • Javassit是一个功能包,作用类似于java的反射,用于操作运行字节码文件,实现动态编程,但性能高于反射。 2、怎么用? 首先需要获取存放class文件的容器ClassPool,根据全类名获取一个CtClass对象,根据需要修改...
  • 运行一个Java程序的步骤:1、编辑源代码xxx.java (推荐学习:java课程)2、编译xxx.java文件生成字节码文件xxx.class3、JVM中的类加载器加载字节码文件4、JVM中的执行引擎找到入口方法main(),执行其中的方法从源码到...
  • 如下图所示,我们可以知道一个源文件到执行文件的过程:java程序运行关系1、java源文件(.java文件)2、java编译器即javac.exe3、java字节码文件(.class文件)4、由解释执行器即(java.exe)将字节码文件加载到java虚拟器...
  • 我们都知道Java 是跨平台的,原因是 Java 的虚拟机只认识 字节码 (ByteCode),甚至只要你的语言在运行时能够装换成 字节码,同样可以在虚拟机上运行,比如 JRuby、Scala等语言;这也是当初Java设计者说出的霸气宣言 ...
  • Java代码执行步骤编译Java文件通过JVM...解释器是软件实现的,他将字节码转换成汇编指令,可以实现同一份Java字节码在不同的硬件上运行,而将汇编指令转换成机器指令由硬件直接实现,所以他的速度会更快。JVM为了提...
  • 字节码Class文件解读

    2019-02-28 23:57:36
    一、前言 刚开始学习Java的时候老师告诉我们Java是跨平台语言,一次编译到处运行,那么在Java编译...将原来的.java文件经过编译器编译转换成.class字节码文件。编译器的存在主要就是编译不同的源文件,将其转换成...
  • Java代码执行步骤编译Java文件通过JVM...解释器是软件实现的,他将字节码转换成汇编指令,可以实现同一份Java字节码在不同的硬件上运行,而将汇编指令转换成机器指令由硬件直接实现,所以他的速度会更快。JVM为了提...
  • class文件结构、类加载机制、类加载器、运行时数据区这四个java技术体系中非常重要的知识,学习完了这些以后,我们知道一个类是通过类加载器加载到虚拟机,存储到运行时数据区,而且我们也知道了我们方法体内的代码...
  • 会通过编译器将代码编译成.class后缀的字节码文件Java是平台无关的,实现语言无关性的基础就是虚拟机和字节码存储格式只要编译器按照虚拟机规范,编译成对应的class文件,这个class文件就能够被JVM加载,不同的操作...
  • 对于Java来说其编写好的文件为.java文件,经过编译产生.class文件,对于Java虚拟机来说,class文件中存放着Java代码运行的指令。新版本的JDK拥有很多语法上的新特性,在我们从老版本的JDK升级到新版本的JDK时,总是...
  • 从以上的图可以知道, 字节码文件就是由jvm处理的文件, 它符合jvm的规范. java不同于C++, C++里面的C运行时库会因为不同系统而不同,因为需要支持不同的操作系统API.java称为“一次编译,到处运行...
  • 从栈帧看字节码是如何在JVM中进行流转的我们都知道java文件需要编译成class文件,然后jvm负责加载并运行class文件,那么字节码文件长什么样子?字节码又是怎么执行的?工具介绍javapjavap是JDK自带的查看字节码的...
  • 1、javac编译器把java源代码翻译成字节码文件,称为前端编译器 2、JIT编译器将字节码转换成本地机器代码后运行,另一种是java解释器直接解释执行字节码。 3、字节码被JVM加载到虚拟机内存结构中(也叫运行时数据区...
  • Java"一次编写,到处运行"是怎么做到的呢? Java代码编译后的结果是从本地机器码转变为字节码Java虚拟机不与某种特定语言绑定,而是和".class"文件绑定,Class文件中包含了Java虚拟机指令集,符号表等。 因此,...
  • Java源文件(.java),通过java编译器(javac)编译生成一个ByteCode字节码文件(.class),字节码由java自己设计的一个计算机(即java虚拟机,JVM)解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其
  • java代码是怎么运行起来的 编写源代码程序,即*.java 打包,打成jar包或者war包,打包过程中,会有编译的过程,将*.java文件编译成 *.class文件 ...类加载器把编译好的 *.class字节码文件给加载到JVM中,供后续使用 ...
  • javac即启动编译器,将调用JDK中一整套工具将源文件转换为字节码文件 首先进行语法检查,都没问题了,就转换为字节码文件   第四不,使用java命令运行类文件(将编译后的二进制文件交给JVM去运行)  java即...
  • java的代码需要编译成class文件,class文件保存的是java的操作码(opcode),操作码又被称为java字节码,因为每条java的操作码都是一个字节。java的操作码可以被反汇编成人类能读懂的指令: # 最左列是偏移;中间
  • 首先一个问题入题:是否知道java和c++在运行方式上的区别?...Java虚拟机具体是怎样运行java字节码的? 从虚拟机的视角来看,执行java代码首先需要将它编译而成的class文件加载到java虚拟机中。加载后的j
  • 避开开发工具,你是否还记得怎么用cmd指令运行java程序吗?过程中容易在哪些地方出错,让我们来回顾一下~ ...5.javac对.java文件进行编译工作使其变成字节码的.class文件 6.对.class文件进行解释 成功运行
  • 我们都知道java文件需要编译成class文件,然后jvm负责加载并运行class文件,那么字节码文件长什么样子?字节码又是怎么执行的? 工具介绍 javap javap是JDK自带的查看字节码的工具。 javap的使用方法如下: $ javac ...
  • 使用一、编译这个过程是把人能看懂而机器看不懂的程序代码通过javac编译器编译后生成机器能看懂的字节码文件!用官方点的话说就是把一种语言规范转化为另一种语言规范!在这个转化过程中,编译器会做什么事情呢?...
  • 字节码是否依赖于它创建的Java版本?解决方法:If I compiled a java file in the newest JDK, would an older JVM be able to run the .class files?这取决于三件事:>您正在谈论的实际Java版本.例如,1.4.0 JVM...
  • 我们平时编码过程中,可能很少去查看 Java 文件编译后的字节码指令。但是,不管你是因为对技术非常热爱,喜欢刨根问底,还是想在别人面前装X 。我认为,都非常有必要了解一下常见的字节码指令。这对于我们理解代码的...
  • 无论什么语言写的代码,其到最后都是通过机器码运行的,无一例外。...前端编译器:源代码到字节码对于 Java 虚拟机来说,其实际输入的是字节码文件,而不是 Java 文件。那么对于 Java 语言而言,其实怎么Java...

空空如也

空空如也

1 2 3 4 5 ... 16
收藏数 304
精华内容 121
关键字:

java字节码文件怎么运行

java 订阅