【JVM】一文详解类加载器

文章目录

类加载器的概述

Java中的类加载机制是指在Java程序运行时,将类文件(通常是.class文件)加载到内存中的一系列步骤和过程。这一机制确保了类能够在需要的时候被正确、安全地加载到Java虚拟机(JVM)中,并进行初始化和使用。Java的类加载机制遵循着"按需加载"原则 ,即只有在需要用到某个类的时候,才会加载该类。

简单来说:ClassLoaderJava的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例

类加载器的分类

启动类加载器(Bootstrap ClassLoader )

根类加载器 ,是虚拟机的一部分 ,由C++语言实现的(所以不属于Java当中的某个具体的类,打印的时候会显示null) ,且没有父加载器 ,没有继承java.lang.ClassLoader .主要用于加载JDK核心库,加载时的搜索路径为sun.boot.class.path(JDK8之前) java.class.path (JDK17) .

扩展类型加载器(ExClassLoader)

扩展类加载器是由Java语言进行编写的,父加载器是根类加载器 ,负责加载的是<JAVA_HOME>\jre\lib\ext

系统类加载器(Application ClassLoader )

系统类加载器也称之为应用类加载器,也是纯java类,他的父加载器是扩展类加载器 ,他负责从classpath环境变量

或者java.class.path所指定的目录中加载类,他是用户自定的类加载器的默认父加载器 ,该加载器是程序默认的类加载器,可以ClassLoader.getSystemClassLoader()直接获得

总结

jvm虚拟机class文件 采用的是按需加载 的方式,也就是说当需要使用该类时才会将他的.class文件加载到内存上

生成class对象,而且加载某个类的.class文件时,jvm采用的是双亲委派机制 ,即将加载类的请求交由父加载器处理.

双亲委派机制

概念

除了根加载器 之外,其他的类加载器都需要有自己的父加载器 ,双亲委派机制可以很好的保护java程序的安全,除了虚拟机自带的根加载器之外,其余的类加载器都有唯一 的父加载机制.所以,在加载某个类的时候,会先让该类的类加载器委托自己的父加载器先去加载这个类,如果父加载器可以加载,则由父加载器进行加载,否则才使用该类的类加载器进行加载 .即每个类加载器都很懒,加载类的时候都先让父加载器去尝试加载,一直到根加载器为止,加载不到的时候自己才去加载.

注意:双亲委派机制的父子关系并非是OOP当中的继承关系,而是通过使用组合模式来复用父加载器的代码.

双亲委派机制的优势

  1. 可以避免类被重复加载,当父加载器已经加载了该类的时候,就没有必要再使用子加载器进行再一次的加载了.
  2. 避免安全隐患.java核心API中定义的类型不能被随意更改,假设通过网络传递一个名为java.lang.Object的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心Java API发现了这个名字的类,发现该类已经被加载,所以就不会重新加载网络传递过来的java.lang.Object.class,这样可以方式核心的API库被随意篡改.

问题:我们可以自己定义一个Java.lang.String

执行结果:

原因:

程序在执行时识别的是src中的java.lang.Stringsrc就是classpath,这时我们自定义的类,所以会调用系统加载器。但根据双亲委派机制,系统加载器会逐层委派双亲来加载此类,在委派的时候,最上层的加载器是根加载器,即根加载器优先级最高。而根加载器能够在jre\lib\rt.jar包中找到一个重名的java.lang.String(即jdk自带的String),因此根据双亲委派最终会由最顶层的根加载器来执行jdk自带的java.lang.String。显然,jdk中的String并没有main()方法,因此报错找不到main()

也就是:我们最终加载到的还是java核心库中的String

ClassLoader

ClassLoader的源码中,有一个方法叫做Class<?> loadClass(String name, boolean resolve),这就是双亲委派模式的代码实现.

JVM尝试加载一个类时:会首先调用 loadClass 方法。loadClass 方法首先会检查该类是否已经被加载(即检查是否已经存在于JVM的类缓存中),如果没有,它会委托给父类加载器(如果有的话),直到到达引导类加载器(Bootstrap ClassLoader)。如果父类加载器无法加载该类,那么 loadClass 方法会调用 findClass 方法来尝试加载类。因此,findClass 方法的具体实现应该负责找到类的字节码,并将其传递给 defineClass 方法。

//name:包名+类名
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{      
    synchronized (getClassLoadingLock(name)) {
        // 1.先查询当前的所需的类是否已经被加载到了内存中
        Class<?> c = findLoadedClass(name);
        //2.没有被加载到内存中
        if (c == null) {
            long t0 = System.nanoTime();
            try {
            /* 双亲委派模式的实现*/
            //如果当前不是父加载器,那么去父加载器中继续寻找
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                //是根加载器,那么就要从根加载器中获取该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
              
            }

            if (c == null) {
               
                long t1 = System.nanoTime();
                //一直到根加载器,也没有找到该类
                //一般来说就是在自定义类被加载的时候
                //所以我们需要复写这个方法findClass(String name)
                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;//这里返回的是一个class对象
    }
}
//由于我们要复写这个方法,所以这里给了一个默认的抛异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

步骤:

  • findLoadedClass(String) 通过该方法查找class是否已经载入内存,如果有,返回该class文件,没有到第二步
  • 通过loadClass方法,让父类先去载入,父类继续调用父类,循环至顶层的bootstarp加载器去加载.class文件
  • 看最后一段注释,findClass(String)方法一般定义在子类中,extClassLoader,AppClassLoaderfindClass方法定义在它们的父类(不是父加器)UrlClassLoader里面

findClass

public class BuiltinClassLoader extends SecureClassLoader
{
protected Class<?> findClass(String cn) throws ClassNotFoundException {
    
    if (!VM.isModuleSystemInited())
        throw new ClassNotFoundException(cn);
    
    LoadedModule loadedModule = findLoadedModule(cn);

    Class<?> c = null;
    if (loadedModule != null) {

        if (loadedModule.loader() == this) {
            c = findClassInModuleOrNull(loadedModule, cn);
        }

    } else {
        if (hasClassPath()) {
            c = findClassOnClassPathOrNull(cn);
        }
    }
    if (c == null)
        throw new ClassNotFoundException(cn);
    return c;
}
}

defineClass

用来byte字节解析成虚拟机能够识别的Class对象 ,defineClass()方法通常与findClass()方法一起使用,在自定义类加载器 的时候,会直接覆盖ClassLoaderfindClass方法获取到要加载类的字节码文件 ,然后使用defineClass方法生成Class对象,即将字节码数组转换成Class对象

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                  ProtectionDomain protectionDomain)throws ClassFormatError
{
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(this, name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}

loadClass,findClass,defineClass之间的关系

resolveClass

连接指定的类,类加载器就可以使用此方法来连接类。

URLClassLoader

java.net包中,JDK提供了一个更加易用的类加载器 URLClassLoader,它扩展了ClassLoader,能够从本地或者网络上指定的位置加载类 。我们可以使用该类作为自定义的类加载器使用

  1. 构造方法:
  • public URLClassLoader(URL[] urls, ClassLoader parent):指定要加载的类所在的URL地址,并指定父类加载器

    //构造方法1
    public class URLClassLoader extends SecureClassLoader implements Closeable {
    //.......
    public URLClassLoader(URL[] urls, ClassLoader parent) {
    super(parent);
    this.acc = AccessController.getContext();
    this.ucp = new URLClassPath(urls, acc);
    }

    //.......
    }

  • public URLClassLoader(URL[] urls):指定要加载的类所在的URL地址,父类加载器默认为系统类加载器

    //构造方法2:
    public class URLClassLoader extends SecureClassLoader implements Closeable {
    //........
    public URLClassLoader(URL[] urls) {
    super();
    this.acc = AccessController.getContext();
    this.ucp = new URLClassPath(urls, acc);
    }

    //.......
    }

案例一:加载磁盘上的类

解释一下URIURL

URI统一资源标识符 )是一个用于标识某一互联网资源名称的字符串 。它是对资源的位置、访问方式以及网络中资源的标识进行抽象的描述。URI有几种不同的类型,其中最常见的类型是URL统一资源定位符 )和URN统一资源名称 )。
URLURI一个子集 ,它不仅标识了资源 ,而且还提供了访问该资源的具体位置访问方法URL通常用于互联网上的网页地址 ,格式通常包括协议(如httphttps)、主机名、端口(可选)、路径以及查询参数等。例如,http://www.example.com:80/path/to/resource?query=parameter是一个URL

首先,现在D盘中建立一个名字为demo.java文件

public class test {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {

        File file =new File("d:/");//将D盘中的资源转换成file的属性
        //URI和URL
        URI uri=file.toURI();//获取到file的uri
        URL url = uri.toURL();//获取到file的url
        //使用url对classLoader进行创建
        URLClassLoader classLoader=new URLClassLoader(new URL[]{url});
        System.out.println("父类加载器:"+classLoader.getParent());//找到父加载器
        //使用该类加载器,读取我们想要读取的全限定类名
        Class clazz=classLoader.loadClass("com.fbl.ClassLoader.demo");//返回一个字节码文件
        //进行实例化
        clazz.newInstance();
    }
}

运行结果:

打印出了构造方法中的语句demo instance,说明demo实例化成功

自定义类加载器

需要继承ClassLoader类,并且重写findClass方法

举例:

                    // 自定义一个类加载器  //
public class MyClassLoader extends ClassLoader{
    public String dic;//要加载的类所在的目录

    //1.建立两个构造方法

    public MyClassLoader(ClassLoader parent, String dic) {
        super(parent);
        this.dic = dic;
    }

    public MyClassLoader(String name) {
        this.dic = name;
    }
    //2.重写findClass方法:找到该类对应的字节码文件,转化成可以被JVM读取的二进制文件
    
    //这里的name:com.fbl.ClassLoader.demo
    
    protected Class<?> findClass(String name) throws ClassNotFoundException{
        //1.获取到该类对应的字节码文件在磁盘上的绝对路径
        //com.fbl.classloader.demo  ->  D:/com/fbl/ClassLoader/demo.class
        
        String file=dic+File.separator+ name.replace(".",File.separator)+".class";
        System.out.println(dic);
        
        //2.构建输入流:
        int len= 0;
        byte buf[]= new byte[0];//字节码数组
        try {
            InputStream in=new FileInputStream(file);

            //3.构建字节输出流
            ByteArrayOutputStream out=new ByteArrayOutputStream();
            len = -1;
            buf = new byte[1024];

            while((len=in.read(buf))!=-1) {
                out.write(buf, 0, len);
            }
            in.close();
            out.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return defineClass(name,buf,0,len);
    }

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        MyClassLoader classLoader=new MyClassLoader("D:/");
        Class clazz=classLoader.loadClass("com.fbl.ClassLoader.demo");//在loadClass中会调用findClass
        clazz.newInstance();

    }
}

类的显式和隐式加载

显式加载

显式加载 是指在java代码中通过调用ClassLoader加载class对象,比如Class.forName(String name)this.getClass().getClassLoader()来加载类。

隐式加载

隐式加载是指不需要在java代码中明确调用加载的代码,而是通过虚拟机自动加载到内存中。比如在加载某个class时,该class引用了另外一个类的对象,那么这个对象的字节码文件会被虚拟机自动加载到内存中。

相关推荐
学点东西吧.3 分钟前
JVM(五、垃圾回收器)
jvm
请你打开电视看看2 小时前
Jvm知识点
jvm
程序猿进阶3 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
阿龟在奔跑15 小时前
引用类型的局部变量线程安全问题分析——以多线程对方法局部变量List类型对象实例的add、remove操作为例
java·jvm·安全·list
王佑辉15 小时前
【jvm】方法区常用参数有哪些
jvm
王佑辉15 小时前
【jvm】HotSpot中方法区的演进
jvm
Domain-zhuo16 小时前
什么是JavaScript原型链?
开发语言·前端·javascript·jvm·ecmascript·原型模式
Theodore_10222 天前
7 设计模式原则之合成复用原则
java·开发语言·jvm·设计模式·java-ee·合成复用原则
我是苏苏2 天前
Web开发:ORM框架之使用Freesql的DbFrist封装常见功能
java·前端·jvm
天草二十六_简村人2 天前
Java语言编程,通过阿里云mongo数据库监控实现数据库的连接池优化
java·jvm·数据库·mongodb·阿里云·微服务·云计算