从class文件变成内存中的类按先后顺序需要经过加载、链接以及初始化这三大步骤。

目录

加载

链接

初始化


加载

查找字节流,并且根据字节流创建类的过程。JVM是借助类加载器来完成查找字节流的过程,用定制木质家具来比较的话,你首先需要给木匠说一下房型,这里的家具就是类,木匠就是类加载器。每一个木匠都有同一个祖宗“鲁班”,也就是说每一个类加载器都有同一个类加载器叫做启动类加载器,它是用C++来实现的,没有相应的Java对象。所以我们在Java中只能用Null来指代。除了这个启动类加载器,其他的都是ClassLoader类的子类,因此也有对应的Java对象,这些类加载器首先需要启动类加载器将它们加载到JVM中,才能加载。

Java虚拟机中有一个双亲委派模型,就是指:当一个类加载器收到了加载请求的时候,首先自己不能执行,先往上抛,把请求转发给父类加载器,在父类加载器没有找到所请求的情况下,这个类加载器才会尝试着去加载,所以每一个请求最后都会传递到最高的加载器——启动类加载器。这样做的好处就是:避免了类的重复加载,确保一个类的全局唯一性。这样也防止了核心API被改变。

在Java9之前,类加载器除了启动类加载器还有扩展类加载器与应用类加载器。

启动类加载器:它是类加载器的最高父类,并且无法被Java程序直接引用,因为它是由C++实现的,没有相应的Java对象。当用户想编写自定义类加载器的时候,需要将加载请求委派给启动类加载器的时候直接用null来替代就可以了,它负责将存放在JRE里面的lib目录中或者被-Xbootclasspath参数所指定的路径中且被虚拟机识别的类库加载到虚拟机的内存中。

扩展类加载器:它的父类是启动类加载器,它负责加载一些相对来说比较次要的、通用的类。例如就是JRE里面的lib/ext目录下的jar包的类,或者是被java.ext.dirs系统变量所指定的路径中的类库。

应用类加载器:它的父类是扩展类加载器,它负责加载应用程序路径下的类,也就是CLassPath上所指定的类库,开发者可以直接用这个类加载器。

当然这是Java9之前的,因为Java9引入了模块系统,而且将扩展类加载器改为了平台类加载器,JavaSE里面除了少数的几个关键模块,其他的模块均有平台类加载器去加载。

 

这个图就是双亲委派模型的过程。

 

类的唯一性是由类加载器实例以及类的全名一同确定的。即使是同一串字符流如果经由不同的类加载器去加载,也会得到两个不同的类。就仿佛给加了版本号一样。

 

链接

将创建成的类合并至JVM中,并使之可以执行的过程,这个过程可以分为3个子过程,验证、准备与解析。

验证:它是链接过程的第一部分,它确保了被加载的类能够满足Java虚拟机的约束条件,并且不会危害虚拟机自身的安全。一般而言,Java编译器生成的类文件必然满足Java虚拟机条件,但是class文件并不一定要求用Java源码编译而来,可以使用任何途径产生。如果虚拟机不检查字节流并对其完全信任,很有可能因为载入了有害的字节流而导致系统崩溃。

准备:为被加载类的静态字段分配内存,这些变量所使用的内存就会在方法区被分配,

解析:将符号引用转化为实际引用,如果符号引用指向一个未被加载的类,或者未被加载类的方法或字段那么就将触发这个类的加载。

符号引用:在class文件被加载至JVM之前,这个类是无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己的方法、字段的地址。因此,每当需要引用这些成员的时候,Java编译器就会生成一个符号引用,在运行阶段,这个符号引用一般都能无歧义的对应到具体目标上。它以一组符号来描述所引用的目标,与JVM实现的内存布局无关,引用的目标并不一定加载到内存中。

实例引用:指的是直接指向目标的指针、相对偏移量、或者能间接定位到目标目标的句柄,与虚拟机实现的内存布局相关,如果有了实例引用,那么被引用的目标肯定已经在内存中存在。

初始化

这是类加载机制的最后一步,这时候才算是真正的开始执行类中定义的Java程序代码。如果直接赋值的静态字段被final所修饰,并且类型是基本类型或者是字符串,那么这个字段会被Java编译器定义为常量值,初始化直接由JVM完成,除此之外的直接赋值以及静态代码块中的代码,会被Java编译器编译到同一个方法中,并且被命名为<clinit>。JVM通过加锁以保证类的<clinit>方法只被执行一次。

简单地说就是给之前已经分配好的基本类型的变量或者静态字符串赋值,这里的值是代码中的值。

 以下几种情况,必须立即对类初始化

  • 使用new关键字实例化对象,初始化new指令的目标类
  • 读取或者设置一个类的静态字段,初始化该静态字段所在的类
  • 调用一个类的静态方法,初始化该静态方法所在的类
  • 用反射API对某个类进行调用的时候,初始化反射的目标类
  • 初始化子类时,发现父类没有被初始化,应该先触发父类的初始化
  • 虚拟机启动时,用户指定的主类需要进行初始化
  • 初次调用MethodHandler实例时,初始化该MethodHandler指向的方法所在类