1 概述
1.1 字节码文件的跨平台性
- Java语言:跨平台的语言
- 当Java源代码成功变异成字节码后,如果想在不同的平台上面运行,则无需再次编译
- 这个优势不再那么吸引人了。Python、PHP、Pper1、Ruby、Lisp等有强大的解释器。
- 跨平台似乎已经快成为一门语言必选的特性。
- Java虚拟机:跨语言的平台
Java虚拟机不和包括Java在内的任何语言绑定,它只与”Class文件”这种特定的二进制文件格式所关联。无论使用何种语言运行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。可以说,统一而强大的Class文件结构,就是Java虚拟机的基石、桥梁。
- 所有的JVM全部遵守Java虚拟机规范,也就是说所有的JVM环境都是一样的。
- 想要让一个Java程序正常地运行在JVM中,Java源码就必须要被便以为符合JVM规范的字节码文件,
- 前端编译器的主要任务就是负责将符合Java语言规范的Java代码转换为符合JVM规范的字节码文件。
- javac是一种能够将Java源码编译为字节码的前端编译器。
- Javac编译在将Jjava源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法解析、语法解析、语义解析以及生成字节码。
Oracle的JDK软件包括两部分内容:
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 透过字节码指令看代码细节
- BAT面试题
类文件结构有几部分?
知道字节码吗?字节码都有哪些?Integer x = 5; int y = 5; 比较x == y都经过哪些步骤?
- 代码举例
例子1
// 要知道==比较的是地址!!!
public class IntegerTest {
public static void main(String[] args) {
Integer x = 5;
int y = 5;
System.out.println(x == y); // true 这里比较true主要还是拆箱的原因
Integer i1 = 10;
Integer i2 = 10;
System.out.println(i1 == i2); // true 在范围内,都是引用数组中的
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); // false; // 这两个都是创建的新的对象
}
}
// 对应的字节码文件
0 iconst_5 // 将5放入操作数栈
1 invokestatic #2 <java/lang/Integer.valueOf> // 装箱,调用了Integer.valueOf方法,下面有源码
4 astore_1
5 iconst_5
6 istore_2
7 getstatic #3 <java/lang/System.out>
10 aload_1
11 invokevirtual #4 <java/lang/Integer.intValue>
14 iload_2
15 if_icmpne 22 (+7)
18 iconst_1
19 goto 23 (+4)
22 iconst_0
23 invokevirtual #5 <java/io/PrintStream.println>
26 bipush 10
28 invokestatic #2 <java/lang/Integer.valueOf>
31 astore_3
32 bipush 10
34 invokestatic #2 <java/lang/Integer.valueOf>
37 astore 4
39 getstatic #3 <java/lang/System.out>
42 aload_3
43 aload 4
45 if_acmpne 52 (+7)
48 iconst_1
49 goto 53 (+4)
52 iconst_0
53 invokevirtual #5 <java/io/PrintStream.println>
56 sipush 128
59 invokestatic #2 <java/lang/Integer.valueOf>
62 astore 5
64 sipush 128
67 invokestatic #2 <java/lang/Integer.valueOf>
70 astore 6
72 getstatic #3 <java/lang/System.out>
75 aload 5
77 aload 6
79 if_acmpne 86 (+7)
82 iconst_1
83 goto 87 (+4)
86 iconst_0
87 invokevirtual #5 <java/io/PrintStream.println>
90 return
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
// 这个是Integer的valueOf方法,大概知道如果这个数在-128到127之间,则会引用一个数组中的值的地址
// 其它的会创建一个新的对象
例子2
public class IntegerTest {
public static void main(String[] args) {
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
}
}
这个前面已经解释过了,对于str是指向堆中的new String(toString方法来的)。而str1是指向字符串常量池中的。
str2也是指向堆中的new String,但是和tr是不同的对象。
例子3
先来一个简单的
class Father {
int x = 10;
public Father() {
this.print();
x = 20;
}
public void print() {
System.out.println("Father.x = " + x);
}
}
public class SonTest {
public static void main(String[] args) {
Father f = new Son();
}
}
上面这个代码执行后,控制台打印什么??Father.x = 10
为什么?对于(非静态的)成员变量初始化:1.默认初始化、2.显式初始化 / 代码块中初始化(按顺序)、3.构造器中初始化、4.有了对象之后,可以”对象.属性”或对象.方法的方式对成员变量进行赋值。
下面看这个
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;
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);
}
}
运行结果
我们讲一下过程:首先new Son()时(要实例化Son),父类要先初始化,
则父类先给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)。虚拟机中许多指令并不包含操作数,只有一个操作码。
比如:操作码 (操作数)
0 iconst_5
1 invokestatic #2 <java/lang/Integer.valueOf>
4 astore_1
5 iconst_5
6 istore_2
7 getstatic #3 <java/lang/System.out>
10 aload_1
11 invokevirtual #4 <java/lang/Integer.intValue>
14 iload_2
15 if_icmpne 22 (+7)
18 iconst_1
19 goto 23 (+4)
22 iconst_0
23 invokevirtual #5 <java/io/PrintStream.println>
26 bipush 10
28 invokestatic #2 <java/lang/Integer.valueOf>
31 astore_3
32 bipush 10
34 invokestatic #2 <java/lang/Integer.valueOf>
37 astore 4
39 getstatic #3 <java/lang/System.out>
42 aload_3
43 aload 4
45 if_acmpne 52 (+7)
48 iconst_1
49 goto 53 (+4)
52 iconst_0
53 invokevirtual #5 <java/io/PrintStream.println>
56 sipush 128
59 invokestatic #2 <java/lang/Integer.valueOf>
62 astore 5
64 sipush 128
67 invokestatic #2 <java/lang/Integer.valueOf>
70 astore 6
72 getstatic #3 <java/lang/System.out>
75 aload 5
77 aload 6
79 if_acmpne 86 (+7)
82 iconst_1
83 goto 87 (+4)
86 iconst_0
87 invokevirtual #5 <java/io/PrintStream.println>
90 return
- 如何解读供虚拟机解释执行的二进制字节码?
方式一:一个一个二进制的看。这里用到的是Notepad++,需要安装一个HEX-Editor插件,或者使用Binary Viewer。
方式二:使用javap指令,jdk自带的反解析工具
方式三:使用IDEA插件:jclasslib或jclasslib bytecode viewer客户端工具。(可视化更好)
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文件的总体结构如下:
- 魔数
- Class文件版本
- 常量池
- 访问标志
- 类索引,父类索引,接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
这是一张Java字节码总的结构表,我们按照上面的顺序逐一进行解读就可以了。
下面就以这个简单代码讲解:
public class Demo {
private int num = 1;
public int add() {
num = num + 2;
return num;
}
}
经过编译后获得字节码文件的二进制流为:
后面是导入到Excel中。
3.1 魔数:Class文件的标志
3.2 Class文件版本号
我们上面字节码中主版本是34,其是16进制的,对应的十进制就是52,因此其对应的编译器是1.8,即JDK8编译器编译的。
3.3 常量池:存放所有常量
3.3.1 常量池计数器
3.3.2 常量池表
常量池表中主要是字面量和符号引用,是因为大部分是这两个,在idk7之后为了支持动态语言特性,新增了三个。