虚拟机类加载机制是把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
需要注意的是 Java 语言与其他编译时需要进行连接工作的语言不通,它的连接过程是在程序运行期间完成的,这样会在类加载时稍微增加一些性能开销,但是却能为 Java 应用程序提供高度的灵活性。例如,如果编写一个使用接口的应用程序,可以等到运行时再指定其实际的实现。
通过文章,你可以了解到以下内容
类从被加载到虚拟机内存中开始,到卸载出内存,生命周期七个阶段:
加载、验证、准备、解析、初始化、使用、卸载
其中加载、验证、准备、初始化和卸载这五个阶段时确定的,因为 Java 支持运行时绑定,所以解析再某些情况下可以在初始化阶段之后
虚拟机规范规定有且只有四中情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前)
详细介绍一下类加载的全过程,加载、验证、准备、解析和初始化
加载过程需要完成三件事情
主要注意的是,第一点的二进制字节流可以从文件、网络、数据库等地方获取;加载阶段也可以由用户自定义的类加载器完成;加载和连接的部分内容(文件格式校验)是交叉进行的
验证的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全
(1)文件格式验证:
(2) 元数据验证:
(3) 字节码验证:
(4) 符号引用验证:
准备阶段是正式为类变量分配内存并设置类变量初始值;内存分配仅包括类变量(被 static 修饰的变量),而不包括实例变量,而这里的初始化,代表零值,即 static 变量初始化为 0
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
解析的动作主要针对类或接口的解析、字段的解析、类方法解析、接口方法解析
初始化阶段是根据程序员通过程序制定的主观计划去初始化类变量的其他资源;也可以说是执行类构造器
类加载器只用于实现类的加载动作,但是对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在 Java 虚拟机中的唯一性。
虚拟机有两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++ 语言实现,是虚拟机自身的一部分;另外一种就是所有其他的类加载器,这些类加载器都是由 Java 语言实现,独立于虚拟机外部,并且全部继承自抽象类 java.lang.ClassLoader
我们的应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器
双亲委派模型的工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要的类)时,子加载器才会唱谁自己去加载
好处:Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它放在 rt.jar 之中,无论哪一个类加载器加载这个类,最终都是启动类加载器去完成加载,因此 Object 类在程序的各种类加载器环境中都是同一个类;相反,如果没有双亲委派模型,由各个类加载器自行去加载的话,如果用户自己写了一个名为 java.lang.Object 的类,并放在 ClassPath 中,那系统将会出现多个不同 Object 类,造成程序混乱
双亲委派模型是 Java 设计者们推荐给开发者的类加器实现方式,但也有例外,出现过三次大规模的破坏情况
双亲委派模型是 JDK1.2之后才引入的,但类加载器和抽象类 java.lang.ClassLoader 则在 JDK 1.0 存在了,面对已经存在的用户自定义类加载器的实现代码,Java 设计者们引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK 1.2 之后的 java.lang.ClassLoader 添加了一个新的 protected 方法 findClass(),在此之前,用户去继承 java.lang.ClassLoader 的唯一目的是为了重写 loadClass() 方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法 loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的 loadClass()。
JDK1.2之后已不提倡用户再去覆盖 loadClass() 方法,因为 loadClass() 方法是双亲委派的实现,而另外提供了一个 protected 的 findClass() 方法,在 loadClass() 方法逻辑里如果父类加载失败,则会调用自己的 findClass() 方法来完成加载
双亲委派模型解决了各个类加载器的基础类的统一问题,但是如果基础类又要调用回用户的代码的话,是做不到的,因为基础类的类加载器只加载它目录下的文件,但是基础类调用到用户的代码的话,基础类的类加载器就无法加载用户目录下的类了
为了解决这个困境,Java 设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoader() 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器就是应用类加载器。
有了线程上下文类加载器,本来基础类(例如rt.jar)加载用户的类时,只能通过自身的启动类加载器完成的;现在便可以主动获取线程上下文类加载器去完成用户的类加载。对于SPI(Service Provider Interface)服务提供 API,便可以采用线程上下文类加载器,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等
OGSi 是当前业界“事实上”的 Java 模块化标准,而 OSGi 实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换
在 OGSi 环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构,当类加载请求时,OSGi 将按照下面的顺序进行搜索:
(1)将以 java.* 开头的类,委派给父类加载器加载
(2)否则,将委派列表名单内的类,委派给父类加载器加载
(3)否则,将 Import 列表中的类,委派给 Export 这个类的 Bundle 的类加载器加载
(4)否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载
(5)否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle 的类加载器加载
(6)否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载
(7)否则,类查找失败