文章目录
一、类加载器
1.1 什么是类加载器、类加载器作用 1.2 应用场景 1.3 类加载时机 1.4 类加载器分类1.4.1 概述 1.4.2 JDK8及之前的版本 1.4.3 JDK9之后的类加载器 二、双亲委派模型
2.1 什么是双亲委派模型 2.2 JVM为什么采用双亲委派机制 2.3 打破双亲委派机制 2.4 总结 三、说一下类装载的执行过程
3.1 加载 3.2 验证 3.3 准备 3.4 解析 3.5 初始化 3.6 使用 3.7 小节 四、小节
本文重点介绍JVM面试题——类加载器、双亲委派模型、类装载的执行过程。对于相关面试题,进行重点归纳总结;如果想查看具体详情,可参考 类加载器 超详解:什么是类加载器,类加载器作用及应用场景,类加载时机,类加载的完整过程,类加载器分类 、JVM —— 类加载器的分类,双亲委派机制
一、类加载器 1.1 什么是类加载器、类加载器作用 类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。 类加载器会通过二进制流的方式获取到字节码文件的内容,接下来将获取到的数据交给Java虚拟机,虚拟机会在方法区和堆上生成对应的对象保存字节码信息 (类加载器只参与加载过程中的字节码获取并加载到内存这一部分)JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中 ,从而让Java程序能够启动起来。 作用 :负责将.class文件(存储的物理文件)加载在到内存中。通过加载字节码数据放入内存转换成byte[],接下来调用虚拟机底层方法将byte[]转换成方法区和堆中的数据
1.2 应用场景 企业级应用 大量的面试题什么是类的双亲委派机制 打破类的双亲委派机制 自定义类加载器 解决线上问题 1.3 类加载时机 简单理解:字节码文件什么时候会被加载到内存中?
有以下几种情况:
创建类的实例(对象) 调用类的类方法 访问类或者接口的类变量,或者为该类变量赋值 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象 初始化某个类的子类 直接使用java.exe命令来运行某个主类 总结而言:用到了就加载,不用不加载
1.4 类加载器分类 1.4.1 概述 类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。
虚拟机底层实现:源代码位于Java虚拟机的源码中,实现语言与虚拟机底层语言一致,比如Hotspot使用C++。主要目的是保证Java程序运行的基础类被正确地加载 ,比如java.lang.String,Java虚拟机需要确保其可靠性。 JDK中默认提供或者自定义(重点关注 ):JDK中默认提供了多种处理不同渠道的类加载器,程序员也可以自己根据需求使用Java语言定制。所有Java中实现的类加载器都需要继承ClassLoader这个抽象类。 类加载器的设计,JDK8和8之后的版本差别较大(JDK9之后,出现了模块化设计)。
1.4.2 JDK8及之前的版本 首先来看JDK8及之前的版本,JDK8及之前的版本中默认的类加载器有如下几种 :
启动类加载器(Bootstrap ClassLoader、C++实现) :加载JAVA_HOME/jre/lib目录下的库,加载核心类,String类。它是JVM的一部分,负责加载Java核心类库,如java.lang包中的类。它是最顶层的类加载器,通常使用C++实现,无法在Java代码中直接获取到 。通常表示为null ,并且没有父null(通用且重要)扩展类加载器(Extension ClassLoader、Java实现) :主要加载JAVA_HOME/jre/lib/ext目录中的类。加载扩展类,拓展Java中比较通用的类,只是通用,不是特别重要,最重要的在启动类加载器加载了。通常位于JRE的lib/ext目录下应用程序类加载器(Application ClassLoader、Java实现) :也称为系统类加载器(System ClassLoader) ,加载classPath下的类。加载应用classpath中的类,包括我们自己写的类,还有第三方Jar包的类自定义类加载器(Java实现) :可以通过继承 java.lang.ClassLoader 类来自定义类加载器,需要重写findClass方法,实现自定义类加载规则。自定义类加载器可以灵活加载类,实现各种特定需求,比如从网络下载类文件、解密等。
JDK9及之后扩展类加载器(Extension ClassLoader)变成了平台类加载器(Platform ClassLoader) 各自类加载器的详细介绍 可参考 类加载器 超详解:什么是类加载器,类加载器作用及应用场景,类加载时机,类加载的完整过程,类加载器分类
1.4.3 JDK9之后的类加载器 由于JDK9引入了module的概念,类加载器在设计上发生了很多变化:
1)启动类加载器使用Java编写,位于jdk.internal.loader.ClassLoaders类中。Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。
启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一
2)扩展类加载器被替换成了平台类加载器(Platform Class Loader)。平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinCLassLoader实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑
二、双亲委派模型 2.1 什么是双亲委派模型 双亲委派机制(Parent Delegation Model) 是Java类加载器的一种工作方式,用于保证类的加载安全性 和一致性 。
加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类
具体介绍 :如果一个类加载器收到了类加载请求、需要加载某个类时,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
双亲委派机制-问题
重复的类:如果一个类重复出现在三个类加载器的加载位置,应该由谁来加载? ——启动类加载器加载,根据双亲委派机制,它的优先级是最高的 String类能覆盖吗:在自己的项目中去创建一个java.lang.String类,会被加载吗? ——不能,会返回启动类加载器加载在rt.jar包中的String类 双亲委派具体细节 可参考 JVM —— 类加载器的分类,双亲委派机制
2.2 JVM为什么采用双亲委派机制 (1)通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
(2)为了安全,保证类库API不会被修改
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("demo info");
}
}
由于是双亲委派的机制,java.lang.String的在启动类加载器得到加载,因为在核心jre库中有其相同名字的类文件,但该类中并没有main方法。这样就能防止恶意篡改核心API库。
此时执行main函数,会出现异常,在类 java.lang.String 中找不到 main 方法
总的来说,双亲委派机制可以保证类的一致性、安全性和隔离性,避免重复加载 ,同时也提供了灵活的扩展性,使得类加载器可以根据特定需求进行定制。
而虽然双亲委派机制为JAVA类的加载提供了很好的安全性和便捷性。但是有的时候我们不得不打破双亲委派机制,例如:一个Tomcat容器中可以运行多个WEB应用,而如果这两个应用中出现了同名的A类,那么Tomcat就要保证这两个A类都被加载并且是各自不同的类。如果不打破双亲委派机制,那么WEB1中的A类记载后,WEB2中自己的A类就不会加载成功了,按照双亲委派机制来讲,此时会直接返回WEB1中的A类。此时我们就需要打破双亲委派机制。
2.3 打破双亲委派机制 打破双亲委派机制的三种方式:
自定义类加载器 自定义类加载器并且重写loadClass方法,就可以将双亲委派机制的代码去除 Tomcat通过这种方式实现应用之间类隔离 线程上下文加载器 利用上下文类加载器加载类,比如JDBC和JNDI等。SPI机制+线程上下文类加载器 Osgi框架的类加载器(了解即可)历史上Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载 双亲委派具体细节 可参考 JVM —— 类加载器的分类,双亲委派机制
2.4 总结 (1)什么是双亲委派模型
加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类
当一个类加载器去加载某个类的时候,会自底向上查找是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载(自底向上查找是否加载过,再由顶向下进行加载。避免了核心类被应用程序重写并覆盖的问题,提升了安全性 ) 应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器。 双亲委派机制的好处有两点:第一是避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。第二是避免一个类重复地被加载。 (2)JVM为什么采用双亲委派机制 ?
通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。 为了安全,保证类库API不会被修改 三、说一下类装载的执行过程 类从加载到虚拟机中开始、直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载 这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)
3.1 加载 通过类的全名(包名 + 类名),获取类的二进制数据流 将这个类加载到内存中:解析类的二进制数据流为方法区内的数据结构(Java类模型)。 加载完毕创建一个class对象,即创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口 3.2 验证 确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全(文件中的信息是否符合虚拟机规范有没有安全隐患)。
3.3 准备 负责为类的类变量(被static修饰的变量)分配内存,并设置默认初始化值(初始化静态变量)
static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成 static变量是final的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成 static变量是final的引用类型,那么赋值也会在初始化阶段完成 public class Application {
static int b = 10;
static final int c = 20;
static final String d = "hello";
static final Object obj = new Object();
}
3.4 解析 将类的二进制数据流中的符号引用替换为直接引用 (本类中如果用到了其他类,此时就需要找到对应的类)
比如方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。
3.5 初始化 根据程序员通过程序制定的主观计划去初始化类变量和其他资源(静态变量赋值以及初始化其他资源)
对类的静态变量、静态代码块执行初始化操作 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行 3.6 使用 JVM开始从入口方法执行用户的程序代码
调用静态类成员信息(比如:静态字段、静态方法) 使用new关键字为其创建对象实例 3.7 小节 1)当一个类被使用的时候,才会加载到内存
2)类加载的执行过程: 加载、验证、准备、解析、初始化、使用、卸载
加载:查找和导入class文件 验证:保证加载类的准确性 准备:为类变量分配内存并设置类变量初始值 解析:把类中的符号引用转换为直接引用 初始化:对类的静态变量,静态代码块执行初始化操作 使用:JVM 开始从入口方法开始执行用户的程序代码 卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象。 四、小节 (1)什么是类加载器
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中 ,从而让Java程序能够启动起来。
(2)类加载器的作用是什么
类加载器(ClassLoader)负责在类加载器过程中的字节码获取并加载到内存这一部分。通过加载字节码数据放入内存转换成byte[],接下来调用虚拟机底层方法将byte[]转换成方法区和堆中的数据
(3)类加载器有哪些/有几种常见的类加载器
启动类加载器(BootStrap ClassLoader):加载JAVA_HOME/jre/lib目录下的库,加载核心类 扩展类加载器(Extension ClassLoader):主要加载JAVA_HOME/jre/lib/ext目录中的类,加载扩展类 应用类加载器(Application ClassLoader):用于加载classPath下的类 自定义类加载器(Customize ClassLoader):自定义类继承ClassLoader,重写findClass方法,实现自定义类加载规则。
JDK9及之后扩展类加载器(Extension ClassLoader)变成了平台类加载器(Platform ClassLoader) (4)什么是双亲委派机制
每个Java实现的类加载器中保留了一个成员变量叫“父”(Parent)类加载器。
当一个类加载器去加载某个类的时候,会自底向上查找是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载(自底向上查找是否加载过,再由顶向下进行加载。避免了核心类被应用程序重写并覆盖的问题,提升了安全性 )加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类 应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器。 双亲委派机制的好处有两点:第一是避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。第二是避免一个类重复地被加载。 (5)JVM为什么采用双亲委派机制
通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。 为了安全,保证类库API不会被修改 (6)怎么打破双亲委派机制
重写loadClass方法,不再实现双亲委派机制 JNDI、JDBC、JCE、JAXB和JBI等框架使用了SPI机制+线程上下文类加载器 OSGi实现了一整套类加载机制,允许同级类加载器之间互相调用 (7)说一下类装载的执行过程
加载:查找和导入class文件 验证:保证加载类的准确性 准备:为类变量分配内存并设置类变量初始值 解析:把类中的符号引用转换为直接引用 初始化:对类的静态变量,静态代码块执行初始化操作 使用:JVM 开始从入口方法开始执行用户的程序代码 卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象。 参考 黑马程序员相关视频及笔记