走进 JVM —— ClassLoader

目录

ClassLoader 

JVM 的双亲委托机制 

自定义 ClassLoader 

 破坏双亲模式例子——先从底层ClassLoader加载

 重写 findClass 方法:

加载 class 文件到内存

重写 loadClass 方法:破坏双亲委托

编写测试:

 运行结果:

热部署:

参考资料:


ClassLoader 

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的搜索路径(路径存在空格,用双引号包裹)

JVM 的双亲委托机制 

在双亲委派机制中,各个加载器按照父子关系形成了树形结构(逻辑意义),除了根类加载器之外,其他类加载器都有且只有一个 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的实例

 

自定义 ClassLoader 

JVM 提供的类加载器,只能加载指定目录的 jar 和 class,如果我们想加载其他位置的类或 jar 时,例如,加载网络上的一个 class文件,默认的 ClassLoader 就不能满足我们的需求了,那就自己定义一个类加载器。

方式一:继承 ClassLoader,然后覆盖 findClass(String name) 方法即可完成一个带有双亲委派模型的类加载器。

方式二:继承 URLClassLoader 类,然后设置自定义路径的 URL 来加载 URL 下的类。我们将指定的目录转换为 URL 路径,然后重写 findClass(String name) 方法。

 破坏双亲模式例子——先从底层ClassLoader加载

 重写 findClass 方法:

@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;
}

加载 class 文件到内存

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;
}

重写 loadClass 方法:破坏双亲委托

@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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值