虚拟机类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。在Java语言里面,类型的加载、连接和初始化过程都是在程序运行时期完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java可以动态扩展的特性就是依赖运行期动态加载和动态连接特性实现。
类加载的过程和时机
类的加载过程
类从被加载到虚拟机内存到卸载出内存,主要包含七个步骤如图:
其中,验证、准备、解析3个部分统称为连接,简单介绍下五个核心过程:
- 加载:通过ClassLoder加载class文件字节码,生成Class对象。获取二进制字节流,并将字节流所代表的静态存储结构转换为方法区的运行时数据,在内存中生成Class对象。
- 验证:检查加载的class的正确性和安全性,主要分为文件格式验证、元数据验证、字节码验证、符号引用验证。
- 准备:正式为类变量分配并设置类变量的初始值,这些变量所使用的内存都将在方法区中进行分配。
- 解析:解析是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
- 初始化:执行类变量赋值和静态代码块。
类的加载时机
加载、验证、准备、初始化和卸载这5个阶段顺序是确定的。类的解析在某些情况下可以在初始化阶段之后,这是为了支持Java语言运行时绑定。什么情况下开始加载,Java虚拟机规范中并没有进行强制约束,这点交由虚拟机具体实现自由把握。但对于初始化阶段,虚拟机严格规定了五种情况必须立即进行类的初始化(加载、验证、准备需要在此之前):
- 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,若类没有进行初始化,则需要先触发其初始化。生成这4条指令的场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(即包含main()方法的类),虚拟机会先初始化这个类。
- 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
类加载器
类加载器首先用于类的加载阶段,同时对于任意一个类,判断其唯一性要根据加载它的类加载器和这个类本身一同确立。判断两个类是否相等,只有在这两个类由同一个类加载器加载才有意义,只要加载它们的类加载器不同,这两个类必定不相等。
类加载器的种类
从Java虚拟机来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器是用C++语言来实现的,是虚拟机的一部分;另一种就是所有其他的类加载器,这些都是Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。从Java开发人员来讲,则划分更细致,主要分为4种类加载器:
- 启动类加载器(Bootstrap ClassLoader):这个类加载器主要负责加载
/lib或者被-Xbootclasspath参数指定的路径的类,并且是虚拟机识别的(按文件名识别,如Java核心库rt.jar,名字不符合的类库在lib目录中也不会被加载) - 扩展类加载器(Extension ClassLoader):这个类加载器主要负责加载
/lib/ext目录中的类库或者java.ext.dirs系统变量所指定路径中的所有类库。 - 应用程序类加载器(Application ClassLoader):它负责加载用户路径(ClassPath)上所指定的类库,如果没有自定义类加载器,一般就使用它作为默认的类加载器。
- 自定义类加载器:用户可以自定义类加载器。
首先先简单介绍下如何自定义ClassLoader:
1 | import java.io.*; |
首先继承ClassLoader类,然后覆写findClass方法,从指定目录中读取.class的二进制流,最后通过defineClass方法将二进制字节流定义为一个类并返回。
双亲委派机制
类加载器是互相配合进行加载的,有层次关系,这种层次关系称为类加载器的双亲委派模型,如图:
双亲委派模型中,除了顶层启动类之外,其余类加载器都有自己的父类加载器。这里的父子关系,是通过组合的方式来实现的。结合代码我们来分析双亲委派模型,实现双亲委派的代码都集中在ClassLoader的loadClass()方法之中,如下:
1 | protected Class<?> loadClass(String name, boolean resolve) |
假设我们从自定义的ClassLoader开始进行加载,
- 首先自定义的ClassLoader会先查找是否已经加载该类,已经加载则返回,没有则从调用父类的loadClass()方法。
- 同理其父类AppClassLoader也是先查找自己是否已加载该类,有则返回,没有则有委派给它的父类ExtensionClassLoader。
- ExtensionClassLoader查找自己是否已加载该类,没有则委派给它的parent,因为它的parent为null,则去BootStrapClassLoader中去查找。
- BootStrapClassLoader查找并加载,若还没有,则通过ExtensionClassLoader的findClass方法去加载类。
- ExtensionClassLoader未能加载,则返回到AppClassLoader去尝试加载,若未能则继续到自定义的类加载器加载。
- 最后仍然不能加载则抛出ClassNotFoundException异常。
使用双亲委派机制来组织类加载器之间的关系,使得Java类随着类加载器一起具备了一种带有优先级的层级关系。这样对于高层类加载器加载的类库而言,则能够确保最终都是委派给高层的类加载器加载,这样也就能够确保各种类加载器环境中都是同一个类。
突破双亲委派机制
双亲模型固然有着优点,能够让整个系统保持了类的唯一性。但在有些场合,却不适合,也就是说,顶层的启动类加载器的代码无法访问到底层的类加载器。
如在 Java 平台中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,通常可以称为 Service Provider Interface,即 SPI。在 rt.jar 中的抽象类需要加载继承他们的在应用层的子类实现,rt.jar 无法中代码无法访问到应用类加载器,所以目前的双亲机制是无法实现的。因此 JDK 引用了一个不太优雅的设计,上下文类加载器。也就是讲类加载放在线程上下文变量中。通过 Thread.getContextClassLoader(), Thread.setContextClassLoader(ClassLoader) 这两个方法获取和设置 ClassLoader,这样,rt.jar 中的代码就可以获取到底层的类加载了。
双亲模式是虚拟机的默认行为,但并非必须这么做,通过重载 ClassLoader 可以修改该行为。事实上,很多框架和软件都修改了,比如 Tomcat,OSGI。具体实现则是通过重写 loadClass 方法,改变类的加载次序。比如先使用自定义类加载器加载,如果加载不到,则交给双亲加载。