JVM:Java 类的加载机制

时间:2021-06-12 06:26:42

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,转换,解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。

类的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用、卸载七个阶段。其中验证、准备和解析三个部分统称为连接。

JVM:Java 类的加载机制

  • 加载:加载是类加载的第一个阶段,这个阶段,首先要根据类的全限定名来获取定义此类的二进制字节六,讲字节六转化为方法区运行时数据结构。在 Java 堆生成一个代表这个类的 java.lang.class 对象,作为方法区的访问入口。

  • 验证:这一步的的目的是确保 class 文件的字节六包含的信息符合虚拟机的要求。

  • 准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都会在方法区中进行分配。仅仅是类变量,不包括实例变量。

    public static int value = 123;

    变量在准备阶段过后的初始值为0而不是123,123的赋值要在变量初始化以后才会完成。

  • 解析:虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 初始化:初始化是类加载的最后一步,这一步会根据程序员给定的值去初始化一些资源。

加载是我们使用一个类的第一步,加载是如何完成的那?

类加载器

虚拟机设计团队把类加载阶段中的通过一个类的全限定名来获取描述此类的二进制字节流这个动作放到 Java 虚拟机外部去实现,以便让程序自己去决定如何获取所需要的类,这个动作的代码模块称为类加载器

对于一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,比较两个类是否相等需要在这两个类是由同一个类加载器加载的前提下才有意义。

  • 启动类加载器(Bootstrap ClassLoader):这个类负责将 \lib 目录中的类库加载到内存中,启动类加载器无法被 Java 程序直接引用。
  • 扩展类加载器(Extension ClassLoader):负责加载 \lib\ext 目录中的类。开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):这个类加载器是 ClassLoader 中 getSystemClassLoader() 方法的返回值,所以一般称为系统类加载器。如果没有自定义过加载器,一般情况下这个就是默认的类加载器。
  • 自定义类加载器(User ClassLoader):通过自定义类加载器可以实现一些动态加载的功能,比如 SPI。

双亲委派模型

JVM 在加载类时默认采用的是双亲委派模型机制。通俗地讲,某个特定的类加载器在接到加载类的请求时,首先讲加载任务委托给父类加载器,因此所有加载请求最总都应该传送到顶层的启动类加载器中。如果父类无法完成加载请求,子类才会尝试自己加载。

所以,越基础的类会由越上层的加载器加载。

如果不使用双亲委派模型,用户自己写一个 Object 类放入 ClassPath,那么系统中将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无从保证。现在你可以尝试自己写一个名为 Object 的类,可以被编译,但永远无法运行。因为最后加载时都会先委派给父类去加载,在 rt.jar 搜寻自身目录时就会找到系统定义的 Object 类,所以你定义的 Object 类永远无法被加载和运行。

JVM:Java 类的加载机制

双亲委派模型的好处是保证了核心类库不配覆盖和篡改。

打破双亲委派模型

双亲委派模型并不是一个强制性的约束模型,而是 Java 设计者推荐给开发者的类加载器实现方式。Java 世界中大部分的类加载器都遵循这个模型。

双亲委派模型第一次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为他们总是作为被用户代码调用的 API。那如果基础类又要调用回用户的代码,怎么办?

比如 JNDI 服务,JNDI 现在是 Java 的标准服务,他的代码由启动类加载器去加载(rt.jar),但 JNDI 需要由独立厂商实现并部署在应用程序的 Class Path 下的 JNDI 接口提供者的代码,启动类加载器不可能认识这些代码,因为启动类加载器的搜索范围找不到用户应用程序类。

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

有了这个线程上下文加载器,JNDI 服务使用线程上下文加载器去加载所需要的 SPI 代码。也就是父类加载器请求子类加载器完成类加载的动作,这就打破了双亲委派模型。典型的例子有 JNDI 和 JDBC 等。

Tomcat 类加载机制

Tomcat 的类加载机制是违反了双亲委派原则的,对于一些为加载的非基础类(Object,String)等,各个 web 应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给 commonClassLoader 走双亲委派模型。整体结构如下:

JVM:Java 类的加载机制

这其中 JDK 提供的类加载器分别是:

  • Bootstrap-启动类加载器,JVM 的一部分,加载 <JAVA_HOME>/lib/ 目录下特定的文件
  • Extension-扩展类加载器,加载 <JAVA_HOME>/lib/ext/ 目录下的类库。
  • Application-应用程序类加载器,也叫系统类加载器,加载 CLASSPATH 指定的类库。

Tomcat 自定义类加载器分别是:

  • Common - 父加载器是 AppClassLoader,默认加载 ${catalina.home}/lib/ 目录下的类库
  • Catalina - 父加载器是 Common 类加载器,加载 catalina.properties 配置文件中 server.loader 配置的资源,一般是 Tomcat 内部使用的资源
  • Shared - 父加载器是 Common 类加载器,加载 catalina.properties 配置文件中 shared.loader 配置的资源,一般是所有 Web 应用共享的资源
  • WebappX - 父加载器是 Shared 加载器,加载 /WEB-INF/classes 的 class 和 /WEB-INF/lib/ 中的 jar 包
  • JasperLoader - 父加载器是 Webapp 加载器,加载 work 目录应用编译 JSP 生成的 class 文件

JDBC 为什么要破坏双亲委派模型?

在 JDBC 4.0 之后我们需要使用 Class.forName 来加载驱动程序了,我们只需要把驱动的 jar 包放到工程的类加载路径里,那么驱动就会被自动加载。

这个自动加载采用的技术叫 SPI,可以看一下jar包里面的META-INF/services目录,里面有一个java.sql.Driver的文件,文件里面包含了驱动的全路径名。我们只需要下面这一句话就可以创建数据库连接:

Connection con =
DriverManager.getConnection(url , username , password ) ;

因为类加载器受到加载范围的限制,在某些情况下父类加载器无法加载到所需要的文件,这时候就需要委托子类加载器去加载 class 文件

JDBC 的 Driver 接口定义在 JDK 中,其实现由各个数据库服务商来提供,比如 MySQL 的驱动包。DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager位于 $JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载,而其Driver接口的实现类是位于服务商提供的 Jar 包,根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。也就是说 Bootstrap 类加载器还要去加载 jar 包中的 Driver 接口的实现类。Bootstrap 只负责 /lib/rt.jar 里面所有的 class,所以需要子类加载器去加载 Driver,这就破坏了双亲委派模型。

查看 DriverManager 类的源码,看到使用 DriverManager 的时候会触发其静态代码块,调用 loadInitialDrivers() 方法,并调用 ServiceLoader.load(Driver.class) 加载所有在META-INF/services/java.sql.Driver 文件里边的类到JVM内存,完成驱动的自动加载。

    static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
} private static void loadInitialDrivers() { AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
}); }
    public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

这个子类加载器是通过 Thread.currentThread().getContextClassLoader() 得到的上下文加载器。

public Launcher() {
...
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
...
}

可以看到,在 sun.misc.Launcher 初始化的时候,会获取AppClassLoader,然后将其设置为上下文类加载器,所以线程上下文类加载器默认情况下就是系统加载器

Tomcat 为什么要破坏双亲委派模型

每个 Tomcat 的 webappClassLoader 加载自己目录的 class 文件,不会传递给父类加载器

事实上,Tomcat 之所以造出了一堆自己的 classLoader,大致是出于三个目的:

  1. 对于各个 weapp 中的 class 和 lib,需要相互隔离,不能出现一个应用中加载的类影响另一个应用的情况。
  2. 与 JVM 一样出于安全考虑,使用单独的 classLoader 去装载 Tomcat 自身的类库,防止恶意或者无意的破坏。
  3. 热部署。