[JVM] 类加载机制

本文详细解析了Java类加载过程中的关键概念,包括类相等性的判断标准、类加载的不同阶段(如准备与初始化)、双亲委托模型及其工作原理、自定义类加载器的实现方式,以及热部署的技术方案。

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

判定两个类是否相等

在虚拟机中被加载类的类加载器被加载的类全路径都相等的情况下,这两个类才能称上是相等的。

类加载过程

这里写图片描述

重点了解下准备和初始化两部分:

  • 准备:
    这个阶段为静态变量分配内存并设置初始值,当一个类的静态变量定义如下:

    public static int value = 5;

    那么在准备节点这个value的值是0,因为此时尚未执行任何Java方法。

  • 初始化:

    初始化时类加载过程的最后一步,虚拟机讲给会初始化变量(包括静态变量和实例变量),对有静态类变量或静态代码块的类,虚拟机会自动隐式添加个<clinit>()类构造器方法,并先于实例构造方法执行,这个方法是线程安全的,如

    public class Demo {
      public static int value = 5;
      static{
          System.out.println("Hello World.");
      }
      public Demo(){
          System.out.println("Demo init.");
      }
    }

    在执行初始化时value等于5,且输出“Hello World.”,然后才会执行实例构造器方法输出“Demo init.”。注意的是类构造器<clinit>()在上面的生命周期内只会执行一次。

双亲委托模型

在Java虚拟机中,存在以下几种类加载器:

  • 启动类加载器(BootStrap ClassLoader)

    该加载器由C++实现,其它的类加载器都是由JAVA实现,负责加载<JAVA_HOME>\lib目录中的类库,它的实例无法被用户获取

  • 拓展类加载器(Extension ClassLoader)

    由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\etx目录中的类库

  • 应用程序类加载器(Application ClassLoader)

    sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径(ClassPath)上所指定的类库

  • 自定义类加载器

    这个由用户通过继承ClassLoader实现,可以根据需求从其它地方如网络等加载指定类库。

双亲委派模型工作过程:当系统需要一个类时,先从底层向上开始查找这个类是否加载,如果知道顶层的启动类加载器都没法找到这个类,就尝试从顶层向下开始加载直到成功。示意图如下:

这里写图片描述

对应的,看下其加载实现的源码:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查这个类是否加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {//未加载过
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {//如果有父加载器,尝试使用父加载器加载
                        c = parent.loadClass(name, false);
                    } else {//没有父加载器,也就是启动类加载器或者双亲为启动类加载器,则使用启动加载器加载
                        c = findBootstrapClassOrNull(name);
                    }
                } 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);//如果父加载器都不能加载,则使用当前加载器加载

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

至于为什么使用的双亲委派模型,如Object类如果可以被自定义加载器所加载,这个本身也能被启动类加载器加载,那么这个必然导致有多个Object类,导致混乱。

线程上下文加载器

SPI服务接口,因为这服务的接口是Java核心库的一部分,是由启动类加载器加载的;但是它们的实现类确实由各个厂商实现的,类库是放在ClassPath下面,由应用类加载器去加载;常见的例子如JDBC。现有的双亲委派模型就无法解决这个问题,因为启动类加载器不能交给应用程序类加载器去加载实现类。这种方式的实现是通过引入线程上下文加载器(ContextClassLoader)来实现的

自定义类加载器

但是我们可以通过自定义加载器实现,来看下自定义继承的抽象类ClassLoader,主要接口如下:

  • public Class<?> loadClass(String name) throws ClassNotFoundException

    加载指定类名的类,返回这个类的Class实例。找不到则抛出异常

  • protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError

    根name为类名,字节流b的[off,off+len-1]区间为定义了实际的class信息的字节流

  • protected Class<?> findClass(String name) throws ClassNotFoundException

    自定义类加载器时自定义查找Class的方法

  • protected final Class<?> findLoadedClass(String name)

    查找已加载类的方法,注意此方法final修饰不可修改。

如果要符合双亲委派规范,则重写findClass方法改变类的查找逻辑,如从网络加载类等而不是从ClassPath;要破坏的话,重写loadClass方法(双亲委派的具体逻辑实现)

下例源码从指定目录加载一个类

public class Greeting {

   static {
        System.out.println("Hello World.");
    }
}

public class MyClassLoader extends ClassLoader {

    private String classPathPath;

    protected MyClassLoader(String classPathPath) {
        this.classPathPath = classPathPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getData(String name) {
        String path = classPathPath + name;
        try {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int num = 0;
            while ((num = is.read(buffer)) != -1) {
                stream.write(buffer, 0, num);
            }
            return stream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    public static void main(String[] args) throws Exception {

        MyClassLoader mcl = new MyClassLoader("F:/");
        Class cls = mcl.loadClass("Greeting");
        cls.newInstance();
    }
}

将Greeting.class放到F盘根目录下即可,执行时便能看到控制台输出“Hello World.”

热部署

热部署(hotswap)含义是指在不重启Java虚拟机的前提下,自动侦测到Class的变化,并将变化的Class进行。Java类通过虚拟机加载后生成Class对象,随后就可以创建该类的实例,但是虚拟机并不会去更新正在运行的Class,故常规下是没法实现热部署的。

但是我们可以通过实现自定义加载器来实现热部署,思路如下:

  1. 监听Class的变化
  2. 销毁自定义ClassLoader对象(被该加载器加载的Class也会自动卸载)
  3. 创建新的ClassLoader对象去加载Class
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值