目录
ClassLoader
JVM 的双亲委托机制
自定义 ClassLoader
破坏双亲模式例子——先从底层ClassLoader加载
重写 findClass 方法:
加载 class 文件到内存
重写 loadClass 方法:破坏双亲委托
编写测试:
运行结果:
热部署:
参考资料:
ClassLoader 是一个抽象类,它的实例负责类装载过程中的加载阶段,取得类的二进制流转为方法区数据结构,然后在 Java 堆中生成对应的 Class 对象。
注意:程序在启动的时候,并不会一次性加载程序所要用的所有 class 文件,而是根据程序的需要,通过 Java 的类加载机制来动态加载某个 class 文件到内存中。
JVM 的 3 种 ClassLoader:
种类 | 描述 |
BootStrap ClassLoader(启动ClassLoader) | 是native code写的。它是所有ClassLoader的祖先,它是顶级ClassLoader。它负责加载JDK的内部类型,一般来说就是位于$JAVA_HOME/jre/lib下的核心库和rt.jar。 |
Extension ClassLoader(扩展ClassLoader) | 负责加载Java核心类的扩展,加载$JAVA_HOME/lib/ext目录和System Property java.ext.dirs所指定目录下的类(见Java Extension Mechanism Architecture)。 |
App ClassLoader(应用ClassLoader/系统ClassLoader) | 它的parent class loader是extension class loader(可以从sun.misc.Launcher的构造函数里看到),负责加载CLASSPATH环境变量、-classpath/-cp启动参数指定路径下的类。 |
与之相关的参数:
参数 | 描述 |
-Xbootclasspath:bootclasspath | 让jvm从指定的路径中加载bootclass,用来替换jdk的rt.jar。一般不会用到。 |
-Xbootclasspath/a:path | 把路径添加到已存在BootStrap ClassLoader搜索路径的后面。常用!! |
-Xbootclasspath/p:path | 与上一条相反,放前面 |
-Djava.ext.dirs | 设置扩展 类加载器的搜索路径 |
-Djava.class.path= -classpath -cp |
设置AppClassLoader的搜索路径(路径存在空格,用双引号包裹) |
在双亲委派机制中,各个加载器按照父子关系形成了树形结构(逻辑意义),除了根类加载器之外,其他类加载器都有且只有一个 parent
JVM 检查类是否加载的顺序为:
App(应用程序自己的) -> Extension(JRE_HOME/lib/ext中的) -> Bootstrap(jvm自带的)
JVM 尝试加载类的顺序为:
Bootstrap(jvm自带的) -> Extension(JRE_HOME/lib/ext中的) -> App(应用程序自己的)
源码:
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 {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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);
}
return c;
}
}
好处:双亲委派模型能保证基础类仅加载一次,不会让jvm中存在重名的类。比如 String.class,每次加载都委托给父加载器,最终都是 BootstrapClassLoader,都保证Java 核心类都是 BootstrapClassLoader 加载的,保证了Java 的安全与稳定性。
双亲委派模型只是JVM规范要求。
双亲委托机制的问题:顶层 ClassLoader,无法加载底层 ClassLoader 的类
解决:
java.lang.Thread#getContextClassLoader()
java.lang.Thread#setContextClassLoader()
通过上面的俩方法来让位于顶层的 ClassLoader 加载位于下层的Class
基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例
JVM 提供的类加载器,只能加载指定目录的 jar 和 class,如果我们想加载其他位置的类或 jar 时,例如,加载网络上的一个 class文件,默认的 ClassLoader 就不能满足我们的需求了,那就自己定义一个类加载器。
方式一:继承 ClassLoader,然后覆盖 findClass(String name) 方法即可完成一个带有双亲委派模型的类加载器。
方式二:继承 URLClassLoader 类,然后设置自定义路径的 URL 来加载 URL 下的类。我们将指定的目录转换为 URL 路径,然后重写 findClass(String name) 方法。
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
Class> res = this.findLoadedClass(name);
if(null != res) {
return res;
}
byte[] bytes = null;
try {
bytes = loadClassBytes(name);
} catch (FileNotFoundException e) {
throw new ClassNotFoundException("找不到该类:" + name);
}
if (bytes != null) {
res = defineClass(name, bytes, 0, bytes.length);
}
return res;
}
private byte[] loadClassBytes(String className) {
String name = className.replace('.', '/').concat(".class");
FileInputStream fInputStream = null;
ByteArrayOutputStream byteArrOutputStream = new ByteArrayOutputStream();
byte[] data = null;
try {
fInputStream = new FileInputStream(extDirPath + name);
int bytesRead = 0;
byte[] buffer = new byte[1024];
while ((bytesRead = fInputStream.read(buffer)) != -1) {
byteArrOutputStream.write(buffer, 0, bytesRead);
}
data = byteArrOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fInputStream != null) {
fInputStream.close();
fInputStream = null;
}
if(byteArrOutputStream != null) {
byteArrOutputStream.close();
byteArrOutputStream = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
return data;
}
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
Class> res = null;
try {
res = findClass(name);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
if (res == null) {
System.out.println("无法载入类:" + name + "需要请求父加载器");
res = super.loadClass(name);
}
return res;
}
public class TesterMyClassLoader {
private static String classQualifiedName = "com.xinghe.learn.jvm.cl.HelloLoaderBean";
public static void main(String args[]) throws Exception {
// class文件存放目录
String classPath = "E:/Other/Develop/Clz/";
MyClassLoader myLoader = new MyClassLoader(classPath);
Class> clz = myLoader.loadClass(classQualifiedName);
HelloLoaderBean bean = (HelloLoaderBean) clz.newInstance();
bean.print();
}
}
public class HelloLoaderBean {
public void print() {
System.out.println("================== print run =================");
System.out.println("classloader:" + HelloLoaderBean.class.getClassLoader());
System.out.println("run。。。");
System.out.println("==============================================");
}
}
但是最后的输出结果就有点意外啊, 两个一毛一样的类型你跟我说不能转换??
猜测:难道说这个 Class 被多个不同的 ClassLoader 加载了?导致它们是两个 Class?
验证:在上面 loadClass 方法后加上如下输出代码
System.out.println(clz == HelloLoaderBean.class);
System.out.println(HelloLoaderBean.class.getClassLoader());
输出结果:
通过上述结果可以看到,HelloLoaderBean 被俩 ClassLoader 加载了。
HelloLoaderBean bean = (HelloLoaderBean) clz.newInstance();
bean.print();
在上述代码中,HelloLoaderBean.class 会隐性的被 加载当前类的 ClassLoader 加载,当前 main 方法默认的 ClassLoader 为 AppClassLoader,而不是我们自定义的 MyClassLoader。
由此可以得出结论:一个Class的唯一性不仅仅是其全限定名(Fully-qualified-name),而是由【加载其的ClassLoader + 其全限定名】联合保证唯一。
替换一个 class 后立即生效,并且系统无需重启。
示例:
public static void main(String[] args) {
final String classPath = "E:\\Workspace\\JVM\\JVMTuning-ClassLoder\\bin\\";
final String classQualifiedName = "com.ningdd.learn.jvm.cl.hotreplacement.TestBean";
// 创建一个2s执行一次的定时任务
new Timer().schedule(new TimerTask() {
@Override
public void run() {
// 每次都实例化一个ClassLoader,这里传入swap路径,和需要特殊加载的类名
MyClassLoader myClassLoader = new MyClassLoader(classPath);
try {
// 使用自定义的ClassLoader加载类,并调用print方法。
Class> clazz = myClassLoader.loadClass(classQualifiedName);
Object instance = clazz.newInstance();
clazz.getMethod("print").invoke(instance);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}, 0, 2000);
运行结果:
https://chanjarster.github.io/post/jvm/classloader/1-intro/
https://yq.aliyun.com/articles/269781
https://zhuanlan.zhihu.com/p/54693308
https://greenhathg.github.io/2019/05/03/Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E7%AC%94%E8%AE%B0-%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%9C%BA%E5%88%B6/
https://blog.csdn.net/caidai1989/article/details/72729515