文章目录
- 1.概述
- 1.1 字节码文件的跨平台性
- 1.2 Java的前端编译器
- 1.3 透过字节码指令看代码细节
- 2. 虚拟机的基石:Class文件
- 3. Class文件结构
- 3.1 魔数
- 3.2 Class文件版本号
- 3.3 常量池
- 3.4 访问标识
- 3.5 类索引、父类索引、接口索引集合
- 3.6 字段表集合
- 3.7 方法表集合
- 3.8 属性表集合
- 4. 使用javap指令解析Class文件
1.概述
1.1 字节码文件的跨平台性
所有的JVM全部遵守Java虚拟机规范,也就是说所有的JV环境都是一样的,这样一来字节码文件可以在各种JVM上运行。
想要让一个Java程序正确地运行在JVM中,Java源码就必须要被编译为符合JVM规范的字节码。
前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件。javac是一种能够将Java源码编译为字节码的前端编译器。
Javac编译器在将Java源码编译为一个有放的字节码文件过程中经历了4个步骤,分别是词法解析、语法解析、语义解析以及生成字节码。
1.2 Java的前端编译器
前端编译器vs后端编译器
Java源代码的编译结果是字节码,那么肯定需要有一种编译器能够将Java源码编译为字节码,承担这个重要责任的就是配置在path环境变量中的javac编译器。javac是一种能够将Java源码编译为字节码的前端编译器。
HotSpot VM并没有强制要求前端编译器只能使用javac来编译字节码,其实只要编译结果符合JVM规范都可以被JVM所识别即可。在Java的前端编译器领域,除了javac之外,还有一种被大家经常用到的前端编译器,那就是内置在Eclipse中的ECJ(EclipseCompiler for Java)编译器。和Javac的全量式编译不同,EC是一种增量式编译器。
在Eclipse中,当开发人员编写完代码后,使用“Ctrl+S”快捷键时,ECJ编译器所采取的编译方案是把未编译部分的源码逐行进行编译,而非每次都全量编译。因此ECJ的编译效率会比javac更加迅速和高效,当然编译质量和javac相比大致还是一样的。ECJ不仅是Eclipse的默认内置前端编译器,在Tomcat中同样也是使用ECJ编译器来编译jsp文件。由于ECJ编译器是采用GPLv2的开源协议进行源代码公开,所以,大家可以登录eclipse官网下载ECJ编译器的源码进行二次开发。
默认情况下,IntelliJ IDEA 使用javac编译器。(还可以自己设置为AspectJ编译器ajc)
前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给HotSpot的JIT编译器负责。
复习:AOT(静态提前编译器,Ahead Of Time Compiler)
1.3 透过字节码指令看代码细节
大厂面试题
类文件结构有几个部分?│
知道字节码吗?字节码都有哪些? Integer x = 5;int y = 5;比较x == y都经过哪些步骤?
public class IntegerTest {
public static void main(String[] args) {
Integer x = 5;
int y = 5;
System.out.println(x == y);//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
}
}
System.out.println(x == y);//true 返回true是因为x自动拆箱成int类型,两个都是基本数据类型直接比较值是否相等。
System.out.println(i1 == i2);//true 返回true是因为Integer类型对-128到127之间的数字做了缓存。
System.out.println(i3 == i4);//false 超出了缓存
在Integer类加载到内存中时,会执行其内部类IntegerCache中静态代码块进行初始化,把 [-128,127]之间的数包装成Integer类并把其对应的引用存入到cache数组中,这样在方法区中开辟空间存放这些静态Integer变量,同时静态cache数组也存放在这里,供线程享用,这也称静态缓存。当用Integer声明初始化变量时,会先判断所赋值的大小是否在-128到127之间,若在,则利用静态缓存中的空间并且返回对应cache数组中对应引用,存放到运行栈中,而不再重新开辟内存。
public class StringTest {
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
}
}
当拼接两个变量时,+其实底层是StringBuilder的append方法,最终调用了StringBuilder.toString方法,new了一个新字符串来返回
而str1 最保存到了字符串常量池,new String(“helloworld”)也是构建了一个新的字符串,所以两个都是false
/*
成员变量(非静态的)的赋值过程: ① 默认初始化 - ② 显式初始化 /代码块中初始化 - ③ 构造器中初始化 - ④ 有了对象之后,可以“对象.属性”或"对象.方法"
的方式对成员变量进行赋值。
*/
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);
}
}
执行结果其实并不尽如人意,是吧???
Son.x = 0
Son.x = 30
20
new Son()的时候会先去调用父类的构造方法,而父类的构造方法中调用了this.print(),此处的this指的是当前的调用对象,谁调谁就是this,所以this.print()调用的子类的print方法,因为此时子类属性还未进行显式初始化,只是进行了默认初始化,所以是0。然后执行x = 20;将父类的属性x的值由10改为了20。然后执行子类的构造方法,调用this.print();此时子类属性已经完成了显式初始化,所以x是30。最后打印f.x的值,因为属性并不具有多态性,方法才具有多态性,所以是20。
2. 虚拟机的基石:Class文件
字节码文件里是什么?
源代码经过编译器编译之后便会生成一个或多个字节码文件,字节码是一种二进制的类文件,
它的内容是JVM的指令,而不像C、C++编译直接生成机器码。
什么是字节码指令(byte code)?
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数
的操作数( operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
字节码指令:操作码(操作数)
比如astore_3只有操作码,astore 4 既有操作码也有操作数。因为astore_0到astore_3比较常用,所以提供了现成的操作码。
3. Class文件结构
官方文档位置:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
. class类的本质
任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在。Class 文件是一组以8位字节为基础单位的二进制流。
. class文件格式
class的结构不像XML等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
Class文件格式采用一种类似于C语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次
关系的复合结构的数据,整个class 文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明
换句话说,充分理解了每一个字节码文件的细节,自己也可以反编译出Java源文件来。
class文件结构概述
class文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的。
3.1 魔数
Class文件的标志Magic Number(魔数)
每个Class 文件开头的4个字节的无符号整数称为魔数(Magic Number)
它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即:魔数是Class文件的标识符。
魔数值固定为OxCAFEBABE。不会改变。
如果一个Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出以下错误:
Error: A JNI error has occurred,please check your installation and try again
Exception in thread “main” java.lang.ClassFormatError: Incompatible magic value 1885430635 in classfile StringTest
使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。
3.2 Class文件版本号
紧接着魔数的4个字节存储的是Class 文件的版本号。同样也是4个字节。第5个和第6个字节所代表的含义就是编译的副版本号minor_version,而第7个和第8个字节就是编译的主版本号major_version。
它们共同构成了class文件的格式版本号。譬如某个Class 文件的主版本号为M,副版本号为 m,那么这个Class 文件的格式版本号就确定为M.m。
版本号和Java编译器的对应关系如下表:
Java 的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1。
不同版本的Java编译器编译的Class文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。
否则JVM会抛出java.lang.UnsupportedClassVersionError异常。(向下兼容)
在实际应用中,由于开发环境和生产环境的不同,可能会导致该问题的发生。因此需要我们在开发时,特别注意开发编译的
DK版本和生产环境中的JDK版本是否一致。
虚拟机J版本为1.k (k >= 2)时,对应的class文件格式版本号的范围为45.0 - 44+k.0(含两端)。
高版本的虚拟机可以执行低版本的字节码文件,反之不可以。
3.3 常量池
常量池:存放所有常量
常量池是Class文件中内容最为丰富的区域。常量池对于Class文件中的字段和方法解析也有着至关重要的作用。随着Java虚拟机的不断发展,常量池的内容也日渐丰富。可以说,常量池是整个Class文件的基石。
在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。
常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(
constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。
常量池表
constant_pool (常量池)
constant_pool是一种表结构,以1~ constant_pool_count - 1为索引。表明了后面有多少个常量项。·常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)
它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同
的特征。第1个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte (标记字节、标签字节)。
3.4 访问标识
3.5 类索引、父类索引、接口索引集合
3.6 字段表集合
3.7 方法表集合
3.8 属性表集合
4. 使用javap指令解析Class文件
javap -v -p test.class 可以看到最全的信息,-v是看不到私有字段和私有方法的。
public class JavapTest {
private int num;
boolean flag;
protected char gender;
public String info;
public static final int COUNTS = 1;
static{
String url = "www.atguigu.com";
}
{
info = "java";
}
public JavapTest(){
}
private JavapTest(boolean flag){
this.flag = flag;
}
private void methodPrivate(){
}
int getNum(int i){
return num + i;
}
protected char showGender(){
return gender;
}
public void showInfo(){
int i = 10;
System.out.println(info + i);
}
}
Classfile /C:/Users/songhk/Desktop/2/JavapTest.class //字节码文件所属的路径
Last modified 2020-9-7; size 1358 bytes //最后修改时间,字节码文件的大小
MD5 checksum 526b4a845e4d98180438e4c5781b7e88 //MD5散列值
Compiled from "JavapTest.java" //源文件的名称
public class com.atguigu.java1.JavapTest
minor version: 0 //副版本
major version: 52 //主版本
flags: ACC_PUBLIC, ACC_SUPER //访问标识
Constant pool: //常量池
#1 = Methodref #16.#46 // java/lang/Object."<init>":()V
#2 = String #47 // java
#3 = Fieldref #15.#48 // com/atguigu/java1/JavapTest.info:Ljava/lang/String;
#4 = Fieldref #15.#49 // com/atguigu/java1/JavapTest.flag:Z
#5 = Fieldref #15.#50 // com/atguigu/java1/JavapTest.num:I
#6 = Fieldref #15.#51 // com/atguigu/java1/JavapTest.gender:C
#7 = Fieldref #52.#53 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #54 // java/lang/StringBuilder
#9 = Methodref #8.#46 // java/lang/StringBuilder."<init>":()V
#10 = Methodref #8.#55 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#11 = Methodref #8.#56 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#12 = Methodref #8.#57 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#13 = Methodref #58.#59 // java/io/PrintStream.println:(Ljava/lang/String;)V
#14 = String #60 // www.atguigu.com
#15 = Class #61 // com/atguigu/java1/JavapTest
#16 = Class #62 // java/lang/Object
#17 = Utf8 num
#18 = Utf8 I
#19 = Utf8 flag
#20 = Utf8 Z
#21 = Utf8 gender
#22 = Utf8 C
#23 = Utf8 info
#24 = Utf8 Ljava/lang/String;
#25 = Utf8 COUNTS
#26 = Utf8 ConstantValue
#27 = Integer 1
#28 = Utf8 <init>
#29 = Utf8 ()V
#30 = Utf8 Code
#31 = Utf8 LineNumberTable
#32 = Utf8 LocalVariableTable
#33 = Utf8 this
#34 = Utf8 Lcom/atguigu/java1/JavapTest;
#35 = Utf8 (Z)V
#36 = Utf8 methodPrivate
#37 = Utf8 getNum
#38 = Utf8 (I)I
#39 = Utf8 i
#40 = Utf8 showGender
#41 = Utf8 ()C
#42 = Utf8 showInfo
#43 = Utf8 <clinit>
#44 = Utf8 SourceFile
#45 = Utf8 JavapTest.java
#46 = NameAndType #28:#29 // "<init>":()V
#47 = Utf8 java
#48 = NameAndType #23:#24 // info:Ljava/lang/String;
#49 = NameAndType #19:#20 // flag:Z
#50 = NameAndType #17:#18 // num:I
#51 = NameAndType #21:#22 // gender:C
#52 = Class #63 // java/lang/System
#53 = NameAndType #64:#65 // out:Ljava/io/PrintStream;
#54 = Utf8 java/lang/StringBuilder
#55 = NameAndType #66:#67 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#56 = NameAndType #66:#68 // append:(I)Ljava/lang/StringBuilder;
#57 = NameAndType #69:#70 // toString:()Ljava/lang/String;
#58 = Class #71 // java/io/PrintStream
#59 = NameAndType #72:#73 // println:(Ljava/lang/String;)V
#60 = Utf8 www.atguigu.com
#61 = Utf8 com/atguigu/java1/JavapTest
#62 = Utf8 java/lang/Object
#63 = Utf8 java/lang/System
#64 = Utf8 out
#65 = Utf8 Ljava/io/PrintStream;
#66 = Utf8 append
#67 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#68 = Utf8 (I)Ljava/lang/StringBuilder;
#69 = Utf8 toString
#70 = Utf8 ()Ljava/lang/String;
#71 = Utf8 java/io/PrintStream
#72 = Utf8 println
#73 = Utf8 (Ljava/lang/String;)V
#######################################字段表集合的信息################################################
{
private int num; //字段名
descriptor: I //字段描述符:字段的类型
flags: ACC_PRIVATE //字段的访问标识
boolean flag;
descriptor: Z
flags:
protected char gender;
descriptor: C
flags: ACC_PROTECTED
public java.lang.String info;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC
public static final int COUNTS;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 1 //常量字段的属性:ConstantValue
#######################################方法表集合的信息################################################
public com.atguigu.java1.JavapTest(); //构造器1的信息
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String java
7: putfield #3 // Field info:Ljava/lang/String;
10: return
LineNumberTable:
line 20: 0
line 18: 4
line 22: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/atguigu/java1/JavapTest;
private com.atguigu.java1.JavapTest(boolean); //构造器2的信息
descriptor: (Z)V
flags: ACC_PRIVATE
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String java
7: putfield #3 // Field info:Ljava/lang/String;
10: aload_0
11: iload_1
12: putfield #4 // Field flag:Z
15: return
LineNumberTable:
line 23: 0
line 18: 4
line 24: 10
line 25: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Lcom/atguigu/java1/JavapTest;
0 16 1 flag Z
private void methodPrivate();
descriptor: ()V
flags: ACC_PRIVATE
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 28: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/atguigu/java1/JavapTest;
int getNum(int);
descriptor: (I)I
flags:
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: getfield #5 // Field num:I
4: iload_1
5: iadd
6: ireturn
LineNumberTable:
line 30: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/atguigu/java1/JavapTest;
0 7 1 i I
protected char showGender();
descriptor: ()C
flags: ACC_PROTECTED
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #6 // Field gender:C
4: ireturn
LineNumberTable:
line 33: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/atguigu/java1/JavapTest;
public void showInfo();
descriptor: ()V //方法描述符:方法的形参列表 、 返回值类型
flags: ACC_PUBLIC //方法的访问标识
Code: //方法的Code属性
stack=3, locals=2, args_size=1 //stack:操作数栈的最大深度 locals:局部变量表的长度 args_size:方法接收参数的个数
//偏移量 操作码 操作数
0: bipush 10
2: istore_1
3: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
6: new #8 // class java/lang/StringBuilder
9: dup
10: invokespecial #9 // Method java/lang/StringBuilder."<init>":()V
13: aload_0
14: getfield #3 // Field info:Ljava/lang/String;
17: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: iload_1
21: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
24: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
//行号表:指名字节码指令的偏移量与java源程序中代码的行号的一一对应关系
LineNumberTable:
line 36: 0
line 37: 3
line 38: 30
//局部变量表:描述内部局部变量的相关信息
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 this Lcom/atguigu/java1/JavapTest;
3 28 1 i I
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: ldc #14 // String www.atguigu.com
2: astore_0
3: return
LineNumberTable:
line 15: 0
line 16: 3
LocalVariableTable:
Start Length Slot Name Signature
}
SourceFile: "JavapTest.java" //附加属性:指名当前字节码文件对应的源程序文件名
注:本文是学习 尚硅谷宋红康JVM全套教程(详解java虚拟机)所做笔记。