类加载过程以及ClassLoader特性和热替换

本文详细探讨了Java的类加载过程,包括加载、链接(校验、准备、解析)和初始化阶段。此外,还介绍了ClassLoader的重要性和其在类加载中的角色,以及类的热替换技术。

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

一、类加载过程:

类加载就是寻找一个类或者一个接口的字节码并通过解析该字节码来构造代表这个类或者这个接口的class对象的过程。在Java中,类加载器把一个类加载到虚拟机中,要经过三个步奏:加载、链接和初始化。而链接阶段又分成验证、准备和解析阶段。各步奏的主要工作内容如下:  

     1. 加载:查找和导入类或接口的字节码;

     2. 链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的; 

            校验:检查导入类或接口的二进制数据的正确性; 

            准备:给类的静态变量分配并初始化存储空间; 

            解析:将符号引用转成直接引用; 

    3. 初始化:激活类的静态变量的初始化 Java 代码和静态 Java 代码块。

那Java中如何进行类加载呢?
1)首先JVM启动,并做一些初始化工作,初始化完毕后,产生第一个类加载器,即BootstrapClassLoader, BootstrapClassLoader负责预先加载%JAVA_HOME%/jre/lib中的类,然后 会去加载sum.misc命名空间下的Launcher.java中的ExtClassLoader,把它的Parent设为null,让ExtClassLoader加载%JAVA_HOME%/jre/lib/ext中的类,并且,加载AppClassLoader,把AppClassLoader的Parent设为ExtClassLoader,让AppClassLoader加载$CLASSPATH中的类,至此,JVM中默认的类加载器初始化完毕。
2)带有main函数的类的主线程开启,此线程默认使用AppClassLoader作为线程上下文的类加载器。那么每次通过网络或者硬盘读入的类都会使用该类加载器进行类的加载。

那么我们来看一下各类类加载器具体是怎样工作的,它们的关系又是如何?

二、JDK默认的ClassLoader
JDK默认提供了如下几种ClassLoader
1.BootStrapClassLoader - JRE/lib/*
    Bootstrap加载器使用C++语言写的,负责加载%JAVA_HOME%/jre/lib中的类
2.ExtClassLoader - JRE/lib/ext/*.jar
    Bootstrap ClassLoader加载ExtClassLoader,并将ExtClassLoader的父加载器设置为Bootstrap,会加载%JAVA_HOME%/jre/lib/ext/*.jar以及java.ext.dirs中指定的类库。
3.AppClassLoader - CLASSPATH指定的所有jar和目录
    Bootstrap ClassLoader加载完ExtClassLoader之后,就会在家AppClassLoader,并将AppClassLoader的父加载器指定为ExtClassLoader,负责加载$CLASSPATH所指定位置的类或者jar包,在eclipse中就是.classpath文件中所指定的目录。

它们之间的关系如下图:

User-Defined ClassLoader是我们定义的类,它继承了ClassLoader抽象类并override抽象类的defineClass方法,就可以实现JVM提供的类加载器的运行模式,也就是双亲委派模型。这里指出一下,在JVM中,要唯一确定的一个类,需要类的全限定名以及加载此类的ClassLoader共同确定,也就是说,一个类被不同类加载器加载,那么它们在JVM中就是不同的两个类对象。

双亲委派模型
双亲委派模型的加载过程:
1.首先在当前类加载器ClassLoader中查询此类已经加载,如果已经加载则直接返回原来已经加载的类。Bootstrap, Extension和App ClassLoader会在JVM启动初始化时就根据对应加载路径将类加载到JVM中并放入缓存中。
2.如果当前ClassLoader的缓存中没有找到被加载的类的时候,则委托父类加载器加载,父加载器采取相同策略,一致到Bootstrap ClassLoader。
3.当所有的父类加载器都没有加载的时候,再由当前类加载器进行加载,并将其放入自己的缓存中,以便下次有加载请求时直接返回。

双亲委派模型的主要目的是为了保证Java中的所有基类都是同一个类,如java.lang.Object类,无论哪个加载器去加载该类,通过双亲委派机制,最终都会由Bootstrap ClassLoader去加载该类。就算我们自定义了一个java.lang.Object类,因为第一次加载不会在当前ClassLoader中有缓存,所以只能往父类加载器中寻找,最终在Bootstrap类加载器中有此类缓存,则自己定义的类不会起作用(当然你把自己的类放在Bootstrap加载路径上也是不行,因为同一个加载路径即空间命名不能出现两个同名的类)。这里说明一下,如果类中引用了另外一个类,即看到new关键字或者Class.forName方法,JVM会使用加载该类的类加载器去加载另外一个类。
我们可以从代码中看到forName方法的实现:
public static Class forName(String className)   
    throws ClassNotFoundException {   
        return forName0(className, true , ClassLoader.getCallerClassLoader());   
     }   
      
  /** Called after security checks have been made. */   
private static native Class forName0(String name, boolean initialize,   
    ClassLoader loader)   
    throws ClassNotFoundException;  

上图中的ClassLoader.getCallerClassLoader()方法就是得到当前调用Class.forName方法的类的类加载器。

那么我们如何自定义类加载器呢?
自定义ClassLoader
如果我们需要自定义ClassLoader,则需要通过继承java.lang.ClassLoader类,并override其中的findClass或loadClass方法实现即可,那么我们来看一下如何ClassLoader中的loadClass方法。

1.loadClass方法
loadClass有两种方法
protected Class<?> loadClass(String name)  throws ClassNotFoundException 
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException

其中loadClass(String name)方法中的实现是:
public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

那么其实实现就是在loadClass(String name, boolean resolve)中, 而在此方法的实现是:
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); //如果没有父类加载器,则委托bootstrap加载器加载
                    }
                } 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);  //如果父类加载器没有加载到,则通过自己的findClass来加载

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

上面很清晰的看到双亲委派机制是如何工作的(再啰嗦一遍)。
1、首先在当前ClassLoader中查询此类是否已经加载,如果直接加载则返回原来已经加载的类。
2、如果没找到则委托父类加载器加载,父类加载器采取相同的策略,首先查看自己的缓存,然后委托父类父类加载器,一直到bootstrap加载器。过程当然是AppClassLoader -> ExtClassLoader -> BootstrapClassLoader
3、如果所有父类加载器都没有加载,则再由当前加载器加载,并放入缓存
4、最后对加载的类进行验证解释,即link步奏
此方法没有被标记为final,则就可以被override了,换句话说是可以破坏双亲委派机制。同时,其中的findClass方法是使用双亲委派模型,用于实现自己的类加载器的。这是Java推荐的自定义方式。

2.findClass
protected Class<?> findClass(String name) 

它的内部实现是:
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

默认直接抛异常,这是因为使用上述双亲委派机制是不会加载不了类的。其实,此方法就是留给我们用来override的,可以从不同的地方读取class的二进制流,然后使用defineClass方法进行进一步的加载。但有个注意的地方:
    加载的class二进制流文件是需要游离在eclipse的sourcefolder以外的,否则,当JVM运行时会把该class二进制文件从AppClassLoader对象所寻找的路径即CLASSPATH中找到该二进制文件,从而导致findClass方法不可达,即不会触发自定义的ClassLoader,所以一般用于加载网络传输的Java类的字节代码。

3.defineClass
protected Class<?> defineClass(String name, byte[] b, int off, int len)

内部实现:
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(name, b, off, len, null);
    }

loadClass和findClass的区别:
如果需要使用双亲委派机制,只需重写findClass方法即可,如果需要抛弃双亲委派机制,则需要重写loadClass方法,一般用于热替换或者热部署。

那么我们使用自定义的ClassLoader来实现一个热替换。原理很简单,就是重新用一个ClassLoader实例加载目标类。
HopSwapClassLoader:继承了ClassLoader类
public class HopSwapClassLoader extends ClassLoader{
 
 //字节码目录
 private final static String CLASS_DIR_PATH = "D:/users/rich/Workspaces/MyEclipse 10/Test/bin/";

 @Override
 protected synchronized Class<?> loadClass(String name, boolean resolve)
   throws ClassNotFoundException {
  Class<?> clazz = findLoadedClass(name); //查找本ClassLoader是否已经加载过
 
  /**
   * 要对系统类进行处理,否则当需要加载系统类的时候会出错,可以尝试去掉下面这段代码
   */
  //----删除代码的分割线----
  if (name.startsWith("java.")) {
   ClassLoader system = ClassLoader
     .getSystemClassLoader(); //获取系统ClassLoader即AppClassLoader
   clazz = system.loadClass(name);
   
   if (resolve) {
    resolveClass(clazz);
   }
  }
  //---尝试的分割线----
 
  if (clazz == null) {
   byte[] data = getClassFileSize(name);
   clazz = defineClass(name, data, 0, data.length); //调用defineClass获得所需的类
  }
  return clazz;
 }
 
 /**
  * 返回class字节码文件数组
  * @param name
  * @return
  */
 private byte[] getClassFileSize(final String name) {
  File classFile = getCompleteFile(name);
 
  try {
   FileInputStream inputStream = new FileInputStream(classFile);
   ByteArrayOutputStream byteOutputStream =
     new ByteArrayOutputStream();
   int b;
   while((b = inputStream.read()) != -1) {
    byteOutputStream.write(b);
   }
   return byteOutputStream.toByteArray();
  } catch (Exception e) {
   e.printStackTrace();
  }
  return null;
 }
 
 /**
  *
  * @param name
  * @return
  */
 public File getCompleteFile(final String name) {
  String simpleClassName = CLASS_DIR_PATH +
    name.replace(".", "/") + ".class";
  File file = new File(simpleClassName);
  return file;
 }
}

TestHopSwapClassLoader: 测试类
public class TestHopSwapClassLoader {
 private final static String CLASS_NAME = "test.Test";
 
 public static void main(String[] args) {
  while(true) { //轮询
   
   HopSwapClassLoader classLoader = new HopSwapClassLoader(); //每次分配一个新的ClassLoader实例
   try {
    Class<?> clazz = classLoader.loadClass(CLASS_NAME);
    Object foo = clazz.newInstance();
    System.out.println(foo.getClass().getClassLoader()); //打印该类的ClassLoader
    //调用sayHello函数
    Method sayHello = foo.getClass()
      .getMethod("sayHello", new Class[]{});
    sayHello.invoke(foo, new Object[]{});
   
    Thread.sleep(1000);
   } catch (Exception e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   }
  }
 }
}

最后是一个Test类,就是我们需要加载的目标类,可以随意修改下面内容,就可以做到热替换了。
public class Test {
 
 public void sayHello() {
  System.out.println("version one");
 }
}

最后你可以用更加通用的URLClassLoader进行自定义的自己的ClassLoader。

如果有觉得不合理的地方,请指出,感谢支持。

参考:









评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值