文章目录
JVM负责Java程序的执行,那么从Java语句到执行程序,之间的过程是怎么发生的呢?
首先,我们提到JVM使Java语言具备跨平台性,其本质实现就是java源程序.class
文件编译后生成的.class
文件,其是JVM能识别的语言,那么为什么要编译呢?
- 可执行性:Java语言是高级语言,JVM并不认识这种语言,因此需要转变成JVM能识别的语言
- 移植性:通过编译成.class文件,能够实现一处编译、处处运行。
之后,就是JVM将能识别的代码通过解释器转换为特定系统的机器执行码。
1. 编译
上面说到,编译就是将java源代码转换成JVM能识别的字节码文件,整个过程如下图:
词法分析:
词法分析就是将源程序(可以认为是一个很长的字符串)读进来,并且“切”成小段(每一段就是一个词法单元 token),每个单元都是有具体的意义的,最后得到一个个“单词”。
比如int a = b + 2
,上面每一个数字或符号都是一个标记,因为他们都有具体的意义,且不可再分。
语法分析:
是对token流进行语法检查、并构建由输入的单词组成的数据结构(语法树/抽象语法树)。抽象语法树是一种描述程序代码语法结构的树形表示方式,比如类、方法、循环结构、接口、代码注释都可以是抽象语法树上的一个节点。
语义分析:
语法分析后,编译器获得了一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的。而语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查,比如进行类型检查,控制流检查,数据流检查,解语发糖。
字节码生成:
将注解语法树转换成字节码,并将字节码文件写入***.class**文件
静态绑定和动态绑定
绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来,绑定分为静态绑定(前期绑定) 和 动态绑定(后期绑定)
静态绑定
静态绑定指的是在程序执行前就已经被绑定(编译过程就确定调用方法所属类),在Java中,只有final、static、private和构造方法
动态绑定(后期绑定)
动态绑定指定的是在运行时再决定这个方法由哪个对象调,这个过程就被成为动态绑定。比如看下面的方法,在编译器并不知道哪个对象调用,而只能在运行期间才能确定方法的调用对象,才能将方法加载到对用的区域。
class A{
public void say(int a){
System.out.println(a);
}
}
public class Singleton {
public static void main(String[] args) {
A a1 = new A();
A a2 = new A();
a1.say(1);
a2.say(2);
}
}
2. 类加载
有了.class
文件后,JVM就可以运行这些.class
文件了,由于.class文件是编译后的静态文件,因此JVM需要将其加载进来,并为执行程序前做准备(连接)。
2.1 类加载流程
加载:
在加载阶段, 虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 将类的class文件读入内存,并为之创建一个
java.lang.Class
对象,也就是说当程序中使用任何类时,系统都会为之建立一个java.lang.Class
对象, 作为方法区这个类的各种数据的访问入口。
连接:
当类被加载后,系统为之生成一个对应的Class对象,接着会进入连接阶段,连接阶段将会负责把类的二进制文件合并到JRE中。类连接分为如下三个阶段:
- 验证:验证是连接阶段的第一步, 这一阶段的目的是为了确保 Class文件的字节流中包含的信息符合当前虚拟机的要求, 井且不会危害虚拟机自身的安全。
- 文件格式校验:验证字节流是否符合 Class文件格式的规范, 井且能被当前版本的虚拟机处理
- 对类的元数据进行语义检验,如是否继承了final类、是否实现了接口的全部方法等,确保不存在不符合java语义的类
- 字节码验证:确保字节码流可以被Java虚拟机安全的执行。字节码流是操作码组成的序列。每一个操作码后面都会跟着一个或者多个操作数。字节码检查这个步骤会检查每一个操作码是否合法;
- 符号引用验证:对类自身以外(常量池中的各种符号引用) 的信息进行匹配性的校验,如符号引用的类能够找到,符号引用的字段能否访问
- 准备:正式为类变量(就是静态变量)分配内存并设置类变量初始值(默认值)的阶段,这些变量所使用的内存都将在**方法区(也称静态区)**中进行分配
- 解析:把类中的符号引用转化为直接引用(比如说方法的符号引用,是有方法名和相关描述符组成,在解析阶段,JVM把符号引用替换成一个指针,这个指针就是直接引用,它指向该类的该方法在方法区中的内存位置)
初始化:
在前面都是JVM控制的执行准备过程,从初始化开始,class代码中的语句开始执行。包括为静态变量赋值,执行静态代码块中的语句,执行父类静态代码块中语句等等,具体过程如下:父类静态代码块—>子类静态代码块。
那么有人就会问了,为什么这里不和其他代码放在一起执行呢,而非要先初始化呢?
原因是为了提升程序的性能,我们知道静态变量或静态代码块是类专属的,与对象无关,因此在程序运行期间只用准备一次即可,因此为了提高性能,提前在类加载时初始化静态代码块,并将静态数据存放在方法区(静态区)。
实例化:
前面我们提到了,程序运行期间静态代码块只会执行一次,静态变量也只与类相关,因此在类加载时就一起初始化了,并且单独放在方法区。接下来,程序执行必然用到类的对象,此时就要执行对象的实例化,实例化的实现和初始化可谓大不相同,具体的之后再说。
2.2 双亲加载机制
2.2.1 为什么提出双亲委派机制
面试官问,能不能自己写个类叫java.lang.system?
答案是不能,首先是编译器就会报错,其次我们如果写好了这个类,然后使用类加载器就加载它,也无法加载,因为类加载器必须继承ClassLoader
类,而类加载采用双亲委派机制,也就是父类能够加载的必须由父类先加载,因此父类会先加载系统的system类,子类也就无法加载自己书写的system类的。
有人会说,我通过重写父类加载器来打破双亲委派机制,不就可以了,实际上是不行,因为BootstrapClassLoader是JVM层面的,对于系统的核心类库都是通过JVM进行加载,如果写了和核心库相同的类,运行时会报错。
自定义的类加载器加载我们自定义的类
- 会调用自定义类加载器的loadClass方法
- 而我们自定义的classLoader必须继承ClassLoader,loadClass方法会调用父类的defineClass方法
- 而父类的这个defineClass是一个final方法,无法被重写
- 所以自定义的classLoader是无论如何也不可能加载到以java.开头的类的
jvm如何认定两个对象属于同一类型
- 都是由同名的类完成实例化的
- 两个实例各自对应的同名的类的加载器必须是同一个。
所以为了避免重复加载和核心库被篡改,Java提出了双亲委派机制。
2.2.2 委派实现过程
在JVM中,类加载是由多个类加载器完成的,并且实现了一种“父委派机制”。
-
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,
-
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,
-
如果父类加载器可以完成类加载任务,就成功返回
-
倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载
- 启动类加载器(Bootstrap ClassLoader):负责加载
JAVA_HOME\lib
目录下文件的类库 - 扩展类加载器(Extension ClassLoader):负责加载
JAVA_HOME\lib\ext
目录中的类库 - 应用程序类加载器(Application ClassLoader):负责加载用户路径(
classpath
)下的类库
特点:
- 全盘负责:当一个类加载器加载一个类时,该类所依赖的其他类也会被这个类加载器加载到内存中。
- 缓存机制:所有的Class对象都会被缓存,当程序需要使用某个Class时,类加载器先从缓存中查找,找不到,才从class文件中读取数据,转化成Class对象,存入缓存中。
那么如果说一个由父类加载器加载的类要使用由子类加载器记载的类,而起步就是父类加载器,其无法委托子类加载器来实现加载,现在怎么办呢?
2.3 破坏双亲委派
前面提到,双亲委派机制避免了重复类加载并保证了核心库的安全,但是有的代码库却破坏了双亲委派,这是为什么呢? 这要提到双亲委派的弊端:无法做到不委派,也无法向下委派。
2.3.1 JDBC破坏双亲委派
JDBC为什么要打破双亲委派
- JDBC的Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,比如MySQL驱动包
- DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager位于 $JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载,
- 而其Driver接口的实现类是位于服务商提供的 Jar 包,而无法利用Bootstrap类加载器加载
**根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。**因此,需要定义子类加载去加载Driver实现,这就是向下委派,打破了双亲委派机制。
首先需要注意一点,JDBC4.0之前使用Class.forName("")方式加载驱动是不会破坏双亲委派的,在JDBC4.0之后使用spi机制才会破坏双亲委派机制。
Class.forName("")方式不破坏双亲委派机制
由于这段代码是写在我们调用方,由我们自己来加载驱动类,由于遵循全盘负责委托机制,他使用的必定是和我们调用类的加载器一样的 ApplicationClassLoader这个加载器。从 ApplicationClassLoader由下而上进行委派加载。
spi机制
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。由于SPI是实现向下委派的,因此为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。下面是jdbc实例
java.sql.DriverManagement这个类的静态代码块去加载驱动类
public class DriverManager {
//省略多余代码
static {
//这个方法的具体逻辑则是从驱动包下META-INF/services文件中,加载里面写好的Driver的实现类。
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
// 但是,加载这个类的加载器使用的是线程上下文中的AppClassLoader,