字节码文件概述
字节码文件是跨平台的吗?
Java 虚拟机不和包括 Java 在内的任何语言绑定,它只与“Class 文件”这种特定的二进制文件格式所关联。
无论使用何种语言进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。可以说,统一而强大的Class文件结构,就是Java虚拟机的基石、桥梁。
想要让一个Java程序正确地运行在JVM中,Java源码就必须要被编译为符合JVM规范的字节码。
· 所有的JVM全部遵守Java虚拟机规范,也就是说所有的JVM环境都是一样的,这样一来字节码文件可以在各种JVM上运行。
从Java虚拟机的角度看,通过Class文件,可以让更多的计算机语言支持Java虚拟机平台。因此,Class文件结构不仅仅是Java虚拟机的执行入口,更是Java生态圈的基础和核心。
class文件里是什么?字节码文件里是什么?
源代码经过编译器编译之后便会生成一个字节码文件,字节码是一种二进制的类文件,它的内容是JVM的指令,而不像C、C++经由编译器直接生成机器码。
随着Java平台的不断发展,在将来,Class文件的内容也一定会做进一步的扩充,但是其基本的格式和结构不会做重大调整。
能介绍下生成class文件的编译器吗?
1、从位置上理解
前端编译器 vs 后端编译器
2. 前端编译器的种类
? Java源代码的编译结果是字节码,那么肯定需要有一种编译器能够将Java源码编译为字节码,承担这个重要责任的就是配置在path环境变量中的javac编译器。javac是一种能够将Java源码编译为字节码的前端编译器。
? HotSpot VM并没有强制要求前端编译器只能使用javac来编译字节码,其实只要编译结果符合JVM规范都可以,被JVM所识别即可。
? 在Java的前端编译器领域,除了javac之外,还有一种被大家经常用到的前端编译器,那就是内置在Eclipse中的ECJ (Eclipse Compiler for Java)编译器。和Javac的全量式编译不同,ECJ是一种增量式编译器。
· 默认情况下,IntelliJ IDEA 使用 javac 编译器。(还可以自己设置为AspectJ编译器 ajc)
3. 前端编译器的任务
· 前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件。
目前前端编译器的局限性
? 前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给HotSpot的JIT编译器负责。
AOT(静态提前编译器,Ahead Of Time Compiler)
· jdk9引入了AOT编译器(静态提前编译器,Ahead Of Time Compiler)
· Java 9 引入了实验性 AOT 编译工具jaotc。它借助了 Graal (图形算法语言)编译器,将所输入的 Java 类文件转换为机器码,并存放至生成的动态共享库之中。
· 所谓 AOT 编译,是与即时编译相对立的一个概念。我们知道,即时编译指的是在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。而 AOT 编译指的则是,在程序运行之前,便将字节码转换为机器码的过程。
· .java -> .class -> .so
· 最大好处:Java虚拟机加载已经预编译成二进制库,可以直接执行。不必等待即时编译器的预热,减少Java应用给人带来“第一次运行慢”的不良体验。
· 缺点:
· 破坏了java“一次编译,到处运行”,必须为每个不同硬件、OS编译对应的发行包。
· 降低了Java链接过程的动态性,加载的代码在编译期就必须全部已知。
还需要继续优化中,最初只支持Linux x64 java base
哪些类型对应有Class的对象
类别:
(1)class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
(2)interface:接口
(3)[]:数组
(4)enum:枚举
(5)annotation:注解@interface
(6)primitive type:基本数据类型
(7)void
/**
* 拥有Class对象的类型
* @author wuwei
*/
@Test
public void test1() {
//类
Class c1 = Object.class;
Class c2 = Test01.class;
//接口
Class c3 = Map.class;
//数组
Class c4 = String[].class;
//枚举
Class c5 = Month.class;
//注解
Class c6 = Test.class;
//基本数据类型
Class c7 = int.class;
Class c8 = double.class;
//void
Class c9 = void.class;
//测试不同长度数组的Class对象是否一致
int[] a = new int[10];
int[] b = new int[20];
Class c10 = a.getClass();
Class c11 = b.getClass();
System.out.println(c10);
System.out.println(c11);
//只要元素的类型与维度一样,就是同一个Class
System.out.println(c10 == c11); //true
}
字节码指令
什么是字节码指令?
字节码指令(byte code)
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
代码如下:
字节码指令如下
为什么要懂字节码指令?
@Test
public void test() {
int i = 10;
i++;
System.out.println(i);
}
@Test
public void test2() {
int i = 10;
i = i++;
System.out.println(i);
}
@Test
public void test3() {
int i = 2;
i *= i++;
System.out.println(i);
}
@Test
public void test4() {
Integer i1 = 10;
Integer i2 = 10;
System.out.println(i1 == i2);
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);
Boolean i5 = false;
Boolean i6 = false;
System.out.println(i5 == i6);
}
@Test
public void test5(){
String str = new String("hello") + new String("world");
str.intern();
String str1 = "helloworld";
System.out.println(str==str1);
String a3 = new String("AA");
a3.intern();
String a4 = "AA";
System.out.println(a3 == a4);
}
上述代码:字节码指令解析
test2()
0 bipush 10 -- 将常量int10放入操作数栈中
2 istore_1 -- 将int从栈中取出存储到局部变量表中索引为1的位置
3 iload_1 -- 从局部变量表中取出索引为1的int值放入操作数栈中
4 iinc 1 by 1 -- 局部变量表中索引为1的int值自增1
7 istore_1 -- 将操作数栈中的数据存入到局部变量表索引为1的位置
8 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
11 iload_1 -- 加载局部变量表中索引为1的数据到操作数栈
12 invokevirtual #3 <java/io/PrintStream.println : (I)V> -- 打印
15 return
test3()
0 iconst_2 -- 将常量int2压入到操作数栈中
1 istore_1 -- 将操作数栈中的int取出存到局部变量表中索引为1的位置
2 iload_1 -- 加载局部变量表索引为1的int值压入到栈中
3 iload_1 -- 同上(同一个数值取出了两次压到了栈中)
4 iinc 1 by 1 -- 局部变量表中索引为1的int值自增1
7 imul -- 从栈中取出两个int值相乘再将结果压入到栈中
8 istore_1 -- 将结果存入到局部变量表索引为1的位置
9 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
12 iload_1 -- 将局部变量表索引为1的int值加载压如到栈中
13 invokevirtual #3 <java/io/PrintStream.println : (I)V> -- 打印
16 return
test4()
0 bipush 10 -- 压栈
2 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
-- 调用Integer.valueOf
-- 若值在-128到127之间
-- 就将值从缓存中取出
-- 若没有就new一个新的Integer
5 astore_1 -- 将引用存储到局部变量表中索引为1的位置
6 bipush 10 -- 将int10压栈
8 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;> -- 同上
11 astore_2 -- 将引用存储到局部变量表中索引为2的位置
12 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
15 aload_1 -- 将索引为1的引用入栈
16 aload_2 -- 将索引为2的引用入栈
17 if_acmpne 24 (+7) -- 比较栈顶两个引用,若不相等跳转到24行
20 iconst_1 -- 将1压到栈中
21 goto 25 (+4) -- 跳转到地址为5的指令中
24 iconst_0
25 invokevirtual #5 <java/io/PrintStream.println : (Z)V> -- 打印布尔结果
28 sipush 128 -- 将128值压栈
31 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
-- 超出了缓存范围,new一个新的integer
34 astore_3 -- 将引用放入3中
35 sipush 128 -- 压栈
38 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
-- 超出范围 new新的
41 astore 4 -- 将引用放入到局部变量表索引为4的位置
43 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
46 aload_3 -- 将局部变量表索引3的引用取出后压栈
47 aload 4 -- 将局部变量表索引4的引用取出后压栈
49 if_acmpne 56 (+7) -- 比较栈顶的两个引用,不一致则跳转到位置为56的指令
52 iconst_1
53 goto 57 (+4)
56 iconst_0 -- 将0放入到栈中
57 invokevirtual #5 <java/io/PrintStream.println : (Z)V> 打印结果
60 iconst_0 -- 将FALSE放入栈中
61 invokestatic #6 <java/lang/Boolean.valueOf : (Z)Ljava/lang/Boolean;>
64 astore 5 -- 将FALSE的引用存储到局部变量表索引为5的位置
66 iconst_0 -- 同上
67 invokestatic #6 <java/lang/Boolean.valueOf : (Z)Ljava/lang/Boolean;>
70 astore 6 -- 存到局部变量表6的位置
72 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
75 aload 5 -- 取出压栈
77 aload 6 -- 取出压栈
79 if_acmpne 86 (+7) -- 比较栈顶的引用
82 iconst_1 -- 相同
83 goto 87 (+4) -- 跳转到87位置的指令
86 iconst_0
87 invokevirtual #5 <java/io/PrintStream.println : (Z)V> -- 打印Boolean值
90 return
test5() 不看字节码了
1.只在常量池上创建常量
String a1 = "AA";
2.只在堆上创建对象
String a2 = new String("A") + new String("A");
3.在堆上创建对象,在常量池上创建常量
String a3 = new String("AA");
4.在堆上创建对象,在常量池上创建引用
String a4 = new String("A") + new String("A");//只在堆上创建对象AA
a4.intern();//将该对象AA的引用保存到常量池上
5.在堆上创建对象,在常量池上创建常量,在常量池上创建引用(不可能)
String a5 = new String("A") + new String("A");//只在堆上创建对象
a5.intern();//在常量池上创建引用
String a6 = "AA";//此时不会再在常量池上创建常量AA,而是将a5的引用返回给a6
System.out.println(a5 == a6); //true
举个例子:父子依赖字节码解析
class F1 {
int x = 10;
public F1() {
this.print();
x = 20;
}
public void print() {
System.out.println("F1.x = " + x);
}
}
class Z1 extends F1 {
int x = 30;
public Z1() {
this.print();
x = 40;
}
public void print() {
System.out.println("Z1.x = " + x);
}
}
public class BateCode02 {
public static void main(String[] args) {
F1 f = new Z1();
System.out.println(f.x);
}
}
打印结果如下:
Z1.x = 0
Z1.x = 30
20
|