Java高阶私房菜:JVM类加载机制和双亲委派模型

目录

类加载子系统简介

什么是类加载子系统

加载子系统的三大特点

三个模块组成类加载子系统

类加载器(ClassLoader)

链接器(Linker)

初始化器(Initializer)

双亲委派机制

什么是双亲委派机制

为什么需要双亲委派模型

了解加载流程

JDK9模块化系统

新版本的JDK9后的类加载器

关键类BuiltinClassLoader

ClassLoader源码解读和自定义类加载器场景

 为什么使用自定义类加载器

ClassLoader核心源码解读

loadClass

findClass

defindClass

resolveClass

核心代码解读

自定义类加载器流程

高频面试题

编码测试

JDK多版本切换(Linux环境)


        在Java开发中,类加载子系统是一个至关重要的组成部分,这是Java开发者值得深入研究的重要主题之一。通过了解类加载器的结构和工作原理,我们可以更好地掌握Java开发中的核心概念和技术,并在实际项目中更加游刃有余。在文章中将提及类加载的高频面试题和案例,希望能给准备跳槽的小伙伴带来帮助,接下来让我们逐步深入了解JVM内部原理,探索里面的奥秘吧!

类加载子系统简介

什么是类加载子系统

        是Java虚拟机的一个重要子系统,主要负责将类的字节码加载到JVM内存的方法区,并将其转换为JVM内部的数据结构。

         验证、准备、解析,一般统称为链接。加载类之后,虚拟机会对类文件进行链接。

加载子系统的三大特点

        双亲委派模型:Java虚拟机采用双亲委派模型来加载类,即先从父类加载器中查找类,如果找到了就直接返回,否则再由自己的加载器加载,这种模型可以避免类的重复加载,提高系统的安全性。

        延迟加载:Java虚拟机采用延迟加载的策略,即只有在需要使用某个类时才进行加载,这种策略可以减少系统启动时间,提高系统的性能。

        动态加载:Java虚拟机支持动态加载类,即可以在程序运行时动态地加载和卸载类,这种特性可以使Java程序更加灵活和可扩展。

三个模块组成类加载子系统

        加载器(ClassLoader)(有锅往上抛):加载器负责将类的字节码加载到JVM中。主要有以下加载器:

类加载器(ClassLoader

        1)启动类加载器(Bootstrap ClassLoader):c/c++实现,加载核心类库使用,不继承ClassLoader,没父加载器。

        2)平台类加载器(Platform ClassLoader):JDK9之前是扩展类加载器 Extension ClassLoader。

        3)应用程序类加载器(Application ClassLoader):程序的默认加载器,我们写的代码基本都是由这个加载器负责加载。

类加载器用父类加载器、子类加载器这样的名字,虽然看似是继承关系,实际上是组合(Composition)关系

链接器(Linker)

        负责将Java类的二进制代码链接到Java虚拟机中,并生成可执行的Java虚拟机代码,包括验证、准备和解析等。

        1)验证操作主要是验证类的字节码是否符合JVM规范;

        2)准备操作主要是为类的静态变量分配内存并设置默认值;

        3)解析操作主要是将符号引用转换为直接引用;

初始化器(Initializer)

        负责执行Java类的静态初始化,包括静态变量的赋值、静态代码块的执行等。初始化器是类加载子系统的最后一个阶段。

双亲委派机制

什么是双亲委派机制

        是一种加载机制,类加载器之间形成了一条类加载链,每个类加载器都有一个父类加载器,形成了从下到上的一条继承链。如果所有的类加载器都无法加载该类,那么就会抛出ClassnotFoundException异常。

为什么需要双亲委派模型

        打个比方java.lang.Object 这些存放在rt.jar中的类,其实无论使用哪个类加载器加载,最终都会委派给最顶端的启动类加载器加载。不同加载器加载的Object类都是同一个,如果没有使用双亲委派模型,各个类加载器自行去加载的话,就会出现问题。假设用户编写了一个称为java.lang.Object或String的类,并放在classpath下,那系统将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证。后面将会以一个例子说明,这里需要注意的是不同类加载器加载相同的class类是不一样的。

        优点在于它可以保证类的唯一性和安全性,由于每个类加载器都只能加载自己的命名空间中的类。由于类加载器之间形成了一个继承链,因此可以保证类的安全性,防止恶意代码的注入。

了解加载流程

        一个类的加载请求首先会被委派给其父类加载器进行处理,如果父类加载器无法加载该类,则会将加载请求委派给其自身进行加载。如果自身也无法加载该类,则会将加载请求委派给子类加载器进行处理,直到找到能够加载该类的类加载器为止。

JDK9模块化系统

        是一种新的Java平台的组织方式,将Java SE分成多个模块,每个模块都有自己的API和实现。每个模块都有一个唯一的标识符和版本号,可以独立地进行开发、测试、部署和维护,模块之间的依赖关系通过模块描述文件(module-info.java)来声明,s这个文件包含模块的名称、版本号、导出的包、依赖的模块等信息。在编译和运行时,模块系统会根据模块描述文件来加载和链接模块,确保模块之间的依赖关系正确。

JDK9之前和JDK9后类库的结构

 

新版本的JDK9后的类加载器

模块化系统中的类加载器可以分为两种类型:

        1)应用程序类加载器,加载器用于加载应用程序中的模块;

        2)平台类加载器,加载器用于加载 JDK 中的模块;

        当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果findLoadedModule 可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。在模块化系统中,每个模块都有一个类加载器,它根据模块的依赖关系来加载模块中的类和依赖的模块中的类。在模块化系统中,类加载器的原理与传统的类加载器相似,都是采用双亲委派模型。

        当一个类被加载时,类加载器首先会检查自己是否已经加载过该类,如果没有,则会将该类的加载请求委托给其父加载器。直到达到顶层的 Bootstrap ClassLoader 为止。如果所有的父加载器都无法加载该类,则由当前加载器自己来加载该类。

关键类BuiltinClassLoader

        BuiltinClassLoader 是 jdk9 中代替 URLClassLoader 的加载器,是 PlatformClassLoader 与 AppClassLoader 的父类。

ClassLoader源码解读和自定义类加载器场景

 为什么使用自定义类加载器

        其目的是为 Java 应用程序提供更加灵活和可定制的类加载机制,可用于拓展加载源,可从网络,数据库等地方加载类;防止源码泄漏,自定义类加载器加载加密的类文件,保护类的安全性;实现类隔离,自定义类加载器可以实现类隔离,避免类之间的冲突和干扰,该方式在tomcat中有大量应用。

比较两个类是否相等,只有两个类是由同一个类加载器加载的前提下才有意义,否则即使两个类来自同一个class文件,但是由于加载他们的类加载器不同,那这两个类就不相等,不同类加载器加载同一个class文件得到的类型是不同的。

ClassLoader核心源码解读

loadClass

        用于加载指定名称的类,双亲委派模型核心实现,一般不建议重写相关方法,直接由ClassLoader自己实现,遵循双亲委派模型,首先委派给父类加载器进行加载,如果父类加载器无法加载该类,则自身进行加载。

findClass

        是用于查找类的方法,它通常由子类加载器实现,用于查找自身命名空间中的类,由于历史JDK1.2之前版本兼容问题,自定义类加载器则推荐重写这个方法。findClass()方法是在loadClass()方法中调用,当loadClass()方法中加载失败后,则调用自己的findClass()方法来完成类加载,但是ClassLoader的findClass没有实现,需要自己实现具体逻辑,findClass方法通常是和defineClass方法一起使用的。

defindClass

        是用于定义类的方法,它将字节数组转换为 Class 对象,并将其添加到类加载器的命名空间中,ClassLoader方法里面已经实现,findClass方法通常是和defineClass方法一起使用的。

resolveClass

        是用于解析类的方法,它将类的引用转换为实际的类,并进行链接和初始化。

核心代码解读

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //首先检查这个class是否已经加载过了

            Class<?> c = findLoadedClass(name);
            //c==null表示没有加载
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    //如果父类的加载器为空 则递归到bootStrapClassloader,这里就是双亲委派模型的实现
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                  /如果bootstrapClassLoader 仍然没有加载过,则自己去加载class
                    long t1 = System.nanoTime();
                  //是一个空方法,返回内容为class,方法其中没有任何内容,只抛出了个异常,说明这个方法需要开发者自己去实现
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

这里需要注意两个方法区别:

        1)findClass( )用于写类加载逻辑;

        2)loadClass( )方法的逻辑里,如果父类加载器加载失败则会调用自己的findClass( )方法完成加载,保证了双亲委派规则;

如果不想打破双亲委派模型,那么只需要重写findClass方法即可;

如果想打破双亲委派模型,多数情况下需要重写整个loadClass方法;

自定义类加载器流程

        1)继承ClassLoader类;

        2)重写loadClass方法(会破坏双亲委派机制,不推荐);

        3)重写findClass方法 (推荐);

// 重写findclass
public class MyClassLoader extends ClassLoader {

    private String codePath;

    public MyClassLoader(String codePath) {
        this.codePath = codePath;
    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = codePath + name + ".class";
        System.out.println(fileName);
        //获取输入流
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName));
             ByteArrayOutputStream bos = new ByteArrayOutputStream();) {
            int len;
            byte[] data = new byte[1024];
            while ((len = bis.read(data)) != -1) {
                bos.write(data, 0, len);
            }
            //获取内存中字节数组
            byte[] byteCode = bos.toByteArray();
            //执行 defineClass 将字节数组转成Class对象
            Class<?> defineClass = defineClass(null, byteCode, 0, byteCode.length);
            return defineClass;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;

    }
}
//测试自定义类加载器
public static void test1()throws Exception{
        MyClassLoader myClassLoader = new MyClassLoader("/Users/muller/Desktop/");
        //加载指定类
        Class<?> clazz = myClassLoader.loadClass("HeapDemo");
        Object obj = clazz.getDeclaredConstructor().newInstance();

        System.out.println(obj.getClass().getName()+"————"+obj.getClass().getClassLoader().getClass().getName()+"类加载器加载");
    }

高频面试题

        两个不同的类加载器加载同一个class类,JVM是否认为它们相同?

        该问题主要考查的是自定义类加载器的知识,其定义类是否相同的条件在于类加载器相同,class文件相同。不同类加载器会加载同名的 class 类,这些类在 JVM 中是不同的,即它们的 Class 对象是不同的。

        简而言之 JVM 中,不同类加载器加载同一个类时,可能会出现重复加载的情况,当不同类加载器加载同一个类时,每个类加载器都会在自己的命名空间中创建一个新的 Class 对象,即使这些 Class 对象的字节码是一样的,也会被认为是不同的类。重复加载同一个类会导致一些问题,例如类的静态变量和代码块会被多次执行,导致出现意料之外的行为。而JVM正是采用了类的双亲委派模型来避免重复加载同一个类。

编码测试

 //测试不同classloader加载同个class
    public static void test2()throws Exception{
        MyClassLoader myClassLoader = new MyClassLoader("/Users/muller/Desktop/");
        //加载指定类
        Class<?> clazz = myClassLoader.loadClass("HeapDemo");
        Object obj = clazz.getDeclaredConstructor().newInstance();
        System.out.println(obj.getClass());
        // false,前置
        System.out.println(obj instanceof HeapDemo);

        //jvm.HeapDemo————jvm.MyClassLoader类加载器加载
        System.out.println(obj.getClass().getName()+"————"+obj.getClass().getClassLoader().getClass().getName()+"类加载器加载");

        System.out.println(HeapDemo.class.getName()+"————"+HeapDemo.class.getClassLoader().getClass().getName()+"类加载器加载");

    }

JDK多版本切换(Linux环境)

JAVA_HOME_17=/Library/Java/JavaVirtualMachines/jdk-17.0.5.jdk/Contents/Home
JAVA_HOME_11=/Library/Java/JavaVirtualMachines/jdk-11.0.17.jdk/Contents/Home
JAVA_HOME_8=/Library/Java/JavaVirtualMachines/jdk1.8.0_351.jdk/Contents/Home
JRE_HOME=$JAVA_HOME/jre
PATH=$PATH:$JAVA_HOME/bin
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar

MAVEN_HOME=/Users/xdclass/Documents/software/source/maven
PATH=$PATH:$MAVEN_HOME/bin
export PATH JAVA_HOME CLASSPATH MAVEN_HOME
export JAVA_HOME=$JAVA_HOME_11


alias jdk11="export JAVA_HOME=$JAVA_HOME_11"
alias jdk17="export JAVA_HOME=$JAVA_HOME_17"
alias jdk8="export JAVA_HOME=$JAVA_HOME_8"

  • 54
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值