参考
尚硅谷:宋红康(b站视频)
一、类加载器子系统作用
- 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
- 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
二、类加载器ClassLoader角色
- class file(在下图中就是Car.class文件)存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
- class file加载到JVM中,被称为DNA元数据模板(在下图中就是内存中的Car Class),放在方法区。
- 在.class文件–>JVM–>最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色
三、类加载器的执行过程
3.1 类加载器的三个阶段(五个步骤)
加载:根据查找路径找到相应的 class 文件然后导入;
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作。
3.2 加载(执行顺序1-2-3-4)
-
加载刚好是加载过程的一个阶段(就是将字节码加载到jvm中,还没有进行其他的操作),二者意思不能混淆
-
通过一个类的全限定名获取定义此类的二进制字节流
a. 本地系统获取
b. 网络获取,Web Applet
c. zip压缩包获取,jar,war
d. 运行时计算生成,动态代理
e. 有其他文件生成,jsp
f. 专有数据库提取.class文件,比较少见
g. 加密文件中获取,防止Class文件被反编译的保护措施 -
将这个字节流所代表的的静态存储结果转化为方法区的运行时数据结构
-
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
3.3 链接
3.3.1 验证
-
目的:确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
-
四种验证:
a. 文件格式验证- 开头:CA FE BA BE(魔数,Java虚拟机识别)
- 主次版本号
- 常量池的常量中是否有不被支持的常量类型
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
b. 元数据验证(可以理解成一些语法校验)
对字节码描述的信息进行语义分析,保证描述符合Java规范
类是否有父类,除了Object之外,所有的类都应该有父类
类的父类是否继承了不允许被继承的类(被final修饰的类)
如果这个类不是 抽象类,是否实现了其父类或接口中要求实现的所有方法。
类的字段,方法是否与父类的产生矛盾。例如方法参数都一样,返回值不同c. 字节码验证
通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的。
对类的方法体,进行校验分析,保证在运行时不会做出危害虚拟机的行为
保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,不会出现类似于在操作数栈放了一个int类型的数据,使用时却按照long类型加载到本地变量表中的情况。
保障任何跳转指令都不会跳转到方法体之外的字节码指令上。d. 符号引用验证
通过字符串描述的全限定名是否能找到对应的类
符号引用中的类、字段、方法的可访问性是否可被当前类访问
3.3.2 准备
- 为类变量(静态变量)分配内存,并且设置该类变量的初始值,即零值
- 不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化
- 不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象一起分配到Java堆中
eg:变量a在准备阶段会赋初始值,但不是1,而是0,在初始化阶段会被赋值为 1
public class HelloApp {
//prepare:a = 0 ---> initial : a = 1
private static int a = 1;
public static void main(String[] args) {
System.out.println(a);
}
}
3.3.3 解析
- 将常量池内的符号引用转换为直接引用的过程
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
3.4 初始化
- 遇到new,getstatic,putstatic或invokestatic这四条字节码指令时。
- 使用new关键字实例化对象
- 读取或设置一个类型的静态字段(final修饰已在编译期将结果放入常量池的静态字段除外)
- 调用一个类型的静态方法的时候
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(比如:Class.forName(“com.atguigu.Test”))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
补充:除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化,即不会执行初始化阶段(不会调用 clinit() 方法和 init() 方法)
clinit():
- 初始化阶段就是执行类构造器方法()的过程
- 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。也就是说,当我们代码中包含static变量的时候,就会有clinit方法
- ()方法中的指令按语句在源文件中出现的顺序执行
- ()不同于类的构造器。(关联:构造器是虚拟机视角下的())
- 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕
- 虚拟机必须保证一个类的()方法在多线程下被同步加锁
四、类加载器的分类
4.1 简述java类加载机制?
JVM把描述类的数据从Class文件加载到内存(方法区),并对数据进行校验,解析和初始化,最终形成可以被JVM直接使用的java类型。
4.2 什么是类加载器,类加载器有哪些?
4.2.1 什么是类加载器
通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器
4.2.2 四种类加载器
- 启动类加载器(Bootstrap ClassLoader):用来加载java核心类库,无法被java程序直接引用
- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。JVM的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
- 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现
解释: JVM严格来讲支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader);从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
但是在我们java程序中,用的最多的还是前三者;
可以通过代码看看:
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(systemClassLoader);
//获取其上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
//sun.misc.Launcher$ExtClassLoader@1540e19d
System.out.println(extClassLoader);
//获取其上层:获取不到引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
//null
System.out.println(bootstrapClassLoader);
//对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classLoader);
//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
ClassLoader classLoader1 = String.class.getClassLoader();
//null
System.out.println(classLoader1);
}
}
解释:为什么在获取BootStrap类加载器的时候会是null呢,因为引导类加载器右 C/C++ 语言,我们获取不到
tips:两次获取系统类加载器的值都相同:sun.misc.Launcher$AppClassLoader@18b4aac2 ,这说明系统类加载器是全局唯一的
4.3 几种类加载器的详解
4.3.1 启动类加载器
- C/C++语言实现,嵌套JVM内部
- 用来加载Java核心类库,rt.jar,resources.jar,sun.boot.class.path路径下的内容
- 并不继承java.lang.ClassLoader,没有父加载器
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java\javax\sun等开头的类
4.3.2 扩展类加载器
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从jre/lib/ext子目录下加载类库
4.3.3 应用程序类加载器(系统类加载器)
- Java语言编写,由sun.misc.Launcher$AppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载器是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过ClassLoader#getSystemClassLoader()方法可以后去到改类加载器
4.3.4 三种加载器加载路径代码展示
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("**********启动类加载器**************");
//获取BootstrapClassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);
System.out.println("***********扩展类加载器*************");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
//sun.misc.Launcher$ExtClassLoader@1540e19d
System.out.println(classLoader1);
}
}
执行结果:
**********启动类加载器**************
file:/D:/java/jdk/jre/lib/resources.jar
file:/D:/java/jdk/jre/lib/rt.jar
file:/D:/java/jdk/jre/lib/sunrsasign.jar
file:/D:/java/jdk/jre/lib/jsse.jar
file:/D:/java/jdk/jre/lib/jce.jar
file:/D:/java/jdk/jre/lib/charsets.jar
file:/D:/java/jdk/jre/lib/jfr.jar
file:/D:/java/jdk/jre/classes
null
***********扩展类加载器*************
D:\Java\jdk\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
sun.misc.Launcher$ExtClassLoader@6e0be858
4.3.5 用户自定义类加载器
-
为什么要用自定义类加载器
a. 隔离加载类:例如使中间件的Jar包与应用程序Jar包不冲突
b. 修改类加载的方式:启动类加载器必须使用,其他可以根据需要自定义加载
c. 扩展加载源
d. 防止源码泄露:对字节码进行加密,自定义类加载器实现解密 -
实现步骤
a. 继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器
b. 1.2之前,继承并重写loadClass方法,
c. 1.2之后,建议把自定义的类加载逻辑写在findClass()方法中
d. 如果没有太过复杂的需求,可以直接继承URLClassLoader类,可以避免自己编写findClass()方法,及其获取字节码流的方式,使自定义类加载器编写更加简洁
代码示例:
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
return defineClass(name, result, 0, result.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCustomPath(String name) {
//从自定义路径中加载指定类:细节略
//如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
return null;
}
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
Class<?> clazz = Class.forName("One", true, customClassLoader);
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.3.6 关于ClassLoader
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
代码展示:
public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
//1.
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
//2.
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);
//3.
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
结果:
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@14ae5a5
五、双亲委派
5.1 原理
Java虚拟机对Class文件采用的是按需加载,而且加载class文件时,Java虚拟机使用的是双亲委派模式,即把请求交由父类处理,它是异种任务委派模式
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载。而是把这个请求委托给父类的加载器去执行
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器
- 如果父类的加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
5.2 优势
- 避免类的重复加载
- 保护程序安全,防止核心API被篡改
5.3 沙箱安全机制
- 保证对Java核心源代码的保护,不会被篡改
- 自定义String类时:在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java.lang.String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。可以看下代码:
package java.lang;
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
//错误: 在类 java.lang.String 中找不到 main 方法
public static void main(String[] args) {
System.out.println("hello,String");
}
}
结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
eg2:也不能将类定义在某些特殊含义的包下
public class ShkStart {
public static void main(String[] args) {
System.out.println("hello!");
new ConcurrentHashMap<>();
}
}
结果:
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main"
5.4 补充
5.4.1 如何判断两个class对象是否相同?
- 类的完整类名必须一致,包括包名
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
- 换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的
5.4.2 对类加载器的引用
- JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的
- 如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中
- 当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的(后面讲)
六、总结
感谢大家阅、互相学习;
感谢尚硅谷提供的学习资料;
有问题评论或者发邮箱;
gitee:很多代码仓库;
[email protected]