笔记标签:类加载、类加载器、双亲委派、打破双亲委派
笔记大纲
- 类加载概念和类加载器
- 类的生命周期
- 类加载器
- 双亲委派机制
- 自定义类加载器
1.类加载机制
我们在使用一个Java类的时候,比如使用累的静态属性或者new一个对象,JVM虚拟机会将我们目标类的.class文件加载到JVM特定的内存区域,然后经过一系列的加载流程,最后我们的程序才能够使用,这整个流程称之为类加载机制,也就是说我们要使用一个类,必须先加载才能使用。当然,一个类的声明周期不单单只是加载,还有许多阶段,例如初始化、卸载等操作。
JVM类加载是通过一系列的类加载器进行实现,类加载器具有加载一个.class的文件能力,不同的类加载器有着不同的效果。
如图所示,我们要使用自己写的com.minor.test类,则JVM需要经历很多阶段,每个阶段很重要。
2.类的生命周期 / 类加载过程
2.1 类的生命周期总览(7步,加载5步)
2.1.1 阶段顺序
类加载的5步中,加载、校验、准备、初始化这几个阶段是严格顺序的,但是解析阶段不一定,因为Java支持运行时动态解析绑定(静态绑定、动态绑定)。
2.1.2 加载的时机
懒加载是JVM加载类的一个策略,当程序需要用到类的时候才回去检查和加载。
2.1.3 类加载是线程安全的
JVM会保证一个类的加载时线程安全的。
2.2 加载 loading
1. 根据要加载的类的全限定名获取到该类的二进制文件,通过磁盘I/O转化为字节流。
1. 将字节流信息结构转化为方法区的运行时数据结构。
1. 在JVM堆内存中创建一个该类的`java.lang.Class`对象,作为方法区类元数据的访问入口。
2.3 校验 / 验证 verification
校验阶段是类加载连接阶段的第一步,目的是检查class文件是否符合JVM规范要求。其中,校验阶段一共分为4个校验点:
2.3.1 文件格式验证
验证clsss文件字节流是否符合JVM规范,并支持当前JVM版本,例如:
- 文件是否是cafe babe开头
- 主版本号和副版本号是否支持当前JVM版本
- 常量是否有不符合格式的定义,等等...
2.3.2 元数据验证
字节码文件描述信息的语义是否符合Java语言语法规范,例如:
- 这个类是否继承了final修饰的类
- 是否是抽象类,是的话检查是否实现了父类或接口的方法
- 重写、重载等是否有语法错误,等等...
2.3.3 字节码验证
通过数据流分析、控制流分析,判断程序语义是否合法,保证不会危害JVM安全,例如:
- 跳转指令符合跳转规则
- 类型转换是否符合Java规范,等等...
2.3.4 符号引用验证(在解析阶段完成)
检查符合引用的合法性,例如符号引用是否能指向目标、符号引用是否可访问等问题
2.4 准备 preparation
准备阶段会为类中定义的静态变量分配内存并设置初始值,基本类型就赋值默认值,引用类型就是null。 但是,如果是final修饰的静态变量,且是基本类型和String且赋值时字面量形式,在准备阶段会赋值真实值,如果不是字面量形式,那么会在初始化赋值。
2.5 解析 resolution
解析阶段是将JVM常量池类信息的符号引用替换为直接引用过程,该阶段会将一些静态方法替换为指向数据所真实存在的内存地址,称之为静态链接 or 静态绑定,动态绑定则是在程序运行时动态完成。
2.6 初始化 initialzation
初始化阶段主要是对class中定义的静态块和静态变量赋予真实值。JVM规范中定义了6种情况必须对类进行初始化操作,包括:
1. 使用new关键字实例化对象时,读取 or 设置一个类的静态变量时,调用静态方法时。
1. 使用java.lang.reflect包进行反射时。
1. 发现父类没有初始化时,会初始化父类。
1. 程序启动类main方法所在类,严格触发初始化。
1. JDK7后,使用java.lang.invoke.MethodHandle句柄操作类时。
1. 当接口中存在default修饰符时,且实现类发生了初始化时。
3.类加载器
类加载过程的执行者就是一个称为类加载器的东西处理的(5个步骤),JDK提供了三层类加载器:
3.1 引导类加载器 Bootstrap ClassLoader
任何类加载的行为都要经过它,他负责加载java核心类库:rt.jar、resources.jar、charsets.jar等,也可以通过JVM指令进行指定加载路径:-Xbootclasspath,我们在Java程序中打印rt.jar包下面的类的加载器是显示null,是因为这个引导类加载器是C++语言构造。
3.2 扩展类加载器 Extention ClassLoader
主要用户加载lib/ext包下的jar包和.class文件,它继承自URLClassLoader。
3.3 应用类加载器 App ClassLoader
用来加载工程classpath下的jar包和.class文件。
3.4 自定义类加载器 Custom ClassLoader
负责加载用户自定义路劲的jar包和.class文件。
3.5 JVM类加载器的问题
如果在java.lang包下,再自己写一个String类,其实自己写的String类并不会生效,除非放在我们自己的classpath下才能生效。这是因为java的类加载机制提供了一个双亲委派的模型来保证核心类库的安全性,不被随意修改,因为同一个类被两个不同的类加载器加载就会是不同的类了。
4.双亲委派机制
双亲委派机制的简要流程是一个
递归的过程,如下:
1.AppClassLoader检查该类是否已经被加载findLoadClass(),如果又返回,则不用加载,直接返回。
2.如果没有被加载过,则判断是否有父加载器父加载器不是父类,他们没有继承关系,如有有则委托父 加载器加载loadClass()。
3.如果父加载器加载不了,则调用当前类加载器进行加载。
4.1 为什么要设计双亲委派模型?
1.保证安全,防止JDK核心类库不会被篡改。 2.避免重复加载,保证类的唯一性。
5.自定义类加载器
自定义一个类加载器,只需要继承java.lang.ClassLader类,并重写findClass()。
6.打破双亲委派机制(Tomcat为例)
可以通过覆盖loadClass()进行双亲委派的自定义。但是打破双亲委派是在ExtClassLoader之下的类加载器,JDK核心包仍然是不能打破的,会抛出package java权限错误。
例如Tomcat就是打破双亲委派机制典型的代表:
6.1 CommonClassLoader
Tomcat最基本的类加载器,加载各个webapp应用公用库的加载。
6.2 CatalinaClassLoader
对webapp应用不可见。
6.3 SharedClassLoader
对tomcat容器和catalina不可见,对所有webapp应用可见。
6.4 WebappClassLoader
各个应用的类加载器,各个应用的类相互隔离,每个WebappClassLoader加载自己目录的class文件和jar,并不会委托给父加载器,打破了双亲委派模。
如果Tomcat使用的是Java默认的双亲委派模型,那么会有如下问题: 1.无法加载两个全限定名一样的类,实现不了多版本的class加载问题。 2.web容器和应用程序的类库容易混淆,不安全。