前言
前面众多文章有关讲解的都是些JVM的内存与垃圾回收器相关信息,那么对于本篇开始我们将把目光转移到Class文件与加载器身上去,去看看字节码文件里到底有些什么信息?是怎么加载到我们内存里?
一、Class文件的概述
字节码文件的跨平台性
================================
Java 语言:跨平台性(write one run anywhere)
当Java源代码成功编译成字节码后,如果想在不同的平台上面运行,则无须再次编译
这个优势不再那么吸引人了。Python、PHP、Per1、Ruby、Lisp等有强大的解释器
跨平台似乎已经快成为一门语言必选的特性
java 虚拟机:跨语言的平台
Java虚拟机不和包括Java 在内的任何语言绑定,它只与“Class 文件”这种特定的二进制文件格式所关联。
无论使用何种语言进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。可以说,统一而强大的Class文件结构,就是Java虚拟机的基石、桥梁。
JAVA语言和JVM的规范
可访问官方入口查看详细的规范:访问地址
遵守Java虚拟机规范,也就是说所有的JVM环境都是一样的,这样一来字节码文件可以在各种Jw上运行。
Java的前端编译器
================================
JAVA源代码遵循JVM规范可正常运行在JVM中
前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件
javac是一种能够将Java源码编译为字节码的前端编译器
Javac编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤分别是
- 词法解析
- 语法解析
- 语义解析
- 生成字节码
前端编译器VS后端编译器
HotSpot VN并没有强制要求前端编译器只能使用javac来编译字节码
,其实只要编译结果符合JVW规范都可以被JVM所识别即可
在Java的前端编译器领域除了javac之外
,还有一种被大家经常用到的前端编译器,那就是内置在Eclipse中的ECJ(EclipseCompiler for Java)编译器
和Javac的全量式编译不同,EC是一种增量式编译器
在Eclipse中,当开发人员编写完代码后,使用“Ctrl+S”快捷键时,ECJ编译器所采取的编译方案是把未编译部分的源码逐行进行编译,而非每次都全量编译
因此ECJ的编译效率会比javac更加迅速和高效,当然编译质量和javac相比大致还是一样的
EC不仅是Eclipse的默认内置前端编译器,在Tomcat中同样也是使用ECJ编译器来编译jsp文件
由于ECJ编译器是采用GPLv2的开源协议进行源代码公开,所以大家可以登录eclipse官网下载ECJ编译器的源码进行二次开发。
默认情况下,IntelliJ IDEA使用javac编译器。(还可以自己设置为Aspect]编译器ajc)
前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给HotSpot的3IT编译器负责
透过字节码指令看代码细节
================================
先来看看几个常见的面试题
- 类文件结构分几个部分?
- 字节码都有哪些?Integer x = 5,int y = 5 比较 x == y都经过哪些步骤
在面对这些问题的时候,接下来先看下面这个示例代码是怎么回事
public class IntegerTesti
public static void main( String[] args) {
Integer x = 5;
int y = 5;
System.out.print1n(x == y);
Integer i1 = 10;
Integer i2 = 10;
System.out.print1n(i1 == i2);
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);
}
}
按照我们前面的知识,我们可以回顾一下看看操作数栈与局部变量表里是怎么样操作的?
接下来我们可以在使用一个示例代码来体会这种过程
public class stringTest {
public static void main( string[] args) i
string str = new string("hello") + new String("world");
String str1 ="hellowor1d";
system.out.println(str == str1);
}
}
我们回顾回顾前面知识点,看看第一行代码 + 符号做了些什么事情
接下来我们可以在使用一个示例代码来体会这种过程
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.print1n( "Son.x = ” +x);
}
}
此时我们用一个Test类调用这里两个类,看看将程序运行后会输出什么呢?
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
这时会有小伙伴好奇了,为什么会是这样的一个输出结果呢?我们先看简单的是什么情况
public class SonTest{
public static void main(String[] args) {
Father f = new Father();
system.out.println(f.x);
}
}
//运行结果如下:
Father.x = 10
20
对于非静态的成员变量的赋值过程主要分为以下几种:
- 默认初始化
- 显示初始化/代码块初始化
- 构造器中初始化
- 有对象后,可通过对象.属性名调用(权限允许情况下)
接下来我们通过字节码指令的方式看看是不是与我们说的步骤是差不多的?
此时我们回到刚刚Test调用Son类的情况,进行分析看看
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
接下来我们通过字节码指令的方式看看Son做了哪些事情
之前我们分析了Father的构造器方法有哪些,以及做了哪些事情
此时调用Father的构造器时,应该执行print方法,但是Son类重写了所以调用Son类的方法
二、虚拟机的基石:CLass文件
字节码文件里是什么?
================================
源代码经过编译器编译之后便会生成一个字节码文件,字节码是一种二进制的类文件,它的内容是jVM的指令
,而不像C、C++经由编译器直接生成机器码
。
什么是字节码指令(byte code)?
================================
JAVA虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成
。
虚拟机中许多指令并不包含操作数,只有一个操作码,比如说我们可以看看上面示例代码的
如何解读供虚拟机解释执行的二进制字节码
================================
方式一:采用notepad++,安装HEX-Edirot插件或者Binary Viewer插件
方式二:IDEA插件: jclasslib 或jclasslib bytecode viewer客户端工具
方式三:使用Javap指令:jdk自带反解析工具
三、Class文件的结构
官方文档位置
================================
可访问官方入口查看详细的内容:访问地址
Class 类的本质
================================
任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在(可网络传)。Class 本质是一组以8位字节为基础单位的二进制流。
Class 文件格式
================================
Class 的结构不像XML等描述语言,由于它没有任何分隔符号。
所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
文件格式采用一种类似于c语言结构体的方式进行数据存储,这种结构中只有:无符号数和表
- 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于
描述有层次关系的复合结构的数据
,整个Class 文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明
Class 文件结构
================================
Class文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的。
Class文件的总体结构如下:
- 魔数
- class文件版本常量池
- 访问标志
- 类索引,父类索引,接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
魔数结构讲解:
用四个字节来表示魔数,主要对应识别当前文件是不是一个Class文件的标识
Class文件版本结构讲解:
当前字节码文件是在那个版本下运行的编译,分为大版本/小版本(主版本/副版本)
常量池结构讲解:
通过两个字节告诉我常量池多长,下面是该常量池的数组
访问标识标识讲解:
用于当前标识是否为一个类还是接口、权限是什么?有没有abstract修饰、final修饰等
类索引、父类索引、接口索引集合讲解:
用于指明当前类是什么名、父类是什么名、当前类实现的接口长度、存放的数组等信息
字段表集合讲解:
一个类可有多个字段、所以这里也要表明长度、以及存放字段的数组信息
方法表集合讲解:
当前类定义的方法,也有一个方法表的长度、以及存放方法的数组信息
属性表集合讲解:
我们进行一个类的字节码的时候,一般方法里Code这些属性,就指的这