1.概述
类加载器是JVM执行类加载机制的前提。
ClassLoader的作用:
ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行,则由Execution Engine决定。
1.1 类的加载分类(显式加载VS隐式加载)
举例:
public class UserTest {
public static void main(String[] args) {
try {
// 隐式加载
User user = new User();
// 显式加载,并初始化
Class clazz = Class.forName("com.test.java.User");
// 显式加载,但不初始化
ClassLoader.getSystemClassLoader().loadClass("com.test.java.Parent");
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.2 类加载器的必要性
1.3 命名空间
// 自定义类加载器
public class UserClassLoader extends ClassLoader {
private String rootDir;
public UserClassLoader(String rootDir) {
this.rootDir = rootDir;
}
/**
* 编写findClass方法的逻辑
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的class文件字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//直接生成class对象
return defineClass(name, classData, 0, classData.length);
}
}
/**
* 编写获取class文件并转换为字节码流的逻辑 * @param className * @return
*/
private byte[] getClassData(String className) {
// 读取类文件的字节
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
// 读取类文件的字节码
while ((len = ins.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 类文件的完全路径
*/
private String classNameToPath(String className) {
return rootDir + "/" + className.replace('.', '/') + ".class";
}
// 测试
public static void main(String[] args) {
String rootDir = "/Users/zhangshouan/WorkSpace/spring-kernel/jvm/src";
try {
// 创建自定义的类的加载器1
UserClassLoader loader1 = new UserClassLoader(rootDir);
Class clazz1 = loader1.findClass("com.jvm.lecture.test.User");
// 创建自定义的类的加载器2
UserClassLoader loader2 = new UserClassLoader(rootDir);
Class clazz2 = loader2.findClass("com.jvm.lecture.test.User");
// clazz1与clazz2对应了不同的类模板结构
System.out.println(clazz1 == clazz2); // false clazz1与clazz2对应不同的类模版结构
System.out.println(clazz1.getClassLoader()); // com.jvm.lecture.test.UserClassLoader@28d93b30
System.out.println(clazz2.getClassLoader()); // com.jvm.lecture.test.UserClassLoader@4554617c
Class clazz3 = ClassLoader.getSystemClassLoader().loadClass("com.jvm.lecture.test.User");
System.out.println(clazz3.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(clazz1.getClassLoader().getParent()); // sun.misc.Launcher$AppClassLoader@18b4aac2
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
1.4 类加载机制的基本特征
2.类的加载器分类
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:
- 除了顶层的启动类加载器外,其余的类加载器都应当有自己的“父类”加戟器。
- 不同类加载器看似是继承(Inheritance)关系,实际上是包含关系。在下层加载器中,包含着上层加载器的引用。
class ClassLoader{
ClassLoader parent;//父类加载器
public ClassLoader(ClassLoader parent){
this.parent = parent;
}
}
class ParentClassLoader extends ClassLoader{
public ParentClassLoader(ClassLoader parent){
super(parent);
}
}
class ChildClassLoader extends ClassLoader{
public ChildClassLoader(ClassLoader parent){ //parent = new ParentClassLoader();
super(parent);
}
}
2.1 引导类加载器(Bootstrap ClassLoader)
启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
- 它用来加载Java的核心库(JAVAHOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供JVM自身需要的类。
- 并不继承自java.lang.ClassLoader,没有父加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
- 使用-XX:+TraceClassLoading参数得到。
启动类加载器使用C++编写的?Yes!
- C/C++:指针函数&函数指针、C++支持多继承、更加高效
- Java:由C++演变而来,(C++)-- 版本,单继承
2.2 扩展类加载器(Extension ClassLoader)
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("**********启动类加载器**************");
// 获取BootstrapClassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
// 从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader); // null 再次证明我们无法获取到启动类加载器
System.out.println("***********扩展类加载器*************");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
// 从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d
}
}
**********启动类加载器**************
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/classes
null
***********扩展类加载器*************
/Users/zhangshouan/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
sun.misc.Launcher$ExtClassLoader@1540e19d
2.3 系统类加载器(AppClassLoader)
public class ClassLoaderTest {
public static void main(String[] args) {
// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 获取其上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@28d93b30
// 获取其上层:获取不到引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader); // null
// 对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// String类使用引导类加载器进行加载的。--> Java的核心类库都是使用引导类加载器进行加载的。
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1); // null
}
}
2.4 用户自定义类加载器
测试不同的类加载器
public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
// 1.Class.forName().getClassLoader()
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader); // null String 类由启动类加载器加载,我们无法获取
// 2.Thread.currentThread().getContextClassLoader()
// 获取当前线程上下文的ClassLoader
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1); //sun.misc.Launcher$AppClassLoader@18b4aac2
// 3.ClassLoader.getSystemClassLoader().getParent()
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2); //sun.misc.Launcher$ExtClassLoader@61bbe9ba
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
// 获得当前类的ClassLoader
clazz.getClassLoader()
// 获得当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
// 获得系统的ClassLoader
ClassLoader.getSystemClassLoader()
说明
站在程序的角度看,引导类加载器与另外两种类加载器(系统类加载器和扩展类加载器)并不是同一个层次意义上的加载器,引导类加载器是使用C++语言编写而成的,而另外两种类加载器则是使用Java语言编写而成的。由于引导类加载器压根儿就不是一个Java类,因此在Java程序中只能打印出空值。
数组类的Class对象,不是由类加载器去创建的,而是在Java运行期JVM根据需要自动创建的。对于数组类的类加载器来说,是通过Class.getClassLoader()返回的,与数组当中元素类型的类加载器是一样的;如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的。
// 运行结果:null
String[] strArr = new String[6];
System.out.println(strArr.getClass().getClassLoader());
// 运行结果:sun.misc.Launcher$AppCLassLoader@18b4aac2
// 与数组元素的类的加载器相同
ClassLoaderTest[] test=new ClassLoaderTest[1];
System.out.println(test.getClass().getClassLoader());
// 运行结果:null 不需要类加载器
int[]ints =new int[2];
System.out.println(ints.getClass().getClassLoader());
3.ClassLoader源码解析
ClassLoader与现有类的关系:
除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。
3.1 ExtClassLoader和AppClassLoader源码分析
Launcher
首先创建扩展类加载器
父类为NULL(上层类加载器)
扩展类加载器实例传入到系统类加载器中
系统类加载器的父类是扩展类加载器
获取当前线程上下文的ClassLoader是AppClassLoader
3.2 ClassLoader的主要方法
简单举例
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的字节数组
byte[] classData =getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else{
//使用defineClass生成class对象
return defineClass(name,classData,θ,classData.length);
}
}
1. public final ClassLoader getParent()
获取前边设置的parent
2. public Class<?> loadClass(String name) throws ClassNotFoundException
loadClass()的剖析(双亲委派机制的逻辑)
测试代码:ClassLoader.getSystemClassLoader().loadClass(“cn.jvm.java.User”);
// resolve:true 的时候,加载class的时候同时进行解析操作
// resolve:false 不需要解析
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 同步操作,只能加载一次
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先,在缓存中判断是否已经加载同名的类
Class<?> c = findLoadedClass(name);
// 如果没有
if (c == null) {
// 获取当前的系统时间
long t0 = System.nanoTime();
try {
// 获取当前类加载器的父类加载器
if (parent != null) {
// 如果存在父类加载器,则调用父类加载器进行类的加载
c = parent.loadClass(name, false);
} else {
// parent 为 null 父类加载器为引导类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 当前类的加载器的父类加载器未加载此类 or 当前类加载器未加载此类
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 调用当前ClassLoader的findClass() 方法
// findClass()--》 defineClass()
c = findClass(name); // 真正加载的方法
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
// 是否进行解析操作
if (resolve) {
resolveClass(c);
}
return c;
}
}
3.3 SecureClassLoader与URLClassLoader
3.4 ExtClassLoader与AppClassLoader
我们发现ExtClassLoader并没有重写loadClass()方法,这足矣说明其遵循双亲委派模式,而AppClassLoader重载了loadClass()方法,但最终调用的还是父类loadClass()方法,因此依然遵守双亲委派模式。
3.5 Class.forName()与ClassLoader.loadClass()
4.双亲委派模型
类加载器用来把类加载到Java虚拟机中。从JDK1.2版本开始,类的加载过程采用双亲委派机制,这种机制能更好地保证Java平台的安全。
4.1 定义与本质
定义
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。
本质
规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。
4.2 优势与劣势
4.3 破坏双亲委派机制
双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。
在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,直到Java模块化出现为止,双亲委派模型主要出现过3次较大规模“被破坏”的情况。
第一次破坏双亲委派机制
第二次破坏双亲委派机制:线程上下文类加载器
默认上下文加载器就是应用类加载器,这样以上下文加载器为中介,使得启动类加载器中的代码也可以访问应用类加载器中的类。
第三次破坏双亲委派机制
4.4 热替换的实现
5.沙箱安全机制
JDK1.0时期
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱(Sandbox)机制。如下图所示JDK1.0安全模型
JDK1.1时期
JDK1.0中如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。
因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限。
如下图所示JDK1.1安全模型
JDK1.2时期
在Java1.2版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示JDK1.2安全模型:
JDK1.6时期
6.自定义类的加载器
注意
在一般情况下,使用不同的类加载器去加载不同的功能模块,会提高应用程序的安全性。但是,如果涉及Java类型转换,则加载器反而容易产生不美好的事情。在做Java类型转换时,只有两个类型都是由同一个加载器所加载,才能进行类型转换,否则转换时会发生异常。
6.1 实现方式
说明
其父类加载器是系统类加载器。
JVM中的所有类加载都会使用java.lang.ClassLoader.loadClass(String)接口(自定义类加载器并重写java.lang.ClassLoader.loadClass(String)接口的除外),连JDK的核心类库也不能例外。
// 自定义类加载器
public class UserClassLoader extends ClassLoader {
private String rootDir;
public UserClassLoader(String rootDir) {
this.rootDir = rootDir;
}
/**
* 编写findClass方法的逻辑
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的class文件字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//直接生成class对象
return defineClass(name, classData, 0, classData.length);
}
}
/**
* 编写获取class文件并转换为字节码流的逻辑 * @param className * @return
*/
private byte[] getClassData(String className) {
// 读取类文件的字节
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
// 读取类文件的字节码
while ((len = ins.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 类文件的完全路径
*/
private String classNameToPath(String className) {
return rootDir + "/" + className.replace('.', '/') + ".class";
}
// 测试
public static void main(String[] args) {
String rootDir = "/Users/zhangshouan/WorkSpace/spring-kernel/jvm/src";
try {
// 创建自定义的类的加载器1
UserClassLoader loader1 = new UserClassLoader(rootDir);
Class clazz1 = loader1.findClass("com.jvm.lecture.test.User");
// 创建自定义的类的加载器2
UserClassLoader loader2 = new UserClassLoader(rootDir);
Class clazz2 = loader2.findClass("com.jvm.lecture.test.User");
// clazz1与clazz2对应了不同的类模板结构
System.out.println(clazz1 == clazz2); // false clazz1与clazz2对应不同的类模版结构
System.out.println(clazz1.getClassLoader()); // com.jvm.lecture.test.UserClassLoader@28d93b30
System.out.println(clazz2.getClassLoader()); // com.jvm.lecture.test.UserClassLoader@4554617c
Class clazz3 = ClassLoader.getSystemClassLoader().loadClass("com.jvm.lecture.test.User");
System.out.println(clazz3.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(clazz1.getClassLoader().getParent()); // sun.misc.Launcher$AppClassLoader@18b4aac2
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
7.Java9新特性
2.平台类加载器和应用程序类加载器都不再继承自java.net.URLClassLoader。
现在启动类加载器、平台类加载器、应用程序类加载器全都继承于jdk.internal.loader.BuiltinClassLoader。
如果有程序直接依赖了这种继承关系,或者依赖了URLClassLoader类的特定方法,那代码很可能会在JDK9及更高版本的JDK中崩溃。
3.在Java9中,类加载器有了名称。该名称在构造方法中指定,可以通过getName()方法来获取。平台类加载器的名称是platform,应用类加载器的名称是app。类加载器的名称在调试与类加载器相关的问题时会非常有用。
4.启动类加载器现在是在jvm内部和java类库共同协作实现的类加载器(以前是C++实现),但为了与之前代码兼容,在获取启动类加载器的场景中仍然会返回null,而不会得到BootClassLoader实例。
5.类加载的委派关系也发生了变动。当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(ClassLoaderTest.class.getClassLoader());
System.out.println(ClassLoaderTest.class.getClassLoader().getParent());
System.out.println(ClassLoaderTest.class.getClassLoader().getParent().getParent());
//获取系统类加载器
System.out.println(ClassLoader.getSystemClassLoader());
//获取平台类加载器
System.out.println(ClassLoader.getPlatformClassLoader());
//获取类的加载器的名称
System.out.println(ClassLoaderTest.class.getClassLoader().getName());
}
}