看到这个题目的时候,你可能就会觉得,阿粉,这不是挺简单的一个问题么
如何加载?不就是 加载,链接,初始化 这三步嘛,说白了不就是类加载过程么
那么,你知道这三步具体又做了什么嘛?这就是本篇文章想要写的
加载
加载的过程,就是查找字节流,并根据查找到的字节流来创建类的一个过程
Java 语言的类型可以分成两大类:基本类型和引用类型。基本类型就是由 JVM 预先定义好的,所以也就没有查找字节流这一说了
对于引用类型来说的话,又可以细分为四种:类,接口,数组类和泛型参数。因为泛型参数在编译过程中会被擦除,所以在 JVM 中就只有前三种。而数组类又是由 JVM 直接生成的,所以查找字节流的话,就只有类和接口了。
那么 JVM 是怎么查找字节流的呢?如果你对这块内容比较熟的话,应该就能想起来类加载器,它主要有四类:启动类加载器,扩展类加载器,应用程序类加载器和用户自定义类加载器
这块又有个知识点就是双亲委派机制:大概就是如果一个类加载器收到了类加载的请求,首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成。通过双亲委派机制就能保证同样一个类只被加载一次
经过类加载器之后,这个类就算是加载进来了
链接
对链接过程而言, jvm 实现具有灵活性,但必须保留下列属性:
1、在链接之前,类或者接口必须已经被完全加载;
2、在初始化之前,类或者接口必须已经被完全验证和准备;
3、链接过程中检测到的程序错误会抛出到程序中某个位置,在该位置上,程序将采取某些操作,这些操作可能会直接或间接地链接到类或者接口所涉及到的类或者接口。
链接这块又分为三部分:验证,准备,解析
验证阶段就是想要看看 class 文件的前 8 位是不是 java 标识符,想看看符不符合规范什么的
准备阶段就是给静态字段分配内存。除了分配内存之外,部分 JVM 还会在此阶段构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的方法表,这个方法表是用来解决动态绑定的问题的,
解析时通过这个方法表,根据实际类型来解析获取对应的方法。
在 class 文件被加载到 JVM 之前,这个类没办法知道其他类和方法,字段所对应的具体地址,甚至都不知道自己的方法,字段的地址,所以如果需要引用这些成员时, Java 编译器就会生成一个符号引用,在运行阶段,这个符号引用一般都可以准确的定位到具体目标上
解析阶段主要就是将符号引用解析成实际引用。如果符号引用指向一个未被加载的类,或者没有被加载类的字段或方法,此时解析阶段就会触发这个类的加载(但不一定会触发这个类的链接以及初始化)
在解析阶段,不同的 JVM 有不同的解析策略,例如:
public class A {
public void main(String args[]) {
B b = null;
}
}
策略 1 :链接 A 的时候发现引用了 B,因此加载 B
策略 2 :链接 A 的时候发现引用了 B,但是 B 没有被使用,所以暂时不加载 B。在真正使用 B 的时候才进行加载,比如
b = new B()
;
所以在一些 JVM 实现中,可能采取在使用时才会解析类或接口中的符号引用,或采取在该类或者接口被验证时一次性解析全部符号引用。这取决于采用的是哪种策略,也意味着解析过程可能在类或者接口被初始化后还会进行
初始化
在 Java 代码中,如果想要初始化一个静态字段,可以在声明的时候直接赋值,也可以选择在静态代码块中对它赋值
如果直接赋值的静态字段被 final 修饰了,而且这个静态字段是基本类型或者字符串时,就会被 Java 编译器标记成常量值,初始化就直接被 JVM 完成了。除此之外的直接赋值操作,还有所有静态代码块中的代码,就会被 Java 编译器放到同一个方法中,并且把它命名为
类加载的最后一步就是初始化,就是给标记为常量值的字段赋值,执行
方法的过程。这个时候 JVM 会通过加锁来确保类的
方法只被执行一次
方法可厉害了,因为:
-
方法与类的构造函数不同,它不需要显示的调用父类的
方法,虚拟机会保证在子类的
方法执行之前,父类的
方法已经执行完毕。因此在虚拟机中第一个被执行的
方法的类肯定是 java.lang.Object
-
方法对于类或接口来说不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成
方法。
-
接口中不能使用静态初始化块,但是仍有 static 变量的赋值操作,所以也会有
方法,但是接口执行
方法不需要先执行父接口的
方法。只有当父接口中定义的变量被使用到时,才会执行
方法。
-
虚拟机会保证一个类的
方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的
方法,其它线程都需要阻塞等待