Java 类加载器(一)

概念

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。

类加载器的特性

延迟加载

JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。

比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类别 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类别,因为静态方法会访问静态字段。而实例字段的类别需要等到你实例化对象的时候才可能会加载。

传递性

每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。

程序在运行过程中,虚拟机会使用调用者 Class 对象的 ClassLoader 来加载当前的类。

类加载器的作用

加载class

类加载的加载阶段的第一个步骤,就是通过类加载器来完成的,类加载器的主要任务就是“通过一个类的全限定名来获取描述此类的二进制字节流”,在这里,类加载器加载的二进制流并不一定要从class文件中获取,还可以从其他格式如zip文件中读取、从网络或数据库中读取、运行时动态生成、由其他文件生成(比如jsp生成class类文件)等。

加载.class文件的方式
  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件
确定类的唯一性

类加载器除了有加载类的作用,还有一个举足轻重的作用,对于每一个类,都需要由加载它的加载器和这个类本身共同确立这个类在Java虚拟机中的唯一性。也就是说,两个相同的类,只有是在同一个加载器加载的情况下才“相等”,这里的“相等”是指代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括instanceof关键字对对象所属关系的判定结果。

类加载器的分类

类加载器分为如下几种:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)和自定义类加载器(Customer ClassLoader),其中启动类加载器属于JVM的一部分,其他类加载器都用java实现,并且最终都继承自java.lang.ClassLoader。

启动类加载器(Bootstrap ClassLoader)

是由C/C++编译而来的,看不到源码,所以在java.lang.ClassLoader源码中看到的Bootstrap ClassLoader的定义是native的“private native Class findBootstrapClass(String name);”。启动类加载器主要负责加载JAVA_HOMElib目录或者被-Xbootclasspath参数指定目录中的部分类,具体加载哪些类可以通过“System.getProperty(“sun.boot.class.path”)”来查看。

扩展类加载器(Extension ClassLoader)

由sun.misc.Launcher.ExtClassLoader实现,负责加载JAVA_HOMElibext目录或者被java.ext.dirs系统变量指定的路径中的所有类库,可以用通过“System.getProperty(“java.ext.dirs”)”来查看具体都加载哪些类。

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

由sun.misc.Launcher.AppClassLoader实现,负责加载用户类路径(我们通常指定的classpath)上的类,如果程序中没有自定义类加载器,应用程序类加载器就是程序默认的类加载器。

自定义类加载器(Customer ClassLoader)

JVM提供的类加载器只能加载指定目录的类(jar和class),如果我们想从其他地方甚至网络上获取class文件,就需要自定义类加载器来实现,自定义类加载器主要都是通过继承ClassLoader或者它的子类来实现,但无论是通过继承ClassLoader还是它的子类,最终自定义类加载器的父加载器都是应用程序类加载器,因为不管调用哪个父类加载器,创建的对象都必须最终调用java.lang.ClassLoader.getSystemClassLoader()作为父加载器,getSystemClassLoader()方法的返回值是sun.misc.Launcher.AppClassLoader即应用程序类加载器。

ClassLoader与双亲委派模型

类加载器java.lang.ClassLoader中的核心逻辑loadClass()方法:


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) {
           }

           if (c == null) {//如果上面用父加载器还没加载到类,就自己尝试加载
               long t1 = System.nanoTime();
               c = findClass(name);
               sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
               sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
               sun.misc.PerfCounter.getFindClasses().increment();
           }
       }
       if (resolve) {
           resolveClass(c);
       }
       return c;
   }
}


双亲委派模式是在Java1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

在这里插入图片描述

双亲委派模式优势
  • 避免类的重复加载
    采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

  • 安全
    java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

双亲委托模式的弊端

判断类是否加载的时候,应用类加载器会顺着双亲路径往上判断,直到启动类加载器.但是启动类加载器不会往下询问,这个委托路线是单向的,即顶层的类加载器,无法访问底层的类加载器所加载的类。

类加载过程

ClassLoader 里面有三个方法 loadClass()findClass()defineClass()

loadClass()

loadClass() 方法是加载目标类的入口,它首先会首先调用 findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类。

findClass()

如果父类加载器无法加载该类的话,就调用 findClass()方法来查找该类

defineClass()

类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。

自定义类加载器注意点

为了不破坏双亲委派,我们在自定义类加载器时最好不要覆写 loadClass()方法,而是覆写 findClass()方法。否则可能会导致自定义加载器无法加载内置的核心类库。。

Java线程上下文类加载器与SPI

线程上下文类加载器(context class loader)是从JDK 1.2开始引入的。类 java.lang.Thread中的方法getContextClassLoader()和setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。

为了加载类,Java还提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

Class.forName

当我们在使用 jdbc 驱动时,经常会使用 Class.forName 方法来动态加载驱动类。

    
    Class.forName("com.mysql.cj.jdbc.Driver");

该forName 方法使用调用者 Class 对象的 ClassLoader 来加载目标类。


public static Class<?> forName(String className)
            throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

通过下面形式的 forName 方法可以使用自定类加载器,允许我们自由加载其它任意来源的类库。


public static Class<?> forName(String name, boolean initialize,ClassLoader loader)
    throws ClassNotFoundException
{
    Class<?> caller = null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        // Reflective call to get caller class is only needed if a security manager
        // is present.  Avoid the overhead of making this call otherwise.
        caller = Reflection.getCallerClass();
        if (sun.misc.VM.isSystemDomainLoader(loader)) {
            ClassLoader ccl = ClassLoader.getClassLoader(caller);
            if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                sm.checkPermission(
                    SecurityConstants.GET_CLASSLOADER_PERMISSION);
            }
        }
    }
    return forName0(name, initialize, loader, caller);
}

Class.forName和classloader的区别

Class.forName除了将类的.class文件加载到jvm中之外,还会对加载的类进行初始化,但是我们可以设置initialize来决定是否初始化。

而classloader只干一件事情,就是将.class文件加载到jvm中,不会对加载的类进行初始化,只有在newInstance才会去初始化。

Tomcat中的类加载器

Servlet类加载器的规范

每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。

Tomcat类加载器的结构

在Tomcat目录结构中,有三组目录(“/common/”,“/server/”和“shared/”)可以存放公用Java类库,此外还有第四组Web应用程序自身的目录“/WEB-INF/”,把java类库放置在这些目录中的含义分别是:

  • 放置在common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
  • 放置在server目录中:类库可被Tomcat使用,但对所有的Web应用程序都不可见。
  • 放置在shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
  • 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
    在这里插入图片描述

灰色背景的3个类加载器是JDK默认提供的类加载器,这3个加载器的作用前面已经介绍过了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 则是 Tomcat 自己定义的类加载器,它们分别加载 /common/、/server/、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 类库。其中 WebApp 类加载器和 Jsp 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器。

从图中的委派关系中可以看出,CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

热部署

通过上面内容我们可以总两方面来考虑热部署的问题:

  1. 类加载器分为应用类加载器和核心类库加载器,我们的代码主要是在AppClassLoader中加载。
  2. 在同一个类加载器中每个类只能加载一次。

根据以上两点我们要实现热部署就需要重新创建一个应用类加载器将classpath下的类进行加载,并将原来的类加载器被清除,进行gc。

详细设计可以查看DevTools源码。

参考:《深入理解java虚拟机》

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值