图解类加载机制、双亲委派模型、类的热加载(含代码演示)

本文解答的问题

  1. 类是如何被加载的
  2. ClassLoader 如何确定要加载的类的位置
  3. 什么是双亲委派模型?代码中如何体现?
  4. 类的热加载

准备

运行时数据区

图解类加载机制、双亲委派模型、类的热加载(含代码演示)_第1张图片

  • JVM用来存储加载的类信息、常量、静态变量、编译后的代码等数据
  • 虚拟机规范中这是一个逻辑区划。具体实现根据不同虚拟机来实现。
    • oracle的HotSpot在java7中方法区放在永久代
    • java8方法区放在元数据空间,并且通过GC机制对这个区域进行管理

 

类生命周期

图解类加载机制、双亲委派模型、类的热加载(含代码演示)_第2张图片

 

ClassLoader

  • 类加载器负责装入类,搜索网络、jar、zip、文件夹、二进制数据、内存等指定位置的类资源。
  • 一个java程序运行,最少有三个类加载器实例,负责不同类的加载。

图解类加载机制、双亲委派模型、类的热加载(含代码演示)_第3张图片

 

验证

查看类对应的加载器

查看一个类的类加载器

  • 通过JDK-API进行查看
    • java.lang.Class.getClassLoader()
    • 这个方法会返回装载类的类加载器
  • 如果这个类是由 BootstrapClassLoader 加载的,那么这个方法在这种实现中将返回 null
/**
 * 查看类的加载器实例
 */
public class ClassLoaderView {
    public static void main(String[] args) throws Exception {
        // 加载核心类库的 BootStrap ClassLoader(path: jre/lib)
        System.out.println("核心类库加载器:"
                + ClassLoaderView.class.getClassLoader().loadClass("java.lang.String").getClassLoader());
        // 加载拓展库的 Extension ClassLoader(path: jre/lib/ext)
        System.out.println("拓展类库加载器:" + ClassLoaderView.class.getClassLoader()
                .loadClass("com.sun.nio.zipfs.ZipCoder").getClassLoader());
        // 加载应用程序的
        System.out.println("应用程序库加载器:" + ClassLoaderView.class.getClassLoader());

        // 双亲委派模型 Parents Delegation Model
        System.out.println("应用程序库加载器的父类:" + ClassLoaderView.class.getClassLoader().getParent());
        System.out.println(
                "应用程序库加载器的父类的父类:" + ClassLoaderView.class.getClassLoader().getParent().getParent());
    }
}
核心类库加载器:null
拓展类库加载器:sun.misc.Launcher$ExtClassLoader@2b193f2d
应用程序库加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
应用程序库加载器的父类:sun.misc.Launcher$ExtClassLoader@2b193f2d
应用程序库加载器的父类的父类:null

JVM如何知道加载哪些类

  • class信息存放在不同的位置,桌面jar、项目bin目录、target目录等等.
  • JVM 通过读取配置(java.class.path),确定要加载哪些类
//sun.misc.Launcher.AppClassLoader#getAppClassLoader
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
    final String var1 = System.getProperty("java.class.path");
    final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
    return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
        public Launcher.AppClassLoader run() {
            URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
            return new Launcher.AppClassLoader(var1x, var0);
        }
    });
}
  • 利用 jps/jcmd 两个命令验证
    • jps查看 本机JAVA进程
    • 查看运行时配置:jcmd 进程号 VM.system_properties

类不会重复加载

  • 类的唯一性:同一个类加载器,类名一样,代表是同一个类。
  • 识别方式: ClassLoader Instance id + PackageName + ClassName
  • 验证方式:使用类加载器,对同一个class类的不同版本,进行多次加载,检查是否会加载到最新的代码。

/**
 * 指定class 进行加载e
 */
public class LoaderTest {
    public static void main(String[] args) throws Exception {
        URL classUrl = new URL("file:D:\\");//jvm 类放在位置

        URLClassLoader parentLoader = new URLClassLoader(new URL[]{classUrl});

        while (true) {
            
            // 问题:静态块触发
            Class clazz = loader.loadClass("HelloService");
            System.out.println("HelloService所使用的类加载器:" + clazz.getClassLoader());

            Object newInstance = clazz.newInstance();
            Object value = clazz.getMethod("test").invoke(newInstance);
            System.out.println("调用getValue获得的返回值为:" + value);

            Thread.sleep(3000L); // 1秒执行一次
            System.out.println();

            //  help gc  -verbose:class
            newInstance = null;
            loader = null;

        }

        // System.gc();
    }
}

类的卸载

  • 卸载一个类,需要满足两个条件
    • 该 Class 所有的实例都已经被 GC
    • 加载该类的 ClassLoader 实例已经被 GC
  • 验证方式: jvm启动中增加 -verbose:class 参数,输出类加载和卸载的日志信息
    • 参考上面代码

双亲委派模型

是什么

  • 为了避免重复加载由下到上逐级委托,由上到下逐级查找。
  • 首先不会自己去尝试加载类,而是把这个请求委派给父加载器去完成
  • 每一个层次的加载器都是如此,因此所有的类加载请求都会传给上层的启动类(bootClassLoader)加载器
  • 只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
  • PS:类加载器之间不存在父类子类的关系,可以理解为逻辑上定义的上下级关系

图解类加载机制、双亲委派模型、类的热加载(含代码演示)_第4张图片

什么用

  • 一个作用之一,是避免自定义的类加载器加载了一些 lib 包下,本该由 BootClassLoader 加载的类
  • 避免重复加载,lib/ext 已经加载过了,自定义的又加载一次

尝试热加载

热加载 > 重复加载一个类

  • 因为一个类的标识符组成:ClassLoader + Class路径 + Class
  • 解决:每次加载类,都使用一个新的 ClassLoader

/**
 * 指定class 进行加载e
 */
public class LoaderTest {
    public static void main(String[] args) throws Exception {
        URL classUrl = new URL("file:D:\\");//jvm 类放在位置

        URLClassLoader parentLoader = new URLClassLoader(new URL[]{classUrl});

        while (true) {
            // 创建一个新的类加载器
            URLClassLoader loader = new URLClassLoader(new URL[]{classUrl});

            // 问题:静态块触发
            Class clazz = loader.loadClass("HelloService");
            System.out.println("HelloService所使用的类加载器:" + clazz.getClassLoader());

            Object newInstance = clazz.newInstance();
            Object value = clazz.getMethod("test").invoke(newInstance);
            System.out.println("调用getValue获得的返回值为:" + value);
        }
    }
}

为自定义加载器指定父类,验证双亲委派

  • 因为双亲委派模型,会先用父类的类加载器(逻辑上)去尝试加载
  • 所以最终加载这个类的 ClassLoader 是 parent,因为同一个类不会被重复加载,故不会重复加载
  • 注意:如果没有显示的指定,则其加载器父子关系为 CustomClassLoader > ExtClassLoader > BootStrap ClassLoader
/**
 * 指定class 进行加载e
 */
public class LoaderTest {
    public static void main(String[] args) throws Exception {
        URL classUrl = new URL("file:D:\\");//jvm 类放在位置

        URLClassLoader parentLoader = new URLClassLoader(new URL[]{classUrl});

        while (true) {
            // 创建一个新的类加载器,指定父类加载器
            URLClassLoader loader = new URLClassLoader(new URL[]{classUrl}, parentLoader);

            // 问题:静态块触发
            Class clazz = loader.loadClass("HelloService");
            System.out.println("HelloService所使用的类加载器:" + clazz.getClassLoader());

            Object newInstance = clazz.newInstance();
            Object value = clazz.getMethod("test").invoke(newInstance);
            System.out.println("调用getValue获得的返回值为:" + value);
        }
    }
}

 

你可能感兴趣的:(JVM)