1 概述

1.1 字节码文件的跨平台性

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

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

  • 所有的JVM全部遵守Java虚拟机规范,也就是说所有的JVM环境都是一样的。
  1. 想要让一个Java程序正常地运行在JVM中,Java源码就必须要被便以为符合JVM规范的字节码文件,
    1. 前端编译器的主要任务就是负责将符合Java语言规范的Java代码转换为符合JVM规范的字节码文件。
    2. javac是一种能够将Java源码编译为字节码的前端编译器。
    3. Javac编译在将Jjava源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法解析、语法解析、语义解析以及生成字节码

image.png
Oracle的JDK软件包括两部分内容:

  • 一部分是将Java源代码编译成Java虚拟机的指令集的编译器
  • 另一部分是用于实现Java虚拟机的运行时环境。

    1.2 Java的前端编译器

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

Java袁大妈的编译结构是字节码,那么肯定需要有一种编译器能够将Java源码编译为字节码,承担这个重要责任的就是配置在path环境变量中的javac编译器。javac是一种能够将Java源码编译为字节码的前端编译器。

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

  • 在Eclipse中,当开发人员编写完代码后,使用”ctrl + S”快捷键时,ECJ编译器所采取的编译方法是把未编译部分的源码逐行进行编译,而非每次都全量编译。因此ECJ的编译效率会比javac更加迅速和高效,当然编译质量相比大致还是一样的。
  • ECJ不仅是Eclipse的默认内置编译器,在Tomcat中同样也是适用ECJ编译器来编译jsp文件。由于ECJ编译器是采用GPLv2的开源协议进行源代码公开,所以,大家可以登录eclipse官网下载ECJ编译器的源码进行二次开发。
  • 默认情况下,IntelliJ IDEA适用javac编译器。(还可以自己设置AspectJ编译器 ajc)。

其实这里我们就知道每次IDEA运行java代码,感觉上比Eclipse慢一点,因此IDEA默认是全编译(javac)。

前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给HotSpot的JIT编译器负责。

复习:AOT(静态提前编译器,Ahead of Time Compiler)
上面的JIT编译器,是在代码运行时编译的,这可以跨平台。
当然源码也可以提前编译成机器指令,但是呢?就失去跨平台的特性了。

1.3 透过字节码指令看代码细节

  1. BAT面试题

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

  1. 代码举例

例子1

  1. // 要知道==比较的是地址!!!
  2. public class IntegerTest {
  3. public static void main(String[] args) {
  4. Integer x = 5;
  5. int y = 5;
  6. System.out.println(x == y); // true 这里比较true主要还是拆箱的原因
  7. Integer i1 = 10;
  8. Integer i2 = 10;
  9. System.out.println(i1 == i2); // true 在范围内,都是引用数组中的
  10. Integer i3 = 128;
  11. Integer i4 = 128;
  12. System.out.println(i3 == i4); // false; // 这两个都是创建的新的对象
  13. }
  14. }

// 对应的字节码文件

  1. 0 iconst_5 // 将5放入操作数栈
  2. 1 invokestatic #2 <java/lang/Integer.valueOf> // 装箱,调用了Integer.valueOf方法,下面有源码
  3. 4 astore_1
  4. 5 iconst_5
  5. 6 istore_2
  6. 7 getstatic #3 <java/lang/System.out>
  7. 10 aload_1
  8. 11 invokevirtual #4 <java/lang/Integer.intValue>
  9. 14 iload_2
  10. 15 if_icmpne 22 (+7)
  11. 18 iconst_1
  12. 19 goto 23 (+4)
  13. 22 iconst_0
  14. 23 invokevirtual #5 <java/io/PrintStream.println>
  15. 26 bipush 10
  16. 28 invokestatic #2 <java/lang/Integer.valueOf>
  17. 31 astore_3
  18. 32 bipush 10
  19. 34 invokestatic #2 <java/lang/Integer.valueOf>
  20. 37 astore 4
  21. 39 getstatic #3 <java/lang/System.out>
  22. 42 aload_3
  23. 43 aload 4
  24. 45 if_acmpne 52 (+7)
  25. 48 iconst_1
  26. 49 goto 53 (+4)
  27. 52 iconst_0
  28. 53 invokevirtual #5 <java/io/PrintStream.println>
  29. 56 sipush 128
  30. 59 invokestatic #2 <java/lang/Integer.valueOf>
  31. 62 astore 5
  32. 64 sipush 128
  33. 67 invokestatic #2 <java/lang/Integer.valueOf>
  34. 70 astore 6
  35. 72 getstatic #3 <java/lang/System.out>
  36. 75 aload 5
  37. 77 aload 6
  38. 79 if_acmpne 86 (+7)
  39. 82 iconst_1
  40. 83 goto 87 (+4)
  41. 86 iconst_0
  42. 87 invokevirtual #5 <java/io/PrintStream.println>
  43. 90 return
  1. public static Integer valueOf(int i) {
  2. if (i >= IntegerCache.low && i <= IntegerCache.high)
  3. return IntegerCache.cache[i + (-IntegerCache.low)];
  4. return new Integer(i);
  5. }
  6. // 这个是Integer的valueOf方法,大概知道如果这个数在-128到127之间,则会引用一个数组中的值的地址
  7. // 其它的会创建一个新的对象

例子2

  1. public class IntegerTest {
  2. public static void main(String[] args) {
  3. String str = new String("hello") + new String("world");
  4. String str1 = "helloworld";
  5. System.out.println(str == str1); // false
  6. String str2 = new String("helloworld");
  7. System.out.println(str == str2); // false
  8. }
  9. }

这个前面已经解释过了,对于str是指向堆中的new String(toString方法来的)。而str1是指向字符串常量池中的。
str2也是指向堆中的new String,但是和tr是不同的对象。
例子3
先来一个简单的

  1. class Father {
  2. int x = 10;
  3. public Father() {
  4. this.print();
  5. x = 20;
  6. }
  7. public void print() {
  8. System.out.println("Father.x = " + x);
  9. }
  10. }
  11. public class SonTest {
  12. public static void main(String[] args) {
  13. Father f = new Son();
  14. }
  15. }

上面这个代码执行后,控制台打印什么??Father.x = 10
为什么?对于(非静态的)成员变量初始化:1.默认初始化、2.显式初始化 / 代码块中初始化(按顺序)、3.构造器中初始化、4.有了对象之后,可以”对象.属性”或对象.方法的方式对成员变量进行赋值。

下面看这个

  1. class Father {
  2. int x = 10;
  3. public Father() {
  4. this.print();
  5. x = 20;
  6. }
  7. public void print() {
  8. System.out.println("Father.x = " + x);
  9. }
  10. }
  11. class Son extends Father {
  12. int x = 30;
  13. public Son() {
  14. this.print();
  15. x = 40;
  16. }
  17. public void print() {
  18. System.out.println("Son.x = " + x);
  19. }
  20. }
  21. public class SonTest {
  22. public static void main(String[] args) {
  23. Father f = new Son();
  24. System.out.println(f.x);
  25. }
  26. }

image.png
运行结果
image.png
我们讲一下过程:首先new Son()时(要实例化Son),父类要先初始化,
image.png
则父类先给x赋值为0,再赋值为10,再在构造函数中赋值为20.但是注意这里在赋值20前调用了print方法,这个print方法应该是调用的子类的print方法,这个要注意,因此打印出来的是Sson.x = 0;
再然后x赋值为20,再然后子类实例化,自然打印出来是Son.x = 30.
最后输出是f.x = 10,因为属性没有多态。

2 虚拟机的基石:Class文件

  • 字节码文件里是什么?

源代码经过编译器编译之后便会生成一个字节码文件,字节码是一种二进制的类文件,它的内容是JVM的指令,而不像C、C++经由编译器直接生成机器码

  • 什么是字节码指令(byte code)?

Java虚拟机的指令是由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)。虚拟机中许多指令并不包含操作数,只有一个操作码。
比如:操作码 (操作数)

  1. 0 iconst_5
  2. 1 invokestatic #2 <java/lang/Integer.valueOf>
  3. 4 astore_1
  4. 5 iconst_5
  5. 6 istore_2
  6. 7 getstatic #3 <java/lang/System.out>
  7. 10 aload_1
  8. 11 invokevirtual #4 <java/lang/Integer.intValue>
  9. 14 iload_2
  10. 15 if_icmpne 22 (+7)
  11. 18 iconst_1
  12. 19 goto 23 (+4)
  13. 22 iconst_0
  14. 23 invokevirtual #5 <java/io/PrintStream.println>
  15. 26 bipush 10
  16. 28 invokestatic #2 <java/lang/Integer.valueOf>
  17. 31 astore_3
  18. 32 bipush 10
  19. 34 invokestatic #2 <java/lang/Integer.valueOf>
  20. 37 astore 4
  21. 39 getstatic #3 <java/lang/System.out>
  22. 42 aload_3
  23. 43 aload 4
  24. 45 if_acmpne 52 (+7)
  25. 48 iconst_1
  26. 49 goto 53 (+4)
  27. 52 iconst_0
  28. 53 invokevirtual #5 <java/io/PrintStream.println>
  29. 56 sipush 128
  30. 59 invokestatic #2 <java/lang/Integer.valueOf>
  31. 62 astore 5
  32. 64 sipush 128
  33. 67 invokestatic #2 <java/lang/Integer.valueOf>
  34. 70 astore 6
  35. 72 getstatic #3 <java/lang/System.out>
  36. 75 aload 5
  37. 77 aload 6
  38. 79 if_acmpne 86 (+7)
  39. 82 iconst_1
  40. 83 goto 87 (+4)
  41. 86 iconst_0
  42. 87 invokevirtual #5 <java/io/PrintStream.println>
  43. 90 return
  • 如何解读供虚拟机解释执行的二进制字节码?

方式一:一个一个二进制的看。这里用到的是Notepad++,需要安装一个HEX-Editor插件,或者使用Binary Viewer。
image.png
方式二:使用javap指令,jdk自带的反解析工具
方式三:使用IDEA插件:jclasslib或jclasslib bytecode viewer客户端工具。(可视化更好)
image.png

3 Class文件结构

  • Class类的本质

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上并不一定以磁盘文件的形式存在。Class文件是一组以8位字节为基础单位的二进制流

  • Class文件格式

Class的结构不像XML等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

Class文件格式采用一种类似于CC语言结构体的方式进行数据村粗,这种结构只有两种数据类型:无符号数

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所以表都习惯性地以”_info”结尾。表用于描述有层次关系的符合结构的数据,整个Class文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明。

Class文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的。
Class文件的总体结构如下

  1. 魔数
  2. Class文件版本
  3. 常量池
  4. 访问标志
  5. 类索引,父类索引,接口索引集合
  6. 字段表集合
  7. 方法表集合
  8. 属性表集合

image.png
image.png
这是一张Java字节码总的结构表,我们按照上面的顺序逐一进行解读就可以了。

下面就以这个简单代码讲解:

  1. public class Demo {
  2. private int num = 1;
  3. public int add() {
  4. num = num + 2;
  5. return num;
  6. }
  7. }

经过编译后获得字节码文件的二进制流为:
image.png
后面是导入到Excel中。
image.png

3.1 魔数:Class文件的标志

image.png

3.2 Class文件版本号

image.png
image.png
我们上面字节码中主版本是34,其是16进制的,对应的十进制就是52,因此其对应的编译器是1.8,即JDK8编译器编译的。

3.3 常量池:存放所有常量

image.png
image.png

3.3.1 常量池计数器

image.png

3.3.2 常量池表

image.png
image.png
常量池表中主要是字面量和符号引用,是因为大部分是这两个,在idk7之后为了支持动态语言特性,新增了三个。

3.3.2.1 字面量和符号引用

image.png
image.png
image.png
image.png

3.3.2.2 常量类型和结构

image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png

3.4 访问标志

3.5 类索引、父类索引、接口索引集合

3.6 字段表集合

3.6.1 字段计数器

3.6.2 字段表

3.7 方法表集合

3.7.1 方法计数器

3.7.2 方法表

3.8 属性表集合

3.8.1 属性计数器

3.8.2 属性表

3.9 小结

4 使用javap指令解析Class文件

4.1 javap的用法

4.2 使用举例

4.3 总结