JVM之类加载

322 阅读7分钟

类加载即将.class文件加载到JVM中,成为程序可用的Class对象的过程。

5个阶段

类加载主要包含一下几个阶段:

  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化

其中,验证,准备,解析三个阶段又合称为连接。

加载:将.class文件字节码加载到JVM的过程。

验证:验证字节码文件是否符合JVM规范。比如文件魔数等

准备:为类变量申请内存空间并初始化赋值。

1.类变量指的是类的静态变量(static修饰)和常量(static final修饰)

2.初始化赋值。如果是静态变量,会初始化为类型的零值,比如boolean是false,int是0,引用类型是null;如果是常量,则初始化为代码中指定的值。

解析:将符号引用转化为直接引用。

初始化:执行类的构造器的过程。注意,类的构造器不是生成实例的构造方法。

类的构造器由类中的静态变量的声明赋值以及静态代码块构成的。执行顺序按照代码中的前后顺序。值得注意的是,静态代码块中的代码只能访问之前的变量,对于之后的变量,只能赋值,不能访问。比如:

static a = 0;
static {
    b = 1; //可以对后出现的变量进行赋值
    a = b; //不可以访问后出现的变量b,错误
}
static b = 0;

类加载的5个阶段中,我们可以干预的一般是加载和初始化两个阶段。初始化执行的是我们类中的相关代码,加载则通过类加载器来进行干预。

类初始化的时机

JVM规范中没有明确的规定类加载的时机,即这点一般是由各虚拟机实现自己决定的。但是规定了类的初始化的时机。而类的加载,连接需要在初始化之前执行。

类的初始化在且只能在以下几种情况下进行初始化。

  • 遇到new getstatic putstatic invokestatic字节码指令时。即创建实例对象,访问或者赋值静态属性,调用静态方法
  • 通过反射对类进行访问或者创建对象时
  • 初始化子类时先初始化父类(第一个初始化的类是Object)
  • 程序的入口main方法,虚拟机要初始化这个类(由上一条,也是先初始化它的父类)
  • 动态语言支持场景

类的加载

我们来着重分析一下类加载过程中的第一个阶段:加载。

类的加载是通过类加载器实现的。

类加载器

类加载可以分为两类:启动类加载器和其他类加载器。

启动类加载器是由C++写的,由JVM处理的。是唯一没有父加载器的。

其他类加载器都在java代码中。都继承了ClassLoader抽象类。都有父类加载器(ClassLoader抽象类的parent属性)。如果父类加载器是启动类加载器,则用parent为null表示,比如扩展类加载器。

常见的类加载器有:

  • 启动类加载器:BootstrapClassLoader 加载rt.jat下的类
  • 扩展类加载器:ExtClassLoader 加载lib/ext/下的类
  • 应用类加载器:AppClassLoader,又称系统类加载器 加载classpath下的类

双亲委派模型

前边我们讲到除了启动类加载器,其他的类加载器都有父类加载器(通过组合的方式而不是继承的方式)。双亲委派模型指的是当一个类加载器要加载一个类时,不是立即加载,而是先找它的父类加载器执行加载动作。如果父类加载器不能加载时,才会自己进行加载。

优点:

  • 安全性。天然的保证一些类的唯一性,比如String,Object只能被BootstrapCL加载。
  • 层次性。根据不同类加载器进行的加载,使得类天然的具备了一种层次性。

缺点:

不够灵活。

源码如图:

1中如果parent不是null,用parent加载;如果是null,代表parent是BootstrapCL,用BootstrapCL加载,这种情况下一般是ExtClassLoader。

2中才自己进行加载。因此我们一般实现findClass即可。而findClass是被loadClass调用的,loadClass已经保证了双亲委派机制。

打破双亲委派

SPI

SPI全称service provider interface。一般是JDK中提供接口,具体实现是由各厂家实现的,比如JDBC。

由于SPI接口在JDK中,一般是由BootstrapClassLoader进行加载的,它此时不仅要加载该接口,还要扫描并加载全部的实现类。如果是正常情况下,会采用调用者的类加载器进行加载,也即使用BootstrapCL加载。但是我们前边讲到BoostrapCL只能加载rt.jar下边的类。而具体的厂家实现肯定不再rt.jar下边,这样就加载不了了。那么它是怎么处理的呢?

答案是线程上下文类加载器。这里不再使用默认情况下调用者的类加载器,而是取线程中的上下文类加载器,默认是应用类加载器。

下边我们看下源码:

热部署(OSGi)

暂略

Tomcat的类加载

主流的java web服务器一般都要实现自己的类加载器。它们需要解决以下几个问题:

  • 部署在同一个web服务进程中的不同的web服务间要能够实现类库隔离;
  • 部署在同一个web服务进程中的不同的web服务以及tomcat本身要可以类库共享;
  • jsp要能够热切换

简单即:能隔离,能共享,能热切换。

为此,tomcat实现了自己的一些类加载器。如图:

除了JVM和JDK本身提供的Bootstrap,Ext,App外,还有如下:

  • CommonClassLoader:加载tomcat及各web应用共享的类库 一般在common/*目录下
  • CatalinaClassLoader:加载tomcat自身的类库,不能被各web应用访问,一般在server/*下
  • SharedClassLoader:加载各web应用共享的类库 在/shared/*下
  • WebAppClassLoader:加载自己所负责的应用的类库,每个web应用对应一个WebAppClassLoader 一般在webApp/WEB-INF/*下
  • JasperLoader:一个类加载只加载一个jsp编译后的class文件。如果发生变化,就卸载classLoader,生成新的classLoader并重新加载新的jsp class文件。

注意,子类加载器加载的类是可以访问父类加载器能加载的类的,反之则不可以。

原因:比如有两个class loader,一个ChildClassLoader,一个是ParentClassLoader,分别加载了A和B两个类。

假设A使用到了B,那么当使用A时会触发B的加载。而这个加载过程是交给A的类加载器即ChildClassLoader来加载的。因此B也将由ChildClassLoader来加载,而ChildClassLoader可以通过双亲委派机制委托ParentClassLoader完成对B的加载。

反之,如果B使用到了A。那么当使用B时会触发A的加载。而B是由ParentClassLoader来加载的,因此A也会尝试由ParentClassLoader来加载。这是行不通的。

正常情况下,不会出现“反之”的情况,除非SPI或者其他特殊情况。而此时就需要使用线程上下文类加载器来打破双亲委派。

根据上边的讨论,tomcat加载结构中,jsp对应的class是可以访问WebAppClassLoader加载的类的(也即应用程序的类),而WebAppClassLoader的类也可以访问CommonClassLoader的类,而CommonCL的类又可以访问APPClassLoader的类。而反过来,CommonClassLoader的类是不会访问WebAppClassLoader的类的。除此之外,不同的WebAppClassLoader的类也不会互相访问。