实战java虚拟机09- classloader

本文深入讲解Java中的类加载器(ClassLoader)的工作原理,包括其实现细节、双亲委托机制及其实现热替换的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实战java虚拟机
深入理解java虚拟机

ClassLoader

类加载器(ClassLoader),它的主要工作在Class装载的加载阶段。其主要作用是从系统外部获得Class二进制数据流,然后交给java虚拟机进行连接、初始化等操作(ClassLoader无法改变类的连接和初始化工作)。
ClassLoader是一个抽象类,它提供了一些重要的接口,用于自定义Class的加载流程和加载方式.

public abstract class ClassLoader{
    //给定一个类名.加载一个类,返回代表这个类的实例。如果找不到则抛出ClassNotFoundException;
    public Class<?> loadClass(String name) throws ClassNotFoundException;

    //根据给定的字节码流b定义一个类,off和len表示实际Class信息在byte数组中的位置和长度。这是procted方法,只允许ClassLoader以及其子类中使用;
    protected final Class<?> defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain);

    //查找一个类,这是一个受保护的方法,也是重载ClassLoader时,重要的系统扩展点。这个方法会在loadClass()时被调用,用于自定义查找类的逻辑。
    protected Class<?> findClass(String name) throws ClassNotFoundException {};

    //查找已经加载的类,它会去寻找已经加载的类。这个类是final方法,无法被修改。
    protected final Class<?> findLoadedClass(String name) ;

}

ClassLoader的结构中,还有一个重要字段parent,它也是一个ClassLoader的实例,这个字段称为这个ClassLoader的双亲。在类加载的过程中,ClassLoader会将某些请求交给自己的双亲处理。

ClassLoader的分类
在标准的java程序中,java虚拟机会创建3类ClassLoader为整个应用程序服务。它们分别是:

  • BootStrapClassLoader(启动类加载器):负责加载JAVA_HOME/jre/lib目录中,或者-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,resources.jar…,名字不符合的即使放在该目录下也不识别)
-Xbootclasspath/a:path  被指定的文件追加到默认的bootstrap路径中。
-Xbootclasspath/p:path  让jvm优先于默认的bootstrap去加载path中指定的class
命令示例:
    java -Xbootclasspath/p:D:\tmp\boottest.jar
    java -Xbootclasspath/a:D:\tmp\clz
  • ,ExtensionClassLoader(扩展类加载器):负责加载 JAVA_HOME/jre/lib/ext目录中,或者被java.ext.dirs系统变量指定路径中的所有类库
  • AppClassLoader(应用类加载器,也称系统类加载器)。它负责加载用户路径(ClassPath)上所指定的类库。可以通过ClassLoader.getSystemClassLoader()开获取
  • 此外,每个应用程序还可以自定义ClassLoader,扩展java虚拟机的获取Class数据的能力。

这里写图片描述
当系统需要使用一个类时,在判断类是否已经加载时,会先从当前底层的类加载器进行判断。
当系统需要加载一个类时,会从顶部类开始加载,一次向下尝试,直到成功。
这些ClassLoader中,启动类加载器最为特殊,它完全是由C代码实现的,并且在java中没有对象与之对应。


public class PrintClassLoaderTree {
    public static void main(String[] args) {
        ClassLoader cl = PrintClassLoaderTree.class.getClassLoader();
        while (cl != null) {
            System.out.println(cl);
            cl = cl.getParent();
        }
    }
}

打印结果:

sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$ExtClassLoader@1b6d3586

另外:String.class.getClassLoader(); 为null,并不是说没有类加载器,而是说它是的类加载器是 BootStrapClassLoader


ClassLoader的双亲委托机制
双亲委托机制,即在类加载的时候,系统会判断当前类是否已经被加载,如果被加载,就直接返回可用的类。如果没有被加载,则会请求双亲处理,若双亲处理失败,则自己处理。

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, 检查类是否已经被加载
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                    //委托双亲加载
                        c = parent.loadClass(name, false);
                    } else {
                    //bootstrap加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                     long t1 = System.nanoTime();
                    // 若仍未找到,则执行自身的findClass寻找
                    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();
                }
            }

            return c;
        }
    }

双亲委托机制的弊端
双亲委托机制,检查类是否已经加载的委托是单向的。这种方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader加载的类。
这里写图片描述
通常情况下,启动类加载器中加载的类是系统核心类,包括一些重要的系统接口,而在应用类加载器,为应用类。这种模式下,应用类访问系统类自然是没有问题的,但是系统类访问应用类就会出现问题。
比较典型的例子便是JNDI服务,JNDI现在已经是java的标准服务,它的代码由启动类加载器去加载,但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署的在应用程序ClassPath下的JNDI接口提供者(SPI,Servie Provider Interface)的代码),但是启动类加载器不可能“认识”这些代码。

在java平台中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,通常称为(SPI,Service Provider Interface).java中常见的SPI接口有如JDBC,JNDI,JCE,JAXB和JBI等。
普通开发人员可能不熟悉,因为这个是针对厂商或者插件的。在java.util.ServiceLoader的文档里有比较详细的介绍。

为了解决SPI接口调用的问题,java团队引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序全局范围内都没有设置过的话,那么这个类加载器默认是这个应用程序加载器。
这里写图片描述



SPI解决的方法—-JDBC为例
在rt.jar中仅定义了接口java.sql.Driver

/*
 * When a Driver class is loaded, it should create an instance of
 * itself and register it with the DriverManager. This means that a
 * user can load and register a driver by calling
 * <pre>
 *   <code>Class.forName("foo.bah.Driver")</code>
 * </pre>
 *  需要向DriverManager.register()自己;
 **/
public interface Driver { }

引入mysql-connector-java-5.1.46.jar,后通常我们会这样使用,来创建数据库连接,

public Connection getConnection(String username,String password) throws ClassNotFoundException,
            SQLException {
        // 加载MySQL的JDBC的驱动 ---- 无需使用Class.forName,由于已经在mysql.Driver static{}域中注册了自己。
//      Class.forName("com.mysql.jdbc.Driver");
        String url = "jdbc:mysql://127.0.0.1:3306/activitidb";
        Connection conn = DriverManager.getConnection(url, username, password);
        return conn;
    }

很明显,此时的Driver是通过AppClassLoader加载,查看DriverManager源码

public class DriverManager {
    //1.类静态初始化
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    //2.loadInitialDrivers
    private static void loadInitialDrivers() {
          //......
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                //3.使用serviceLoader寻找实现了java.sql.Driver的插件
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

                Iterator driversIterator = loadedDrivers.iterator();
                try{
                    //4.迭代寻找Driver
                    while(driversIterator.hasNext()) {
                        println(" Loading done by the java.util.ServiceLoader :  "+driversIterator.next());
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });


         for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                 //5.使用SystemClassLoader初始化driver
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }

    }
}

ServiceLoader源码:

public final class ServiceLoader<S> implements Iterable<S> {
    private static final String PREFIX = "META-INF/services/";
     // The current lazy-lookup iterator
    private LazyIterator lookupIterator;


 public static <S> ServiceLoader<S> load(Class<S> service) {
     //3.2返回线程上下文类加载器(Thread Context ClassLoader)
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
       return ServiceLoader.load(service, cl);
   }

  private class LazyIterator implements Iterator<S> { 
        Enumeration<URL> configs = null;
        //4.2迭代
         public boolean hasNext() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    //4.3 寻找jar插件中/META-INF/services/java.sql.Driver文件
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }


  }
}

针对各大厂商如Oracle,Mysql都必定会在各自的jar包中存在文件/META-INF/services/java.sql.Driver.
这里写图片描述

debug代码时,观察如下信息:

Thread.currentThread().getContextClassLoader(); //当前线程:sun.misc.Launcher$AppClassLoader@160bc7c0
com.mysql.jdbc.Driver.class.getClassLoader();//厂商驱动:sun.misc.Launcher$AppClassLoader@160bc7c0

DriverManager.class.getClassLoader(); // null -- BootstrapClassLoader
//DriverManager.getCallerClassLoader(); //  sun.misc.Launcher$AppClassLoader@160bc7c0
java.sql.Driver.class.getClassLoader();//null -- BootstrapClassLoader

如果按照上述的代码展示的信息,按照双亲机制,DriverManager.connect加载到com.mysql.jdbc.Driver的,所以DriverManager才会做出如下的抉择:

 public static Connection getConnection(String url,
        java.util.Properties info) throws SQLException {

        // Gets the classloader of the code that called this method, may
        // be null.
        ClassLoader callerCL = DriverManager.getCallerClassLoader();

        return (getConnection(url, info, callerCL));
    }
 private static Connection getConnection(
        String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         * classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        synchronized(DriverManager.class) {
          // synchronize loading of the correct classloader.
          if(callerCL == null) {
              callerCL = Thread.currentThread().getContextClassLoader();
           }
        }

        //............
}


自定义classloader
在jdk1.2之后,java.lang.ClassLoader添加了一个新的protected的方法findClass(),如果不需要修改类加载默认机制,重写findClass方法即可。

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        Class clazz = this.findLoadedClass(className);
        if (null == clazz) {
            File file = null;//获取class源文件
            byte[] bytes = null;//获取资源文件
            clazz = defineClass(className ,bytes , 0 ,bytes.length);
        }
        return clazz;
    }

突破双亲机制
双亲模式的类加载方式是虚拟机的默认的行为,但并非必须这么做,通过重载ClassLoader可以修改这些行为。实际上,不少应用软件和框架都修改了这种行为,比如Tomcat和OSGi框架,都有各自独特的类加载顺序。突破双亲机制,需要通过重载loadClass()方法,改变类加载顺序。

public class OrderClassLoader extends ClassLoader {
    private String clazzBaseForder;

    public OrderClassLoader(String clazzBaseForder) {
        this.clazzBaseForder = clazzBaseForder;
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class re = findClass(name);
        if(re == null) {
            System.out.println("i can't load class:"+name+",need help from parent");
            return super.loadClass(name, resolve);
        }
        return re;
    }

    @Override
    protected Class<?> findClass(String clazzName) throws ClassNotFoundException {
        Class clazz = this.findLoadedClass(clazzName);
        if (null == clazz) {
            String fileName = getClazzFile(clazzName);
            try {
                RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
                FileChannel channel = raf.getChannel();
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                WritableByteChannel writeChannel = Channels.newChannel(baos);
                int len ;
                while(true ) {
                    //Class一次性读取1024byte,会报错 java.lang.ClassFormatError: Extra bytes at the end of class file
                    int read = channel.read(buffer);
                    if(read == -1){
                        break;
                    }
                    buffer.flip();
                    writeChannel.write(buffer);
                    buffer.clear();
                }
                channel.close();
                byte[] bytes = baos.toByteArray();
                clazz = defineClass(clazzName ,bytes , 0 ,bytes.length);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return clazz;
    }

    private String getClazzFile(String clazzName){
        return clazzBaseForder + File.separator + clazzName.replaceAll("\\.", "/")+".class";
    }
}

执行测试代码:

    static void convert() throws Exception{
        OrderClassLoader loader = new OrderClassLoader("D:\\tmp\\clz");
        Class clazz = loader.loadClass("cn.jhs.chap10.HelloLoader");
        HelloLoader helloLoader = (HelloLoader) clazz.newInstance();
    }

会抛出异常:java.lang.ClassCastException: cn.jhs.chap10.HelloLoader cannot be cast to cn.jhs.chap10.HelloLoader

两个不同的ClassLoader加载同一个类,在虚拟机内部,会认为这2个类完全不同的。


热替换(HotSwap)的实现
热替换是指在程序运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。类似计算机外设那样,接上鼠标,U盘不用重启机器就能立即使用。
两个不同的ClassLoader加载同一个类,在虚拟机内部,会认为这2个类完全不同的。 可以利用这个特点,来模拟实现热替换。
这里写图片描述

    static void hotswap()throws Exception{
        while(true) {
            //每一次新的OrderClassLoader,加载同一份字节码都是不同的对象。
            OrderClassLoader loader = new OrderClassLoader("D:\\tmp\\clz");
            Class clazz = loader.loadClass("cn.jhs.chap10.HelloLoader");
            Object obj = clazz.newInstance();
            Method m = clazz.getMethod("print", new Class[0]);
            m.invoke(obj, new Object[0]);

            Thread.sleep(1000);
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值