字节码文件的跨平台性

  • Java 语言:跨平台的语言(write once, run anywhere)

    • 当 Java 源代码成功编译成字节码后,如果想在不同的平台上面运行, 则无须再次编译
    • 但是 这个优势不再那么吸引人了。Python、PHP、Perl、Ruby、Lisp 等有强大的解释器
    • 跨平台似乎已经快称为一门语言必选的特性
  • Java 虚拟机:跨语言的平台

    • Java 虚拟机不和包括 Java 在内的任何语言绑定,它只与”Class 文件”这种特定的二进制文件格式所关联。无论使用何种语言进行软件开发, 只要能将源文件编译为正确的 Class 文件,那么这种语言就可以在 Java 虚拟机上执行,可以说,统一而强大的 Class 文件结构,就是 Java 虚拟机的基石、桥梁。
  • image.png

  • image.png

  • https://docs.oracle.com/javase/specs/index.html
  • 中篇以java8为主体进行讲解https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

  • 3.想要让一个Java程序正确地运行在JVM中,Java源码就必须要编译为符合JVM规范的字节码。

    • 前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件。
    • javac是一种能够将Java源码编译为字节码的前端编译器
    • javac编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤:分别是词法解析,语法解析,语义解析,生成字节码

image.png

  • Oracle的JDK软件包括两部分内容:
    • 一部分是将Java源代码编译成Java虚拟机的指令集的编译器(前端编译器不包含在jvm中)
    • 另一部分是用于实现Java虚拟机的运行时环境

java的前端编译器

image.png
前端编译器 vs 后端编译器

  • Java源代码的编译结果是字节码,需要有一种编译器能够将Java源码编译为字节码,承担这个重要责任的就是配置在path环境变量中的javac编译器,它是一种能够将Java源码编译为字节码的前端编译器。


  • HotSpot VM并没有强制要求前端编译器只能使用javac来编译字节码,其实只要编译结果符合JVM规范都可以被JVM所识别即可。在Java的前端编译器领域,除了javac之外,还有一种被大家经常用到的前端编译器,那就是内置在Eclipse中的ECJ(Eclipse Compiler for Java)编译器。和javac的全量式编译不同,ECJ是一种增量式编译器。

    • 在Eclipse中,当开发人员编写完代码后,使用”Ctrl+S”快捷键时,ECJ编译器所采取的编译方案是把未编译部分的源代码逐行进行编译,而非每次都全量编译。因此ECJ的编译效率会比javac更加迅速和高效,而编译质量和javac相比大致还是一样的。
    • ECJ不仅是Eclipse的默认内置前端编译器,在Tomcat中同样也是使用ECJ编译器来编译jsp文件。
      由于ECJ编译器是采用GPLv2的开源协议进行源代码公开,所以大家可以登录Eclipse官网下载ECJ编译器的源码进行二次开发。
    • 默认情况下,IntelliJ IDEA使用javac编译器。(可自行设置为AspectJ编译器 ajc)
  • 前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给HotSpot的JIT编译器负责。

  • AOT Compiler(Ahead Of Time Compiler),静态提前编译器,程序运行之前将字节码文件直接翻译成机器指令


通过字节码指令看代码细节

BAT 面试题

  • 类文件结构有几个部分?
  • 知道字节码吗?字节码都有哪些?Integer x = 5; int y = 5;比较 x == y 都经过哪些步骤?

代码举例1

  1. public class IntegerTest {
  2. public static void main(String[] args) {
  3. /*
  4. * 通过字节码和源码可以看出,当Integer的范围在 -128 ~ 127 之间会在数组中直接拿取数值
  5. * 超过这个范围会重新 new 对象
  6. * */
  7. Integer x = 5;
  8. int y = 5;
  9. System.out.println(x == y); // true,拆箱,比较数值
  10. Integer i1 = 10;
  11. Integer i2 = 10;
  12. System.out.println(i1 == i2); // true
  13. Integer i3 = 128;
  14. Integer i4 = 128;
  15. System.out.println(i3 == i4); // false
  16. }
  17. }

代码举例2:

package com.atguigu.java;
public class StringTest {
    public static void main(String[] args) {
//通过字节码发现new了StringBuilder,append后再toString返回str
        //toString方法重新new了一个String对象
        String str = new String("hello") + new String("world");
        String str1 = "helloworld";
        System.out.println(str == str1);        //false
        String str2 = new String("helloworld");
        System.out.println(str == str2);       //false
    }
}

代码举例3:

/*
成员变量(非静态的)的赋值过程: ① 默认初始化 - ② 显式初始化 /代码块中初始化 - ③ 构造器中初始化 - ④ 有了对象之后,可以“对象.属性”或"对象.方法"
 的方式对成员变量进行赋值。
 */
class Father {
    int x = 10;

    public Father() {
        this.print();
        x = 20;
    }
    public void print() {
        System.out.println("Father.x = " + x);
    }
}

class Son extends Father {
    int x = 30;
//    float x = 30.1F;
    public Son() {
        this.print();
        x = 40;
    }
    public void print() {
        System.out.println("Son.x = " + x);
    }
}

public class SonTest {
    public static void main(String[] args) {
        Father f = new Son();
        System.out.println(f.x);
    }
}

先看第一种情况比较简单:Father f = new Father();
运行结果:
image.png
image.png

再看情况2:Father f = new Son();
运行结果:
image.png
由于创建的是子类对象,print方法被重写,所以父类构造器的this.print调用的是子类的print
但是属性没有多态,所以f.x=20
image.png