目录
一、类加载器作用
所谓类加载器,就是将.class二进制字节码文件加载到内存中,并将这写静态数据转换为方法区中的运行时数据结构,同时在堆中生成一个代表该类的Class对象,作为方法区类数据访问的入口。
二、类缓存
标准的JAVA SE类加载器可以按要求查找类,一旦这个类被加载到类加载器中,它会维持缓存一段时间,但是JVM垃圾回收器可以回收这些Class类对象。
三、类加载器的分类
如下图,类加载器的层次结构图:
注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。四者之间是包含关系,不是上层和下层,也不是子系统的继承关系。
- 启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部;
- 用来加载Java的核心库(JAVAHOME/jre/1ib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类;
- 并不继承自ava.lang.ClassLoader,没有父加载器;
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器;
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类;
-
扩展类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现;
- 派生于ClassLoader类;
- 父类加载器为启动类加载器;
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载;
- 应用程序类加载器(系统类加载器,AppClassLoader)
- Java语言编写,由sun.misc.LaunchersAppClassLoader实现;
- 派生于ClassLoader类;
- 父类加载器为扩展类加载器;
- 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库;
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载;
- 通过classLoader#getSystemclassLoader()方法可以获取到该类加载器;
- 用户自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。 为什么要自定义类加载器?
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄漏
用户自定义类加载器实现步骤:
- 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求;
- 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadclass()方法,而是建议把自定义的类加载逻辑写在findclass()方法中;
- 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URIClassLoader类,这样就可以避免自己去编写findclass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁;
前面介绍了Java虚拟机提供的四种类加载器,我们通过一个示例说明,如何获取不同类型的加载器,如下:
public class ClassLoaderTest {
public static void main(String[] args) {
// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
// 获取其上层的:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);
// 试图获取 根加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);
// 获取自定义加载器
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);
// 获取String类型的加载器
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);
}
}
运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
null
sun.misc.Launcher$AppClassLoader@18b4aac2
null
从结果可以看出,根加载器无法直接通过代码获取,同时目前用户代码所使用的加载器为系统类加载器。同时我们通过获取String类型的加载器,发现是null,那么说明String类型是通过根加载器进行加载的,也就是说Java的核心类库都是使用根加载器进行加载的。
四、关于ClassLoader类
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)。ClassLoader类加载器的作用是根据一个类的全限定名来找到或者生成对应的.class二进制字节码文件,然后生成该类对应的java.lang.Class类对象,当然,除了加载字节码文件,ClassLoader还负责加载一些图像文件和资源文件等。
- ClassLoader类声明
- ClassLoader类常见的一些方法
- ClassLoader类的继承图
- 获取ClassLoader的途径
- 获取当前ClassLoader:clazz.getClassLoader();
- 获取当前线程上下文的ClassLoader:Thread.currentThread().getContextClassLoader();
- 获取系统的ClassLoader:ClassLoader.getSystemClassLoader();
- 获取调用者的ClassLoader:DriverManager.getCallerClassLoader();
五、双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
就是某个类加载器接收到加载类的请求的时候,先把加载请求交给其父类加载器加载,依次追溯,知道爷爷辈类加载器,如果父类加载器可以加载那么成功加载该类,如果父类加载器不能完成加载任务,那么自己才会去加载。
工作原理:
- 1、当ApplicationClassLoader(系统默认的类加载器)加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtensionClassLoader去执行。
- 2、当ExtensionClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去执行。
- 3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtensionClassLoader来尝试加载;
- 4、若ExtensionClassLoader也加载失败,则会使用ApplicationClassLoader来加载,如果ApplicationClassLoader也加载失败,则会报出异常ClassNotFoundException。
如下图:
双亲委托机制保证了Java核心类库的类型安全。假设我们自己定义了java.lang.String这个类:
package java.lang;
public class String {
public String toString() {
return "value";
}
}
这时候,其实这个String类时无法使用的,这是因为双亲委托机制,加载java.lang.String这个类的时候,根据双亲委派机制,最终到Bootstrap ClassLoader引导类加载器的时候,因为引导类加载器主要负责加载Java核心类库,它发现里面有java.lang.String这个类,所以它并不会去加载我们自定义的java.lang.String这个类,而是加载的核心类库中java.lang.String类,这就保证了Java核心类库的安全。
双亲委托机制是代理模式的一种,并不是所有的类加载器都使用双亲委托机制,例如tomcat服务器类加载也是使用代码模式,但是它首次加载的时候首先自己尝试去加载,如果自己不能加载才会去找父类加载器加载,这与一般的双亲委托机制顺序是相反的。
双亲委派机制的优势:
通过上面的例子,我们可以知道,双亲机制具有下面的优势:
- 避免类的重复加载;
- 保护程序安全,防止核心API被随意篡改;
- 自定义类:java.lang.String
- 自定义类:java.lang.Start(报错:阻止创建 java.lang开头的类)
沙箱安全机制:
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),这样可以保证对Java核心源代码的保护,这就是沙箱安全机制。
六、类加载方式
在Java中类加载的方式主要有三种:
- 命令行启动由JVM初始化加载;
- 通过Class.forName()反射API方法加载;
- 通过ClassLoader.loadClass(String name)方法进行加载;
下面通过一个示例说明一下三种方法加载类:
package com.wsh.jvm.classloader03;
public class TestClassLoad {
static {
System.out.println("init static block");
}
public static void main(String[] args) {
System.out.println("TestClassLoad");
}
}
【a】命令行加载
【b】Class.forName()与ClassLoader.loadClass(String name):
public class TestClassLoaderMethod {
public static void main(String[] args) {
try {
// Class<?> clazz = Class.forName("com.wsh.jvm.classloader03.TestClassLoad");
//使用Class.forName()方法会执行静态代码块进行初始化
//init static block
//如果指定initialize = false的话则不会进行执行静态代码块初始化
Class.forName("com.wsh.jvm.classloader03.TestClassLoad", false, ClassLoader.getSystemClassLoader());
ClassLoader classLoader = TestClassLoaderMethod.class.getClassLoader();
// 使用ClassLoader.loadClass()方法默认不会执行静态代码块进行初始化
Class<?> clazz2 = classLoader.loadClass("com.wsh.jvm.classloader03.TestClassLoad");
//执行了newInstance()方法之后会执行静态代码块
//init static block
clazz2.newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}
Class.forName()和ClassLoader.loadClass()、Class.forName(String name, boolean initialize, ClassLoader loader)区别:
- Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,默认会执行类中的static态代码块进行初始化操作;
- ClassLoader.loadClass():只是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才执行static静态代码块进行初始化操作;
- Class.forName(String name, boolean initialize, ClassLoader loader):可通过控制initialize = true/false的值来决定是否进行初始化。
七、如何判断两个class对象是否相同?
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
- 类的完整类名必须一致,包括包名;
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同;
换句话说,在JVM中,即使这两个类对象(class对象)来源于同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
八、类的主动使用和被动使用
Java程序对类的使用方式分为:主动使用和被动使用。 主动使用,又分为七种情况:
- 创建类的实例;
- 访问某个类或接口的静态变量,或者对该静态变量赋值;
- 调用类的静态方法;
- 反射(比如:Class.forName("com.wsh.Test"));
- 初始化一个类的子类;
- Java虚拟机启动时被标明为启动类的类;
- JDK7开始提供的动态语言支持;
- java.lang.invoke.MethodHandle实例的解析结果REF getStatic、REF putStatic、REF invokeStatic句柄对应的类没有初始化,则初始化;
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
九、总结
本文主要总结了类加载器的作用、分类、层次结构以及类加载过程中的双亲委托机制,文中如有不对之处,希望大家指正,希望对大家的学习有所帮助。