一个Java类从字节码到都能在JVM中使用经历了什么?

一个JAVA类从字节码到能在JVM中使用需要经历:加载,链接,初始化。
细分为:①类加载,②验证,③准备,④解析,⑤初始化


image.png

1.类加载:
ClassLoader 的作用是根据一个指定的类名称,找到或者生成其对应的字节码,然后把字节码转换成一个JAVA类(即java.lang.class)实例,除此之外,还负责加载JAVA应用所需要的资源、Native lib库等。

类加载器分为系统类加载器、应用开发自定义加载器。
其中,系统类加载器分为三层:
①引导类加载器:是由C++ 编写的,是JVM中用原生代码实现的,没有继承自ClassLoader。作用是加载JAVA的核心库。
②扩展类加载器:作用是用来加载JAVA的扩展库,虚拟机的实现会提供一个默认的扩展库目录。该类加载器在此目录里面查找并加载JAVA类。
③系统类加载器:加载应用类路径(ClassPath)下的class,一般Java应用的类加载器都是它加载的。

除了引导类加载器之外,所有的其他类加载器都有一个父类加载器(可以通过 ClassLoader 的 getParent() 方法得到)。

系统类加载器的父类加载器是扩展类加载器,
而扩展类加载器的父类加载器是引导类加载器,
开发自定义的类加载器的父类加载器是加载此类加载器的 Java 类的类加载器。

所以类加载器在尝试自己去加载某个类时会先通过 getParent() 代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推,从而形成了双亲委派模式。

类加载机制是通过 loadClass 方法触发的,查找类有没有被加载和该代理给哪个层级的加载器加载是由 findClass 方法实现的,而真正完成类加载工作是 defineClass 方法实现的。

Java 虚拟机是如何判断两个 Class 类是相同的?
Java 虚拟机不仅要看类的全名是否相同(含包名路径),还要看加载此类的类加载器是否一样,只有两者都相同的情况下才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类也是不同的,譬如一个 Java 类 cn.yan.Test 在编译后生成了字节码文件 Test.class,两个不同的类加载器 ClassLoaderA 和 ClassLoaderB 分别读取了这个 Test.class 文件,然后各自定义出一个 java.lang.Class 类的实例来表示这个类,这两个实例是不相同的,因为对于 Java 虚拟机来说它们是不同的类,这时候如果试图对这两个类的对象进行相互赋值则会抛出 ClassCastException 运行时异常。这在做插件化动态加载中要尤其注意。

2链接:是将 Java 类的二进制代码合并到 JVM 运行状态中的过程,链接依赖于成功的加载流程,链接包括验证、准备和解析等几个步骤

②-①验证:
过程是确保 Java 类二进制结构字节码的正确性,如果验证过程出现错误则会抛出 java.lang.VerifyError 错误。

②-②准备:
过程将创建 Java 类的静态域,同时将这些域的值设为默认值,准备过程不会执行代码(不会执行,不会执行,重要的事情说三遍)。

②-③解析:
过程确保类的继承、组合等关联类、接口能被正确找到,解析的过程可能会导致其它的 Java 类被加载(譬如在一个 Java 类中会包含对其它类或接口的引用或继承,解析就是去确认这些被引用的类能被正确的找到)。不同 JVM 解析策略可能不同,有些是在链接的时候就递归对所有依赖进行解析,有些只会在真正用到时时才进行解析(譬如只在方法中使用到了其他类)。

3初始化:
是指一个类第一次被使用时 JVM 会进行该类的初始化操作,初始化过程主要是执行类里面的静态代码块和初始化静态域。在一个类被初始化之前,它的直接父类也需要被初始化。但是一个接口的初始化不会引起其父接口的初始化。在初始化的时候,会按照源代码中从上到下的顺序依次执行静态代码块或初始化静态域。

Java 类和接口的初始化过程只会在特定时机发生,具体如下:

创建一个未被使用过的 Java 类实例。
调用一个未被使用过的 Java 类静态方法。
给一个未被使用过的 Java 类或接口中声明的静态域赋值。
访问一个未被使用过的 Java 类或接口中声明的静态域且该域不是常值变量。

此外再补充一句,一定搞清类加载初始化流程与类实例化流程的区别,这是两个东西,只有在某些情况下(譬如 new 一个初次使用的对象实例等)这两者才会有直接关联,而关联关系也是先有类家在初始化流程,完事才有类实例化流程。

推荐阅读更多精彩内容