类加载即将.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的类也不会互相访问。