类加载机制
- 一、背景知识补充
- 二、类加载过程/机制
- 1、浅层理解
- 2、大致步骤
- 3、具体步骤
- (3.1)装载loading:查找和导入相应的class文件
- (3.2)链接linking:把类的二进制数据合并到JRE中
- (3.3)初始化initializing:对类的静态变量,静态代码块执行初始化操作,赋初始值
- 三、类的初始化
- 1、类的初始化步骤
- 2、原因
- 四、类加载器
- 1、双亲委派机制定义
- 2、双亲委派机制存在的意义
- 3、类加载器分类
- 4、四种类加载器间关系
- 5、源码解读
- 6、Tomcat的加载机制
一、背景知识补充
1、java执行图序
2、JVM内存模型
3、类变量
类变量,也叫静态变量,用static标识
二、类加载过程/机制
1、浅层理解
类加载:java虚拟机将Class字节码文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型
2、大致步骤
1.找到对应的class文件,解析并校验文件内容是否符合
2.校验通过之后load进jvm虚拟机内存中为当前的java类创建一个相应的Class对象并初始化
3、具体步骤
类装载器把一个类装入jvm中,要经过以下三大步
(3.1)装载loading:查找和导入相应的class文件
1.1 通过一个类的全限定名来获取定义此类的二进制字节流
1.2 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
1.3 在java堆中生成一个代表这个类的java.lang.Class对象,通过该对象访问方法区中的这些数据
(3.2)链接linking:把类的二进制数据合并到JRE中
2.1 校验-verification 检查载入class文件数据的正确性
- 文件格式验证 CA、FE、BA、BE;
- 元数据验证;
- 字节码验证;
- 符号引用验证。
2.2 准备-preparation 给类的静态变量分配存储空间 赋默认值【零值】
- 只包含static 修饰的类变量,会分配在方法区
- 不包含使用final修饰的static,因为final在编译的时候分配。
原因:常量是一种特殊的变量,**在编译器把他们当做值(value) 【直接初始化了变量名和对应的值在字节码中】,而不是域(field)来对待。**若代码中用到了常变量,编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种有用的优化,但是如果需要改变final域的值,那么每一块用到这个域的代码都需要重新编译。
2.3 解析-resolution:虚拟机将常量池内的符号引用替换为直接引用的过程
- 符号引用(Symbolic Reference) : 以一组符号来描述所引用的目标类。
- 直接引用(Direct Reference): 可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
【直接引用与虚拟机实现的内存布局相关,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不相同,若存在了直接引用,则引用的目标必定已经在内存中存在的。】
1). 类或者接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析
2). 字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,若果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束
3). 类方法解析: 对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,且对类方法的匹配搜索,是先搜索父类,再搜索接口
4). 接口方法解析:与类方法解析步骤类似,只是接口不会有父类,因此只递归向上搜索父接口就行
(3.3)初始化initializing:对类的静态变量,静态代码块执行初始化操作,赋初始值
定义:初始化阶段就是开始执行类中定义的java程序代码、执行类构造器方法()。
具体工作:JVM负责对类进行初始化,主要对类变量static赋正确的初始值,在java中对类变量进行初始化的两种方式:
- 声明变量时指定初始值
- 使用静态代码块为类变量指定初始值
三、类的初始化
1、类的初始化步骤
1 初始化父类的静态成员
2 初始化父类的静态代码块
3 初始化子类的静态成员
4 初始化子类的静态代码块
5 初始化父类的非静态成员
6 初始化父类的非静态代码块
7 初始化父类的构造方法
8 初始化子类的非静态成员
9 初始化子类的非静态代码块
10 初始化子类的构造方法
2、原因
1.类构造器方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块) 中的语句合并产生的。
- 编译器收集的顺序由语句在源文件中出现的顺序所决定
2.类构造器方法与类的构造函数不同,它不需要显示地调用父类构造器。虚拟机会保证在子类的类构造器方法执行之前,父类的类构造器方法已经执行完毕,因此在虚拟机中第一个执行的类构造器方法的类是java.lang.Object
3.接口中可能会有变量的赋值操作,因此接口也会生成类构造器方法,但是接口与类不同,执行接口的类构造器方法不需要先执行父接口的类构造器方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另:接口实现类在初始化时也不会执行接口的类构造器方法
4.虚拟机会保证一个类的类构造器方法在多线程环境中被正确地加锁和同步。若有多个线程同时初始化一个类,那么只会有一个线程去执行这个类的类构造器方法,其他线程都需要阻塞等待,直到活动线程执行类构造器方法完毕。
思考:如果这个线程已经执行完该类构造器方法构建类完成,还需要其他线程再调用该类构造器方法吗?
四、类加载器
1、双亲委派机制定义
- 双亲委派机制定义:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
2、双亲委派机制存在的意义
- 通过委派的方式,避免类的重复加载。当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
- 通过双亲委派的方式,保证安全性。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的(除非有人跑到你的机器上, 破坏你的JDK)。因此,可以避免有人自定义一个有破坏功能的java.lang.Integer被加载,有效的防止核心Java API被篡改。
3、类加载器分类
- BootStrap ClassLoader:由C++语言实现启动,加载Java核心类库 /lib/rt.jar。属于虚拟机自身的一部分。
- Extension ClassLoader:扩展类加载器(抽象类) jre/lib/ext.jar
- Application ClassLoader:应用类加载器,加载classpath下的所有类。独立于jvm外部,继承自抽象类java.lang.ClassLoader。
- Custom ClassLoader:自定义类加载器,可加载指定路径的class文件。独立于jvm外部,继承自抽象类java.lang.ClassLoader。
4、四种类加载器间关系
类加载时,虽然用户自定义类不会由bootstrap classloader或是extension classloader加载(由类加载器的加载范围决定),但是代码实现还是会一直委托到bootstrap classloader, 上层无法加载,再由下层是否可以加载,如果都无法加载,就会触发findclass,抛出classNotFoundException.
5、源码解读
简单总结实现流程:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载器请求都会传给顶层的启动类加载器,只有当父类加载器反馈自己无法完成该加载请求(父类加载器未找到对应的类),子加载器才会尝试自己去加载。
- 首先判断该类是否已经被加载
- 该类未被加载,如果父类不为空,交给父类加载
- 如果父类为空,交给bootstrap classloader 加载
- 如果类还是无法被加载到,则触发findclass,抛出classNotFoundException(findclass这个方法当前只有一个语句,就是抛出classNotFoundException),如果想自己实现类加载器的话,可以继承classLoader后重写findclass方法,加载对应的类)。
源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 查找加载 class文件
Class<?> c = findLoadedClass(name);
//
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 优先由 父加载器加载
c = parent.loadClass(name, false);
} else {
// parent == null 时由 BootstrapClassLoader 加载
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;
}
}
6、Tomcat的加载机制
双亲委派机制有他存在的意义,不过也存在许多场景是需要破坏这个机制的。比如 tomcat web容器里面部署了很多的应用程序,但是这些应用程序对于第三方类库的依赖版本却不一样,这些第三方类库的路径又是一样的,如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。所以,Tomcat破坏双亲委派原则,提供隔离机制,为每个web容器单独提供一个WebAppClassLoader加载器。优先加载 Web 应用自己定义的类,没有遵照双亲委派的约定,每一个应用有自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。
参考文献:
[1]: java类加载机制