类加载器
一、类加载器概述
1、类加载器的作用
类加载器:负责将.class文件加载到内存中,并为之生成对应的Class对象(是JVM执行类加载机制的前提)
- ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的。
- ClassLoader是否可以运行,则由Execution Engine决定。
- ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例,然后交给Java虚拟机进行链接、初始化等操作。
因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。

2、类加载的分类
类的加载分类:显式加载 vs 隐式加载(即JVM加载class文件到内存的方式)
-
显示加载:在代码中显示调用ClassLoader加载class对象
如直接使用 Class.forName(name) 或 this.getClass().getClassLoader().loadClass() 加载class对象。
-
隐式加载:通过虚拟机自动加载到内存中,是不直接在代码中调用ClassLoader的方法加载class对象。
如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。
在日常开发以上两种方式一般会混合使用。
// 隐式加载
User user = new User();
// 显式加载,并初始化
Class clazz = Class.forName("com.test.java.User");
// 显式加载,但不初始化
ClassLoader.getSystemClassLoader().loadClass("com.test.java.Parent");
3、类加载器的必要性
一般情况下,Java开发人员并不需要在程序中显式地使用类加载器,但是了解类加载器的加载机制却显得至关重要。从以下几个方面说:
-
避免在开发中遇到 java.lang.ClassNotFoundException异常 或 java.lang.NoClassDefFoundError 异常时,手足无措。
只有了解类加载器的加载机制,才能够在出现异常的时候 快速地根据错误异常日志定位问题和解决问题。
-
需要支持类的动态加载 或 需要对编译后的字节码文件进行加解密操作时,就需要与类加载器打交道了。
-
开发人员可以 在程序中编写自定义类加载器 来重新定义类的加载规则,以便实现一些自定义的处理逻辑。
4、命名空间
每个类加载器都有自己的命名空间,命名空间 由 该类加载器加载的类
及其 父类加载器加载的类
组成
- 父类加载器 加载的类 对 子类加载器 可见,但是反过来 子类加载器 加载的类 对 父类加载器 不可见。
- 在同一命名空间中,不会出现全限定名相同的两个类。
- 在不同的命名空间中,有可能会全限定名相同的两个类。
举例:
public class CustomClassLoader extends ClassLoader {
// 省略自定义类加载器实现...见第七章的代码举例
public static void main(String[] args) {
try {
// 创建自定义的类加载器1
CustomClassLoader loader1 = new CustomClassLoader();
Class<?> clazz1 = loader1.findClass("com.test.java.User");
// 创建自定义的类加载器2
CustomClassLoader loader2 = new CustomClassLoader();
Class<?> clazz2 = loader2.findClass("com.test.java.User");
// clazz1和clazz2对应了不同的类模版结构
System.out.println(clazz1 == clazz2); // false
System.out.println(clazz1.getClassLoader()); // com.test.java.CustomClassLoader@4aa298b7
System.out.println(clazz2.getClassLoader()); // com.test.java.CustomClassLoader@28d93b30
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。
5、类的唯一性
对于任意一个类,都需要由「加载它的类加载器」和「这个类本身」共同确认其在 J a v a 虚拟机中的唯一性。 \color{red}{对于任意一个类,都需要由「加载它的类加载器」和「这个类本身」共同确认其在Java虚拟机中的唯一性。} 对于任意一个类,都需要由「加载它的类加载器」和「这个类本身」共同确认其在Java虚拟机中的唯一性。
即使两个类源自同一个Class文件,被同一个JVM加载,只要加载他们的类加载器不同,那这两个类就必定不相等。
因此,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。
二、类加载器的分类
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(C z ClassLoader)。
有些人可能会有疑问,扩展类加载器 和 应用程序类加载器 是由JDK提供的现成的类加载器,怎么也属于自定义类加载器呢?
- 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的类加载器。
- 而Java虚拟机规范中,将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:
1、启动类加载器/引导类加载器(Bootstrap ClassLoader)
- C/C++语言实现,嵌套在 JVM 内部。
- 并不派生于
java.lang.ClassLoader
。- 没有父加载器。
- 加载
扩展类加载器
和应用程序类加载器
,并指定为他们的父类加载器。
- 用来加载 Java 的核心类库
- 加载
<JAVA_HOME>/jre/lib
、resources.jar
或sun.boot.class.path
路径下的内容, - 用于提供 JVM 自身需要的类(
Object
、String
、ClassLoader
、Exception
)
- 加载
- 出于安全考虑,
引导类加载器
只加载包名为 java、javax、sun 等开头的类
下面我们从代码层面看一下:
public class BootstrapClassLoader {
public static void main(String[] args) {
// 引导类加载器加载的类库
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
System.out.println(urL);
}
// 从上边的路径中随意选择一个类,查看其类加载器
ClassLoader classLoader1 = Object.class.getClassLoader();
System.out.println(classLoader1); // null
// 这里的null并不是说明不需要类加载器,而是因为引导类加载器是用C/C++语言实现的
}
}
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/lib/cat.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/classes
在 VM options
中 加上 -XX:+TraceClassLoading
,就能打印类的加载信息,下面截取其中部分结果展示。
可以看到,启动类加载器
会加载 Object、String、ClassLoader、Exception 等核心类。
还可以看到,启动类加载器
还会加载 扩展类加载器
和 应用程序类加载器
相关的类
2、扩展类加载器(Extension ClassLoader)
- Java 语言编写,由
sun.misc.Launcher$ExtClassLoader
实现。 - 派生于
java.lang.ClassLoader
- 父类加载器为
引导类加载器
。
- 父类加载器为
- 用来加载 Java的扩展类库
- 加载
<JAVA_HOME>/lib/ext
目录中的 JAR 文件和类
- 加载
import org.openjsse.sun.security.util.CurveDB;
public class ExtensionClassLoader {
public static void main(String[] args) {
// 扩展类加载器加载的类库
String extDirs = System.getProperty("java.ext.dirs");
for (String extPath : extDirs.split(":")) {
System.out.println(extPath);
}
// 从上边的路径中随意选择一个类,查看其类加载器
ClassLoader classLoader2 = CurveDB.class.getClassLoader();
System.out.println(classLoader2); // sun.misc.Launcher$ExtClassLoader@232204a1
}
}
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext
/Library/Java/Extensions
3、系统类加载器/应用程序类加载器(Application ClassLoader)
- Java 语言编写,由
sun.misc.Launcher$AppClassLoader
实现 - 派生于
java.lang.ClassLoader
- 父类加载器为
扩展类加载器
。 - 是
用户自定义类加载器
的默认父类加载器。 - 是 线程上下文的加载器。
- 父类加载器为
- 加载 应用程序中的类和资源:
- 自己编写的类、第三方库提供的类、一些其他的资源文件
- 该类加载是程序中默认的类加载器,一般来说,Java 应用的类都是由它来完成加载
public class ApplicationClassLoader {
public static void main(String[] args) {
// 获取应用程序类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 对于自定义的类,查看其类加载器
ClassLoader classLoader3 = CustomClass.class.getClassLoader();
System.out.println(classLoader3); // sun.misc.Launcher$AppClassLoader@18b4aac2
}
}
4、用户自定义类加载器
日常开发中,类的加载几乎是由上述3种类加载器相互配合执行的。必要时,可以自定义类加载器,来定制类的加载方式。
具体内容后续详细展开,这里先了解:
- 可以通过继承
ClassLoader
并重写loadClass()
或findclass()
方法来自定义类加载器。 用户自定义类加载器
的父类加载器 默认是系统类加载器/应用程序类加载器
。
三、ClassLoader 类
ClassLoader 类是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括引导类加载器)

1、获取三种类加载器
public class GetClassLoader {
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@232204a1
// 引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader); // null
}
}
2、不同类的类加载器
public class PrintClassLoader {
public static void main(String[] args) {
// 线程上下文的加载器(应用程序类加载器)
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println(contextClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 自定义类的类加载器(应用程序类加载器)
ClassLoader customClassClassLoader = CustomClass.class.getClassLoader();
System.out.println(customClassClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// org.openjsse.sun.security.util.CurveDB的类加载器(扩展类加载器)
ClassLoader curveDBClassLoader = CurveDB.class.getClassLoader();
System.out.println(curveDBClassLoader); // sun.misc.Launcher$ExtClassLoader@4aa298b7
// String类的类加载器(引导类加载器)
ClassLoader stringClassLoader = String.class.getClassLoader();
System.out.println(stringClassLoader); // null
}
private static class CustomClass {}
}
3、加载配置文件
类加载器ClassLoader存在一个方法,用来加载src类路径下的文件:
// 获取指定资源的输入流(name就是src中文件的路径)
InputStream getResourceAsStream(String name);
在src文件中创建一个属性配置文件 db.properties,准备以下数据:
username=root
password=1234
借助类加载器把 src 中 db.properties 配置文件中的数据读取到 Properties 中去:
public class GetResourceByClassLoader {
public static void main(String[] args) throws IOException {
// 获取当前类的类加载器
ClassLoader classLoader = Test.class.getClassLoader();
// 借助类加载器获取src中的配置文件的输入流
InputStream inputStream = classLoader.getResourceAsStream("db.properties");
// 创建属性集对象
Properties pro = new Properties();
// 用load方法将流中的数据加载到属性集
pro.load(is);
// 打印属性集中的数据
System.out.println(pro);
}
}
四、类加载器的关系
1、类加载器的关系
除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
自定义类加载器
的父类加载器是系统类加载器
系统类加载器
的父类加载器是扩展类加载器
扩展类加载器
的父类加载器是启动类加载器
不同类加载器看似是继承(Inheritance)关系,实际上是包含关系。可以看到,在下层加载器中,包含着上层加载器的引用 parent
。
public abstract class ClassLoader {
// 父类加载器
private final ClassLoader parent;
}
2、Launcher类
public class Launcher {
public Launcher() {
// 创建 扩展类加载器
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError("Could not create extension class loader", e);
}
// 创建 应用程序类加载器
try {
// 这里把ExtClassLoader传进去,最后会赋值给parent
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError("Could not create application class loader", e);
}
// 这里可以看到,当前线程使用的类加载器,是 应用程序类加载器
Thread.currentThread().setContextClassLoader(loader);
// ...
}
static class AppClassLoader extends URLClassLoader {
/**
* 「应用程序类加载器」的构造方法
*/
AppClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent, factory);
ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
ucp.initLookupCache(this);
}
}
static class ExtClassLoader extends URLClassLoader {
/**
* 「扩展类加载器」的构造方法
*/
public ExtClassLoader(File[] dirs) throws IOException {
// 由于引导类加载器是由c/c++实现的,所以这里parent传的是null
super(getExtURLs(dirs), null, factory);
SharedSecrets.getJavaNetAccess().
getURLClassPath(this).initLookupCache(this);
}
}
}
从Launcher
类核心代码可以看出:
ExtClassLoader
的parent
是null
AppClassLoader
的parent
是ExtClassLoader
- 当前线程的ClassLoader 是
AppClassLoader
正是由于子类加载器中包含着父类加载器的引用,所以可以通过子类加载器的方法获取对应的父类加载器。
public class GetClassLoader {
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@232204a1
// 引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader); // null
// 由于引导类加载器是由c/c++实现的,所以这里是null
}
}
五、双亲委派机制
从JDK1.2开始,类的加载过程采用双亲委派机制,这种机制能更好地保证Java平台的安全。
1、工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法加载,会下沉到子加载器去加载,一直到最底层;
- 如果没有任何加载器能加载,就会抛出
ClassNotFoundException
。
2、源码分析 - loadClass
双亲委派模型的代码在 java.lang.ClassLoader
类中的 loadClass
中实现:
public abstract class ClassLoader {
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 同步操作,保证只加载一次
synchronized (getClassLoadingLock(name)) {
// 检查类是否被加载(在当前加载器的缓存中查找)
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 若未加载,递归调用父类加载器的loadClass方法
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// parent==null,则调用引导类加载器的操作
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 若该方法抛出ClassNotFoundException异常,表示父类加载器无法加载
}
// 若以上都未成功加载,则调用当前类加载器的findClass方法加载
if (c == null) {
long t1 = System.nanoTime();
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);
}
// 若父类加载器可以加载,则直接返回Class对象。
return c;
}
}
}
3、特性
可见性,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑。
单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相并不可见。(如下面的 loader1 和 loader2)
public class CustomClassLoader extends ClassLoader {
// 省略自定义类加载器实现...见第七章
public static void main(String[] args) {
try {
// 创建自定义的类加载器1
CustomClassLoader loader1 = new CustomClassLoader();
Class<?> clazz1 = loader1.findClass("com.test.java.User");
// 创建自定义的类加载器2
CustomClassLoader loader2 = new CustomClassLoader();
Class<?> clazz2 = loader2.findClass("com.test.java.User");
// clazz1和clazz2对应了不同的类模版结构
System.out.println(clazz1 == clazz2); // false
System.out.println(clazz1.getClassLoader()); // com.test.java.CustomClassLoader@4aa298b7
System.out.println(clazz2.getClassLoader()); // com.test.java.CustomClassLoader@28d93b30
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
4、优势
-
避免类的重复加载,确保一个类的全局唯一性
-
Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载。
当父亲加载器已经加载了某个类,子类加载器就不会再加载这个类了。
-
在同一命名空间中,不会出现全限定名相同的两个类。
-
-
保护程序安全,防止核心 API 被随意篡改
举例说明:自定义 java.lang.String
并使用,使用的时候会报错!
package java.lang;
public class String {}
package java.lang;
public class StringTest {
public static void main(String[] args) {
String str = new String();
}
}
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:656)
at java.lang.ClassLoader.defineClass(ClassLoader.java:755)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:473)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:419)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
at java.lang.ClassLoader.loadClass(ClassLoader.java:352)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:601)
假设没有双亲委派机制:
-
自定义的类由
系统类加载器
加载—>
系统类加载器
加载自定义的 java.lang.String
-
同一命名空间中,不会出现全限定名相同的两个类
—>
启动类加载器
不会再加载/jre/lib/rt.jar
下的java.lang.String
则核心类 java.lang.String
被篡改成了自定义的String类。
5、双亲委派机制的“破坏”
双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。
大部分的类加载器都遵循这个模型,但也有例外的情况,主要出现过以下3次“被破坏”的情况。
1)兼容旧代码
由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader
则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,
为了兼容已有代码,无法以技术手段避免loadClass()
被子类覆盖的可能性。只能在JDK1.2之后的java.lang.ClassLoader
中
添加一个新的protected
方法findClass()
,并引导用户自定义时尽可能重写findClass()
,而不是loadClass()
。
分析源码时可以看到,按照loadClass()
方法的逻辑,如果父类加载器加载失败,会自动调用自己的findClass()
方法完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派机制的。
2)线程上下文类加载器
双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(即越基础的类由越上层的加载器进行加载),但是如果存在基础类型需要调用回用户的代码的情况,那该怎么办呢?(我们之前说过父加载器时无法访问子加载器加载的类型的)
一个典型的例子便是JNDI服务。JNDI是在JDK 1.3时加入到rt.jar的,现在已经是Java的标准服务,它的代码由启动类加载器加载。JNDI提供统一SPI(Service Provider Interface,在Java平台中,通常把核心类rt.jar中提供外部服务、可由应用层自行实现的接口称为SPI)。可由应用层自行实现。而启动类加载器是无法访问这些实现的代码的。
为了解决这个问题,Java的设计团队引入了线程上下文类加载器(Thread Context ClassLoader)的概念。
这个类加载器可以通过java.lang.Thread
类的setContextClassLoader()
方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器(Launcher类)。
这样以线程上下文类加载器
为中介,使得启动类加载器
中的代码也可以访问系统类加载器
中的类。
不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断。
为了消除这种极不优雅的实现方式,在JDK6时提供了java.util.ServiceLoader
类,以META-INF/services
中的配置信息,辅以责任链模式,这才算是给SPI的加载提供了一种相对合理的解决方案。
3)热替换
还有就是由于用户对程序动态性的追求而导致的。如:代码热替换(Hot Swap)、模块热部署(Hot Deployment)
IBM公司主导的JSR-291(即OSGIR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGI中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bund1e连同类加载器一起换掉以实现代码的热替换。在OSGI环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。
当收到类加载请求时,OSGI将按照下面的顺序进行类搜索:
- 将以java.*开头的类,委派给父类加载器加载。
- 否则,将委派列表名单内的类,委派给父类加载器加载。
- 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
- 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
- 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
- 否则,查找Dynamic Import列表的Bundle,委派给对应Bund1e的类加载器加载。
- 否则,类查找失败。
说明:只有开头两点仍然符合双亲委派模型的原则,其余的类查找都是在平级的类加载器中进行的。
4)小结
双亲委派机制的“破坏”,并不一定是带有贬义的。只要有明确的目的和充分的理由,突破旧有原则无疑是一种创新。
6、热替换
热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。关键需求在于服务不能中断。
对Java来说,热替换并非天生就支持,如果一个类已经加载到系统中,通过修改类文件,并无法让系统再来加载并重定义这个类。因此,在Java中实现这一功能的一个可行的方法就是灵活运用ClassLoader。
不同ClassLoader加载的同名类属于不同的类型,不能相互转换和兼容。即两个不同的ClassLoader加载同一个类,在虚拟机内部,会认为这2个类是完全不同的。
基于这个特点,在程序运行时,只要创建新的ClassLoader去重新加载class文件即可实现热替换。
public class HotReplace {
public static void main(String[] args) {
while (true) {
try {
// 每个循环都会创建「新的」「自定义类加载器」
CustomClassLoader customClassLoader = new CustomClassLoader();
// 加载热替换的类
Class<?> clazz = customClassLoader.findClass("classLoader.User");
// 反射调用方法
Object obj = clazz.newInstance();
Method method = clazz.getMethod("print");
method.invoke(obj);
// 每5s执行一次
Thread.sleep(5000);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
public class User {
public void print() {
System.out.println("old print");
}
}
这里只要在运行过程中,替换.class文件,就可以实现热替换。(新的ClassLoader实例的命名空间与之前的是隔离的)
六、沙箱安全机制
沙箱安全机制
- 保证程序安全
- 保护Java原生的JDK代码
Java安全模型的核心就是Java沙箱(sandbox) \color{red}{} 。什么是沙箱?沙箱是一个限制程序运行的环境。
沙箱机制:将Java代码限定在 JVM 特定的运行范围中,并且严格限制代码对本地系统资源访问。通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏。
沙箱主要限制系统资源访问,那系统资源包括什么呢?CPU、内存、文件系统、网络。
不同级别的沙箱对这些资源访问的限制也可以不一样。所有的Java程序运行都可以指定沙箱,定制安全策略。
1、JDK1.0时期
在Java中将执行程序分成本地代码和远程代码两种。
- 本地代码:默认视为可信任的,可以访问一切本地资源。
- 远程代码:被看作是不受信的。安全依赖于沙箱(Sandbox)机制。
2、JDK1.1时期
JDK1.0中如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。
因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略。允许用户指定远程代码对本地资源的访问权限。
3、JDK1.2时期
在Java1.2版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。
4、JDK1.6时期
当前最新的安全机制实现,则引入了**域(Domain)**的概念。虚拟机中不同的域对应不一样的权限,存在于不同域中的类文件就具有了当前域的全部权限。虚拟机会把所有代码加载到不同的 系统域 和 应用域。
- 系统域:专门负责与关键资源进行交互。
- 应用域:通过系统域的部分代理来对各种需要的资源进行访问。
七、自定义类加载器
1、为什么要自定义类加载器?
-
隔离加载类
在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。
比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。
再比如:Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。
-
修改类加载的方式
类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,可以根据实际情况在某个时间点按需进行动态加载。
-
扩展加载源
比如从数据库、网络、甚至是电视机机顶盒进行加载。
-
防止源码泄漏
Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。
2、实现步骤
-
开发人员可以通过继承
java.lang.ClassLoader
的方式,自定义类加载器,以满足一些特殊的需求。 -
JDK1.2 之前,在自定义类加载器时,总会去继承 ClassLoader 类并重写
loadClass()
方法,从而实现自定义的类加载类。JDK1.2 之后,不再建议用户去覆盖
loadclass()
方法,而是建议把自定义的类加载逻辑写在findClass()
方法中。 -
编写自定义类加载器时,如果没有太过于复杂的需求,也可以直接继承
URLClassLoader
类,这样就可以避免自己去编写
findClass()
方法 及其获取字节码流的方式,使自定义类加载器编写更加简洁。
3、代码举例
public class CustomClassLoader extends ClassLoader {
/**
* 加载什么路径下的class
*/
private String classOutPath = "xxx/xxx/xxx";
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的class文件字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
// 直接生成class对象
return defineClass(name, classData, 0, classData.length);
}
/**
* 获取class文件,并转换为字节码流
*/
private byte[] getClassData(String className) {
String classPath = classNameToPath(className);
try {
// 读取类文件的字节
InputStream inputStream = Files.newInputStream(Paths.get(classPath));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
int len = 0;
byte[] buffer = new byte[4096];
// 读取类文件的字节码
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
return outputStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String classNameToPath(String className) {
return classOutPath + "/" + className.replace(".", "/") + ".class";
}
public static void main(String[] args) {
try {
// 创建自定义的类加载器1
CustomClassLoader loader1 = new CustomClassLoader();
Class<?> clazz1 = loader1.findClass("com.test.java.User");
// 创建自定义的类加载器2
CustomClassLoader loader2 = new CustomClassLoader();
Class<?> clazz2 = loader2.findClass("com.test.java.User");
System.out.println(clazz1 == clazz2); // false
System.out.println(clazz1.getClassLoader()); // com.test.java.CustomClassLoader@4aa298b7
System.out.println(clazz2.getClassLoader()); // com.test.java.CustomClassLoader@28d93b30
// 可以看到,自定义类加载器的父加载器,默认是AppClassLoader
System.out.println(loader1.getParent()); // sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(loader2.getParent()); // sun.misc.Launcher$AppClassLoader@18b4aac2
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}