1、什么是机器码和字节码?Java是如何实现一次编译,到处执行的?
Q1:什么是机器码?
A1:在计算机的世界中,0和1是计算仅能识别的信号,经过0和1的不同组合产生了数字上的操作,同时通过不同的组合也产生了各种字符。同样,可以通过不同的组合产生不同的机器指令。在不同的时代,不同的厂商,机器指令组成的集合是不同的。但毕竟CPU是底层基础硬件,指令集通常以扩展兼容的方式向前不断演进。而机器码是离CPU指令集最近的编码,是CPU可以直接解读的指令,因此机器码肯定是与底层硬件系统耦合的。
Q2:什么是字节码?
A2:如果某个程序因为不同的硬件平台而需要编写多套代码,这肯定是万万不行的。那么怎么实现跨平台呢?答案就是:计算机领域的任何问题都可以通过增加一个中间层(在Java中,JVM就是这个中间层)来解决。因此,中间码(我的理解是由中间层来解读的码)应运而生,即“字节码”。一个字节(8位)可以存储256种不同的指令信息(Java大概有200条左右的指令),一个这样的字节称为字节码(Bytecode)。
Q3:Java是如何实现一次编译,到处执行的呢?
A3:我们编写好的.java文件是源代码文件,并不能交给机器直接执行,需要将其编译成为字节码甚至是机器码文件。所以要先通过静态编译器将源文件编译成为字节码文件,再由JVM将字节码解释执行成计算机能够识别的机器码,屏蔽对底层操作系统的依赖;JVM也可以将字节码编译执行,如果是热点代码(例如循环等常用指令)。会通过JIT(JIT就是Just in time,即时编译技术。使用该技术,可以加快代码程序的运行速度。JIT在运行时,可以将翻译过的代码保存起来,类似于缓存技术,当下次再遇到该代码的时候,就可以直接将该代码编译为机器码。JIT不是必须的,有时候可能会适得其反)动态地编译为机器码,提高执行效率。如下流程图所示:(interpreter译为翻译员)
2、类加载过程
2.1、类加载的步骤
在冯诺依曼定义的计算机模型中,任何程序都需要加载到内存才能与CPU进行交流。字节码.class文件同样需要加载到内存中,才能实例化类。
“兵马未动,粮草先行”,ClassLoader正是准备粮草的先行军,它的使命就是提前加载.class类文件到内存中。在加载类时,使用的是Parent Delegation Model,译为“双亲委派模型”,(码出高效书中提到这个译名有点不妥,应该译为“溯源委派加载模型”更为贴切)。
Java的类加载器是一个运行时核心基础设施模块,如下图所示,主要是在启动之初进行类的Load、Link和Init,即加载、连接、初始化。
第一步,Load(加载)阶段读取类文件产生二进制流,并转化为特定的数据结构,初步校验cafe babe(cafe babe是Gosling定义的一个魔法数,意思是Coffee Baby,其十进制值为3405691582。它的作用是:标志该文件是一个Java类文件,如果没有识别到该标志,说明该文件不是Java类文件或者文件已受损,无法进行加载)魔法数、常量池、文件长度、是否有父类等,然后创建对应类的java.lang.class实例。
第二步,Link(连接)阶段包括验证、准备、解析三个步骤。验证是更详细的校验,比如final是否合规、类型是否正确、静态变量是否合理等;准备阶段是为静态变量分配内存,并设定默认值,解析类和方法,确保类与类之间的相互引用正确性,完成内存结构布局。
第三步,Init(初始化)阶段执行类构造器<clinit>方法,如果赋值运算时通过其他类的静态方法来完成的,那么会马上解析另外一个类,在虚拟机栈中执行完毕后通过返回值进行赋值。
类加载是一个将.class字节码文件实例化成Class对象并进行相关初始化的过程。在这个过程中,JVM会初始化继承树上还没有被初始化的所有父类,并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等。某些类在使用时,也可以按需求由类加载器进行加载。
2.2、类中之王Class
全小写的class是关键字,用来定义类,而首字母大写的Class,它是所有class的类。(我将它理解为抽象的抽象,不知道对不对)这句话理解起来有难度,是因为类已经是现实世界中某种事物的抽象,为什么这个抽象还是另外一个类Class的对象?请看下面的示例代码:
package Test;
import java.lang.reflect.Field;
public class ClassTest {
//数组类型有一个魔法属性:length来获取数组长度
private static int[] array = new int[3];
private static int length = array.length;
//任何小写class定义的类,也有一个魔法属性:class.来获取此类的大写Class类对象
private static Class<One> one = One.class;
private static Class<Another> another = Another.class;
public static void main(String[] args) throws Exception{
//这是我自己加上去的,为了看一下"类.class"到底获取的是什么
System.out.println("one-------->"+one);
System.out.println("another-------->"+another);
//通过newInstance方法创建One和Another的类对象
/**
* Class类下的newInstance()在JDK9中已经置为过时,使用getDeclaredConstructor()的方式。强调一下new和newInstance的区别,
* new是强类型校验,可以调用任何构造方法,在使用new操作的时候,这个类可以没有被加载过,
* 而Class类下的newInstance是弱类型校验,只能调用无参构造方法,如果没有默认的无参构造方法,就抛出InstantiationException异常
*/
One oneObject = one.newInstance();
oneObject.call();
Another anotherObject = another.newInstance();
anotherObject.speak();
//通过one这个大写的Class对象,获取私有成员属性对象Field,Field是反射包中的一个类
/**
* getDeclaredField():获得某个类的所有声明的字段,即包括public、private和proteced,但是不包括父类的申明字段。
* 可以使用类似的方式获取其他声明,如注解、方法等
*/
Field privateFieldInOne = one.getDeclaredField("inner");
//设置私有对象可以访问和修改
/**
* private成员在类外是否可以修改?通过setAccessible(true)操作,即可使用大写Class类的set方法修改其值。
* 如果没有这一步,则抛出异常
*/
privateFieldInOne.setAccessible(true);
privateFieldInOne.set(oneObject,"world changed.");
//成功修改类的私有属性inner变量值为world changed.
System.out.println(oneObject.getInner());
}
}
class One{
private String inner = "time flies.";
public void call(){
System.out.println("hello world.");
}
public String getInner(){
return inner;
}
}
class Another{
public void speak(){
System.out.println("easy coding.");
}
}
执行结果:
one-------->class Test.One
another-------->class Test.Another
hello world.
easy coding.
world changed.
2.3、类加载器的层次结构
类加载器类似于原始部落结构,存在权利等级制度。最高的一层是家族中威望最高的Bootstrap,它是JVM启动时创建的,通常由与操作系统相关的本地代码实现的,是最根基的类加载器,负责装载最核心的Java类,比如Object、System、String等;第二层是JDK9版本中,称为Platform ClassLoader,即平台类加载器,用以加载一些扩展的系统类,比如XML、加密、压缩相关的功能类等,JDK9之前的加载器是Extension ClassLoader(扩展类加载器);第三层是Application ClassLoader的应用类加载器,主要是加载用户定义的CLASSPATH路径下的类。第二、第三层类加载器为Java语言实现,用户也可以自定义类加载器。最高一层的Bootstrap是通过C/C++实现的,并不存在于JVM体系内。类加载器具有等级制度,三是并非继承关系,以组合的方式来复用父加载器的功能,这也符合组合优先原则,详细的双亲委派模型如下图所示:
低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。如果低层次的类记载器想加载一个未知类,要非常礼貌地向上逐级询问:“请问,这个类已经加载了吗?”,被询问的高层次类加载器会自问两个问题:
- 我是否已经加载过此类?
- 如果没有,是否可以加载此类?
只有当所有高层次类加载器在两个问题上的答案均为“否”时,才可以让当前类加载器加载这个未知类。如上图所示,左侧的绿色箭头向上逐级询问是否已加载此类,直至Bootstrap ClassLoader,然后向下逐级尝试是否能够加载此类,如果都加载不了,则通知发起加载请求的当前类加载器,准予加载(在右侧的三个蓝色标签里,列举了此层类加载器主要加载的代表性类库,事实上不止于此)。
2.4、需要自定义类加载器的情况
学习了类加载器的实现机制后,明白了双亲委派模型并非强制模型,用户可以自定义类加载器,那么我们在什么情况下需要自定义类加载器呢?
- 隔离加载类。在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如,阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。
- 修改类加载方式。类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载。
- 扩展加载源。比如从数据库、网络,甚至是电视机机顶盒进行加载。
- 防止源码泄露。Java代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义,还原加密的字节码。
实现自定义类加载器的步骤如下:
- 继承ClassLoader
- 重写findClass()方法
- 调用defineClass()方法
一个简单的类加载器实现的示例代码如下:
package Test;
import java.io.FileNotFoundException;
public class CustomClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{
try{
byte[] result = getClassFromCustomPath(name);
if(result == null){
throw new FileNotFoundException();
}else{
return defineClass(name,result,0,result.length);
}
}catch(Exception e){
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCustomPath(String name){
//从自定义路径中加载指定类
}
public static void main(String[] args){
CustomClassLoader customClassLoader = new CustomClassLoader();
try{
Class<?> clazz = Class.forName("One",true, CustomClassLoader);
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
}catch (Exception e){
e.printStackTrace();
}
}
}