文章目录
一、什么是类加载器、类加载器作用
- 1.1 定义与作用
- 1.2 应用场景
二、类加载时机
三、类加载的完整过程
- 3.1 加载
- 3.2 验证
- 3.3 准备
- 3.4 解析
- 3.5 初始化
- 3.6 使用
- 3.7 小节
四、类加载的分类【理解】
4.1 概述
4.2 JDK8及之前的版本
4.2.1 启动类加载器
4.2.2 扩展类加载器和应用程序类加载器
- 扩展类加载器
- 应用程序类加载器
4.3 JDK9之后的类加载器
4.4 ClassLoader 中的两个方法【应用】
4.5 小节
一、什么是类加载器、类加载器作用
1.1 定义与作用
- 类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。
- 类加载器会通过二进制流的方式获取到字节码文件的内容,接下来将获取到的数据交给Java虚拟机,虚拟机会在方法区和堆上生成对应的对象保存字节码信息(类加载器只参与加载过程中的字节码获取并加载到内存这一部分)
- JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。
作用:负责将.class文件(存储的物理文件)加载在到内存中。通过加载字节码数据放入内存转换成byte[],接下来调用虚拟机底层方法将byte[]转换成方法区和堆中的数据
1.2 应用场景
- 企业级应用
- SPI机制
- 类的热部署
- Tomcat类的隔离
- 大量的面试题
- 什么是类的双亲委派机制
- 打破类的双亲委派机制
- 自定义类加载器
- 解决线上问题
- 使用Arthas不停机解决线上故障
二、类加载时机
简单理解:字节码文件什么时候会被加载到内存中?
有以下几种情况:
- 创建类的实例(对象)
- 调用类的类方法
- 访问类或者接口的类变量,或者为该类变量赋值
- 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
- 初始化某个类的子类
- 直接使用java.exe命令来运行某个主类
总结而言:用到了就加载,不用不加载
三、类加载的完整过程
类从加载到虚拟机中开始、直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这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对象。
四、类加载的分类【理解】
4.1 概述
类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。
虚拟机底层实现:源代码位于Java虚拟机的源码中,实现语言与虚拟机底层语言一致,比如Hotspot使用C++。主要目的是保证Java程序运行的基础类被正确地加载,比如java.lang.String,Java虚拟机需要确保其可靠性。
JDK中默认提供或者自定义(重点关注):JDK中默认提供了多种处理不同渠道的类加载器,程序员也可以自己根据需求使用Java语言定制。所有Java中实现的类加载器都需要继承ClassLoader这个抽象类。
类加载器的设计,JDK8和8之后的版本差别较大(JDK9之后,出现了模块化设计)。
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)
代码演示
public class ClassLoaderClassDemo1 {
public static void main(String[] args) {
//获取应用程序类加载器/系统类加载器 sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//获取应用程序类加载器的父加载器 --- 扩展类加载器 sun.misc.Launcher$ExtClassLoader@6a6824be
ClassLoader classLoader1 = systemClassLoader.getParent();
//获取扩展类加载器的父加载器 --- 启动类加载器 null
ClassLoader classLoader2 = classLoader1.getParent();
System.out.println("应用程序类加载器" + systemClassLoader);
System.out.println("扩展类加载器" + classLoader1);
System.out.println("启动类加载器" + classLoader2);
}
}
补充:Arthas中类加载器相关的功能
Arthas是程序员开发运维必不可少的一个工具,还记得如何使用吗?忘记的话,可以参考 Java字节码文件、组成、详解、分析;jclasslib插件、阿里arthas工具;Java注解
类加载器的详细信息可以通过classloader命令查看:
classloader
查看classloader的继承树,urls,类加载信息,使用classloader去getResource
第1列为类加载名称,第2列为当前类加载器在内存中实例个数,第3列为当前类加载器加载了多少个类。
- BootstrapClassLoader是启动类加载器,numberOfInstances是类加载器的数量只有1个,loadedCountTotal是加载器所加载的类的数量为1861个
- ExtClassLoader是扩展类加载器
- AppClassLoader是应用程序类加载器
- DelegatingClassLoader是用来提升反射效率的类加载器
4.2.1 启动类加载器
- 启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供的、使用C++编写的类加载器,Java程序员无法修改或者扩展源代码,所以只关注这个加载器的作用。
- 作用:默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar等,给java程序提供了一个基础的运行环境
在IDEA项目右侧External Libraries中也能找到对应jar包,这就是启动类加载器所加载的。
/**
* 启动类加载器案例
*/
public class BootstrapClassLoaderDemo {
public static void main(String[] args) throws IOException {
//通过String类获取到它的类加载器。String.class 取到当前堆上的class对象
ClassLoader classLoader = String.class.getClassLoader();
System.out.println(classLoader); //输出null
//让程序不再退出
System.in.read();
}
}
这段代码通过String类获取到它的类加载器并且打印,本来以为是Bootstrap ClassLoader
,结果是null
。这是因为启动类加载器在JDK8中是由C++语言来编写的,在Java代码中去获取既不适合也不安全,所以才返回null
(String类确实是由启动类加载器加载的,但是启动类加载器由虚拟机底层实现、没有存在Java代码中,无法通过Java代码获取底层的虚拟机启动类加载器)
在Arthas中可以通过sc -d 类名
的方式查看加载这个类的类加载器详细的信息,如
通过上图可以看到,java.lang.String类的类加载器是空的,Hash值也是null。所以只要看到class-loader为null,就知道这是启动类加载器
通过启动类加载器去加载用户jar包:
如果用户想扩展一些比较基础的jar包,让启动类加载器加载,有两种途径:
- 打包成jar包,放入jre/lib下进行扩展。不推荐,尽可能不要去更改JDK安装目录中的内容,因为即使放进去由于文件名不匹配的问题也不会正常地被加载(在加载jar包的时候,会对名称进行校验,名称必须符合JVM内部的一些规范)。
- 使用参数进行扩展。推荐,使用
-Xbootclasspath/a:jar包目录/jar包名
进行扩展,参数中的/a代表新增。
下面展示方式二实现流程:
先创建第一个项目,mvn package打包成jar包,把jar包重命名放到D:/jvm/jar目录下,即D:/jvm/jar/classloader-test.jar;
再创建第二个项目,在第二个项目的IDEA配置中添加虚拟机参数,就可以加载D:/jvm/jar/classloader-test.jar
这个jar包了
希望启动类加载帮我们加载A类,在另一个项目中获取A类并初始化:使用Class.forName
获取Jar包的类,可以正常执行初始化,说明自己拓展的Jar包被加载了
应用场景:在企业中开发一些偏底层的基础类,所有用到jdk的项目都需要使用这些基础类,此时就通过启动类加载器去加载用户jar包
4.2.2 扩展类加载器和应用程序类加载器
- 扩展类加载器和应用程序类加载器都是JDK中提供的、使用Java编写的类加载器。
- 它们的源码都位于sun.misc.Launcher中,是一个静态内部类。继承自URLClassLoader,具备通过目录或者指定jar包将字节码文件加载到内存中的能力。
继承关系图如上:
- ClassLoader类:定义了具体的行为模式,简单来说就是先从本地或者网络获得字节码信息,然后调用虚拟机底层的方法创建方法区和堆上的对象。这样的好处就是让子类只需要去实现如何获取字节码信息这部分代码。
- SecureClassLoader:提供了证书机制,提升了安全性。
- URLClassLoader:提供了根据URL获取目录下或者指定jar包进行加载,获取字节码的数据的能力。
扩展类加载器和应用类加载器继承自URLClassLoader,获得了上述的三种能力。
扩展类加载器
扩展类加载器(Extension Class Loader)是JDK中提供的、使用Java编写的类加载器。默认加载Java安装目录/jre/lib/ext下的类文件。
如下代码会打印ScriptEnvironment类的类加载器。ScriptEnvironment是nashorn框架中用来运行javascript语言代码的环境类,他位于nashorn.jar包中被扩展类加载器加载。这些类我们很少用,所以被放到了扩展类加载器中。
/**
* 扩展类加载器
*/
public class ExtClassLoaderDemo {
public static void main(String[] args) throws IOException {
ClassLoader classLoader = ScriptEnvironment.class.getClassLoader();
System.out.println(classLoader);
}
}
通过扩展类加载器去加载用户jar包:
- 放入/jre/lib/ext下进行扩展。不推荐,尽可能不要去更改JDK安装目录中的内容。
- 使用参数进行扩展使用参数进行扩展。推荐,使用
-Djava.ext.dirs=jar包目录
进行扩展,这种方式会覆盖掉原始目录(jre-xx/lib/ext),可以追加上原始目录,并使用 ;(windows系统所用符号) :(macos/linux) 进行分隔
确保自己写的类由扩展类加载器加载(上述A类),ScriptEnvironment仍由扩展类加载器加载、不受影响
使用引号
将整个地址包裹起来,这样路径中即便是有空格也不需要当做特殊字符额外处理。路径中要包含原来ext文件夹,同时在最后加上扩展的路径。
应用程序类加载器
应用程序类加载器会加载classpath下的类文件,默认加载的是项目中的类以及通过maven引入的第三方jar包中的类。
如下案例中,打印出Student
(自己写的)和FileUtils
(引入的)的类加载器:
/**
* 应用程序类加载器案例
*/
public class AppClassLoaderDemo {
public static void main(String[] args) throws IOException, InterruptedException {
//当前项目中创建的Student类
Student student = new Student();
ClassLoader classLoader = Student.class.getClassLoader();
System.out.println(classLoader);
//maven依赖中包含的类
ClassLoader classLoader1 = FileUtils.class.getClassLoader();
System.out.println(classLoader1);
Thread.sleep(1000);
//由于使用Arthas监控该程序,故加上SYstem.in.read()让主方法不退出
System.in.read();
}
}
输出结果如下,这两个类均由应用程序类加载器加载:
Arthas中类加载器相关功能
类加载器的加载路径可以通过classloader –c hash值
查看:
查看应用程序类加载器所加载的jar包
4.3 JDK9之后的类加载器
由于JDK9引入了module的概念,类加载器在设计上发生了很多变化
1)启动类加载器使用Java编写,位于jdk.internal.loader.ClassLoaders类中。Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。
启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一
2)扩展类加载器被替换成了平台类加载器(Platform Class Loader)。平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinCLassLoader实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑
4.4 ClassLoader 中的两个方法【应用】
- 方法介绍
方法名 | 说明 |
---|---|
public static ClassLoader getSystemClassLoader() | 获取系统类加载器 |
public InputStream getResourceAsStream(String name) | 加载某一个资源文件 |
- 示例代码
public class ClassLoaderDemo2 {
public static void main(String[] args) throws IOException {
//static ClassLoader getSystemClassLoader() 获取系统类加载器
//InputStream getResourceAsStream(String name) 加载某一个资源文件
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//利用加载器去加载一个指定的文件
//参数:文件的路径(放在src的根目录下,默认去那里加载)
//返回值:字节流。
InputStream is = systemClassLoader.getResourceAsStream("prop.properties");
Properties prop = new Properties();
prop.load(is);
System.out.println(prop);
is.close();
}
}
4.5 小节
(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实现了一整套类加载机制,允许同级类加载器之间互相调用
参考 黑马程序员相关视频及笔记,大部分内容来源于黑马程序员的视频黑马程序员JVM虚拟机入门到实战全套视频教程,java大厂面试必会的jvm一套搞定(丰富的实战案例及最热面试题),加上自己部分思考