还不懂「JVM-类加载」么?面试官又在问了!
类加载过程
类加载机制:
- Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最 终形成可以被虚拟机直接使用的Java类型
- 在Class文件中述的各类信息,最终都需要加 载到虚拟机中之后才能被运行和使用
类的生命周期
- 一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历
- 加载、验 证 、 准 备、解 析、初 始 化、使 用 和 卸 载 七 个 阶 段
- 加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按 照这种顺序按部就班地开始
- 解析阶段在某些情况下可以在初始化阶段以后开始 也可以称为动态绑定
- 大家可以试想下面这段代码输出什么?
public class SuperClass {
static {
System.out.println("SuperClass init!"); }
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init!"); }
}
class test {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
- 解答:对于静态字段, 只有直接定义这个字段的类才会被初始化,
- 因此通过其子类来引用父类中定义的静态字段,只会触发 父类的初始化而不会触发子类的初始化
加载
加载阶段需要做的三件事
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证
目的
- 是确保Class文件的字节流中包含的信息符合《Java虚 拟机规范》的全部约束要求
- 保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
- 文件格式验证:验证魔数开头、主次版本号、常量池是否支持、Class文件中各部分是否有被删除或附加的信息
- 元数据验证:主要是语义分析、是否有父类、类中字段是否与父类产生矛盾(如覆盖了父类的final字段)
- 字节码验证:主要判断语义是否合法。符合逻辑、保证指令安全
- 符号引用验证:主要判断该类是否缺少或被禁止访问它依赖的某些外部类、方法、字段等资源
准备
目的
- 正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
- 首先是这时候进行内存分配的 仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
- 其 次是这里所说的初始值“ 通常情况”下是数据类型的零值,假设一个类变量的定义为:
public static int value = 123;
- 那变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,
- 而把 value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值 为123的动作要到类的初始化阶段才会被执行
- Java中所有基本数据类型的零值:
解析
目的
- Java虚拟机将常量池内的符号引用替换为直接引用
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可
- 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能 间接定位到目标的句柄
- 类或接口的解析
- 字段解析
- 方法解析
- 接口方法解析
初始化
目的
- Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。
- 进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通 过程序编码制定的主观计划去初始化类变量和其他资源。
- 我们来看下面的代码:
- 会提示以下错误
- 是因为编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问 到定义在静态语句块之前的变量定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
双亲委派模型
- 站在Java虚拟机的角度来看,只存在两种不同的类加载器:
- 一种是启动类加载器(BootstrapClassLoader),另一种是其他所有类的加载器
双亲委派模型的工作过程是
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成
- 每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时
- 子加载器才会尝试自己去完成加载。
这样组织类之间的关系的好处
- Java中的类随着它的类 加载器一起具备了一种带有优先级的层次关系
- 例如类java.lang.Object,它存放在rt.jar之中,无论哪一 个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,
- 因此Object类 在程序的各种类加载器环境中都能够保证是同一个类。
- 反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的 ClassPath中
- 那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应 用程序将会变得一片混乱。
- 以上代码会报如下的错误
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
- 出现以上错误是因为:当执行类加载器的时候,首先会交给父类去加载
- 如果双亲中的某一个加载器 加载成功后,再向下返回成功
- 如果所有的双亲和自己都无法加载,则报异常
==如何破坏双亲委派模型==
- 线程上下文类加载器 (Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方 法进行设置,
- 如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内 都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
- 有了线程上下文类加载器,程序就可以做一些作弊的事情了。JNDI服务使用这个线程上下文类
- 加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,
- 这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则
Tomcat的类加载器架构
==基本需求==
- 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离
- 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享
- Common类加载器能加载的类都可以被Catalina类加载器和Shared类加载器使 用
- 而 Catalina类加载器和Shared类加载器自己能加载的类则与对方相互隔离。
- WebApp类加载器可以使用Shared类加载器加载到的类,但各个WebAp p 类加载器实例之间相互隔离。
- 而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个Class文件,
- 它存在的目的就是为了被 丢弃:当服务器检测到JSP文件被修改时,会替换掉目前的Jasp erLoader的实例
- 并通过再建立一个新 的JSP类加载器来实现JSP文件的HotSwap功能。
作者:xiaoff
链接:https://juejin.cn/post/6993118227521880094
来源:掘金