类加载器简介
Kotlin,Scala等语言也是运行在JAVA虚拟机的语言,那为什么这些不是java的语言也能够运行在jvm虚拟机上呢?原因就是Class 文件,对虚拟机来说Class文件是一个重要的接口,无论使用何种语言进行软件开发,只要能将源文件编译为正确的 Class 文件,那么这种语言就可以在 Java 虚拟机上运行.可以说,Class 文件就是虚拟机的基石,而加载Class文件又是通过类加载器进行的.
类加载器的工作流程
Class 文件通常是以文件的方式存在(任何二进制流都可以是 Class 类型),但只有能被 JVM 加载后才能被使用,才能运行编译后的代码。系统载入 Class 过程可以分为加载,链接和初始化三个步骤。其中,链接也可分为验证,准备和解析3步骤,其中,只有加载过程是程序员能够控制的,后面的几个步骤都是有虚拟机自动运行的。因此,我们的关注点主要放在加载阶段
类加载
JVM第一次使用到这个类时需要对,这个类的信息进行加载。一个类只会加载一次,之后这个类的信息放在堆空间,静态属性放在方法区.
什么时候加载类
- 当创建一个类的实例是,比如使用 new 关键字,或者通过反射,克隆,反序列化。
- 当调用类的静态方法时,即当使用字节码 invokstatic 指令。
- 当使用类或接口的静态字段时(final 常量除外),比如,使用 getstatic 或者 pustatic 指令。
- 当时用 Java.lang.reflect 包中的方法反射类的方法时。
- 当初始化子类,要求先初始化父类。
- 作为启动虚拟机,含有 main()方法的那个类。
类加载器的分类
类加载器主要分为以下几类,除了启动类加载器外,其余扩展器都实现了ClassLoader类
启动类加载器(BootStrap ClassLoader),C++ 语言实现,虚拟机自身的一部分
启动类加载器主要加载${JAVA_HOME}/lib 目录中的包,负责加载JDK中的核心类库,如 rt.jar,我们是无法访问这个类加载器的如下代码
1
System.out.println(String.class.getClassLoader());
控制台中打印的是null,因为String为jdk的核心类由BootStrap ClassLoader加载,我们无法访问这个类加载器.
扩展类加载器
扩展类加载器有 sun.misc.Launcher$ExtClassLoader 实现,负责加载
/lib/ext 目录中的。或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。 应用类加载器
sun.misc.Launcher$AppClassLoader 实现,由于这个类是 ClassLoader 中的 getSystemClassLoader 方法的返回值,也称为系统类加载器,负载加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器。一般情况下,这个就是程序中默认的类加载器。
自定义类加载器
自定义类加载器用于加载一些特殊途径的类,一般也是用户程序类。
类加载器的双亲委派模式
JVM为了保证同一个Class只被加载一次采用了双亲委派模式进行控制,因为同一个Class文件如果被不同类加载器加载那么他们就不是相同的一个类(如同一个类由应用类加载器加载,之后又由自定义类加载器加载,这两个类不是同一个类,即使他们的包名编写的代码时相同的,我们可以简单根据此简单的实现热部署,我们后续会介绍),那么什么是双亲委派模式呢?类加载器的层级关系是启动类加载器>扩展类加载器>应用类加载器>自定义加载器,现在我们在程序中new了一个对象,那么它首先会调用findLoadedClass(name)方法去底层查找这个类时候已经加载(底层堆中),如果有就返回,如果没有就委托父类进行加载,如果父类可以加载则加载不行则退回到子类加载.比如我们自定义了一个A对象,在我们第一次调用new A()实例化对象时,它首先会去启动类加载器中寻找,因为启动类加载器只会加载${JAVA_HOME}/lib目录下的类所以无法加载A这个类,接下来由扩展类加载器(ExtClassLoader)进行加载,由于 其只会加载${JAVA_HOME}/lib/ext 目录下的类所以也不会进行加载,最后退回到应用类加载器(AppClassLoader)加载器,由于其加载用户类路径(ClassPath)上所指定的类库,而我们新建的对象在工程目录下,所以由此加载器完成加载
1 | protected Class<?> loadClass(String name, boolean resolve) |
loadClass()、findClass()、defineClass()区别
loadClass
源码如下其会先调用findLoadedClass去查找是否以及加载了class文件如果没有的话再委托双亲加载
1 | protected Class<?> loadClass(String name, boolean resolve) |
findClass()
这个方法为子类扩展方法,在ClassLoader中并没有对其进行具体的实现,预留出这个接口的我个人理解是,如果你不想突破双亲委派则只需要实现该方法即可,我们可以在loadClass方法中发现,其调用findClass了,如果我们想要使用默认的双亲委派功能去重新编写loadClass代码的话会产生大量冗余代码,所以开放出此接口来实现用户自定义的class加载方式,其通常会在最后调用defineClass去加载class文件,这个方法是父类中已经实现好的加载class文件的方法,至于为什么不突破双亲委派?举个例子:前文提到双亲委派模式加载类的先后顺序是,启动类加载器>扩展类加载器>应用类加载器>自定义加载器,只有当父类加载不了这个类才由子类去实现,比如启动类加载器加载${JAVA_HOME}/lib目录下的类,如果采用双亲模式则这些类就无法通过扩展类加载器去加载,同理,我现在自定义了一个ClassLoader,如果我去实例化这个类的话,那么它会在应用类加载器(AppClassLoader)中去加载这个class文件,因为我编写的java文件路径是在工程目录下的,应用类加载器(AppClassLoader)有权去加载工程目录下的文件,那么我在调用以下代码时得到的是true,因为我调用new 方法去实例对象是使用应用类加载器(AppClassLoader)去构造对象的,而我通过自定义类加载器加载的(classLoader.loadClass(“com.liu.A”))class对象由于使用了双亲加载模式,其委托给了AppClassLoader去加载对象,这两个类都是使用同一个类加载器去生成对象,所以输出true,反之如果我重写loadClass方法,绕过双亲加载模式那么输出的就是false,,因为class虽然是同一个,但是我使用了不同的类加载器去加载class文件,java认定这两个对象不是相同的对象
1 | Class aClass = classLoader.loadClass("com.liu.A"); |
1 | protected Class<?> findClass(String name) throws ClassNotFoundException { |
definclass()
把字节码转化为Class
我们现在编写代码来测试
1 | public class Test { |
控制台打印出的代码为
1 | sun.misc.Launcher$AppClassLoader@18b4aac2 |
注意
- 同一个类不能加载两次一个class文件(多次调用load方法),不然会报 attempted duplicate class definition for name:,所以如果用类加载器实现热部署需要每次都要创建一个新的类加载器,在热部署章节中会体现