类加载器
ClassLoader的作用
ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。因此,ClassLoader在整个加载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行,则由Execution Engine决定。
class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式(在日常开发会混合使用)
- 显式加载:指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name) 或this.getClass().getClassLoader().loadClass() 加载class对象。
- 隐式加载:通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中,比如 new User()。
类加载器的分类
JVM支持两种类型的类加载器,分别为启动(引导)类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范并没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
启动(引导)类加载器 Bootstrap
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部;
- 它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sum.boot.class.path路径下的内容),用于提供JVM自身需要的类(String类就是使用的这个类加载器);
- 由于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类;
- 并不继承自java.lang.ClassLoader,没有父加载器;
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
扩展类加载器 Extension
- Java语言编写,由sum.music.Launcher$ExtClassLoader实现;
- 派生于ClassLoader类,”父类“加载器为启动类加载器(null);
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
应用程序(系统)类加载器 AppClassLoader
- java语言编写,由sum.misc.Launcher$AppClassLoader实现;
- 派生于ClassLoader类,“父类”加载器为ExtClassLoader扩展类加载器;
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库;
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载;
- 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器。
用户自定义类加载器
- 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式(自定义类加载器通常需要继承于 ClassLoader);
- 自定义类加载器可以实现类库的动态加载。加载源可以是本地的JAR包,也可以是网络上的远程资源;
- 自定义 ClassLoader 的子类时候,有两种做法:
- 重写 loadClass() 方法----->不推荐,这个方法会保证类的双亲委派机制
- 重写 findClass() 方法 ----->推荐
- 这两种方法本质上差不多,毕竟loadClass()也会调用findClass(),但是从逻辑上讲最好不要直接修改loadClass()的内部逻辑。建议的做法是只在findClass()里重写自定义类的加载方法,根据参数指定类的名字,返回对应的Class对象的引用。
不同的类加载器
- 每个Class对象都会包含一个定义它的ClassLoader的一个引用
- 获取ClassLoader的途径
//获得当前类的ClassLoader
clazz.getClassLoader();
//获得当前线程上下文的ClassLoader(系统类加载器)
Thread.currentThread().getContextClassLoader();
//获得系统的ClassLoader
ClassLoader.getSystemClassLoader();
- 站在程序的角度看,启动类加载器与另外两种类加载器(系统类加载器和扩展类加载器)并不是同一个层次意义上的加载器,启动类加载器是使用C++语言编写而成的,而另外两种类加载器则是使用Java语言编写而成的。由于启动类加载器不是一个Java类,因此在Java程序中只能打印出空值(null)。
- 数组类的Class对象,不是由类加载器去加载的,而是在Java运行期JVM根据需要自动创建的。对于数组的类加载器来说,是通过Class.getClassLoader()返回的,与数组中元素类型的类加载器是一样的。如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的(基本数据类型由虚拟机预先定义)
public static void main(String[] args) {
ClassLoader classloader1 = ClassLoader.getSystemClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classloader1);
//获取到扩展类加载器
//sun.misc.Launcher$ExtClassLoader@1540e19d
System.out.println(classloader1.getParent());
//获取到引导类加载器 null
System.out.println(classloader1.getParent().getParent());
//获取系统的ClassLoader
ClassLoader classloader2 = Thread.currentThread().getContextClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classloader2);
String[] strArr = new String[10];
ClassLoader classLoader3 = strArr.getClass().getClassLoader();
//null,表示使用的是引导类加载器
System.out.println(classLoader3);
basicTest[] refArr = new basicTest[10];
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(refArr.getClass().getClassLoader());
int[] intArr = new int[10];
//null,如果数组的元素类型是基本数据类型,数组类是没有类加载器的
System.out.println(intArr.getClass().getClassLoader());
}
ClassLoader主要方法
-
public Class<?> loadClass(String name) throws ClassNotFoundException
加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,则返回ClassNot FoundException 异常。该方法中的逻辑就是双亲委派模式的实现。
-
protected Class<?> findClass (String name) throws ClassNotFoundException
查找二进制名称为name的类,返回结果为java.lang.Class类的实例。这是一个受保护的方法JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委派机制,该方法会在检查完父类加载器之后被loadClass()方法调用。
Class.forName() 与 ClassLoader.loadClass() 对比
-
Class.forName():是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个 Class 对象。该方法在将 Class 文件加载到内存的同时,会执行类的初始化。
-
ClassLoader.loadClass():这是一个实例方法,需要一个 ClassLoader 对象来调用该方法。该方法将 Class 文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到一个 ClassLoader 对象,所以可以根据需要指定使用哪个类加载器。