感谢尚硅谷宋红康老师的JVM入门到精通课程,向每一个用心做免费教课程的老师致敬!
本套教程均为我学习课程之后的学习笔记,防止遗忘,并发送给大家分享,感谢大家查看~
本章包含知识点:类的加载过程,不同类加载器详解,双亲委派机制/沙箱安全机制,自定义类加载器!
java中的类要加载到jvm中才能使用,那么负责把java类从硬盘或网络等加载到jvm中的工具,就是类加载器(ClassLoader)。
注意:
举例说明:
演员(执行引擎)表演时,道具师(道具师)会提前将道具搬上舞台,而至于表演是否能正常进行,则由演员决定。
额外补充:常量池
在Java程序中,有很多的东西是永恒的,不会在运行过程中变化。比如一个类的名字,一个类字段的名字/所属类型,一个类方法的名字/返回类型/参数名与所属类型,一个常量,还有在程序中出现的大量的字面值。
public class ClassTest {
private String itemS ="我们 ";
private final int itemI =100 ;
public void setItemS (String para ){...}
}
其中:ClassTest,itemS,我们,itemI,100,setItemS ,para就是常量池中的常量!
类加载的过程有三步:
注意:这三步并不是顺序一次性执行完成的
比如:HelloLoad类
Public class HelloLoad
{
public static void main(String[] args){}
}
执行时JVM就会对HelloLoad类进行加载:
注意: 此时所说的加载并不是本章题目的记载,而是类加载过程分为三步,第一步也叫作加载而已。
加载的过程:
其中,被加载的字节码文件可以以多种方式加载,比如:
实现这些功能可以通过自定义类加载器实现
连接阶段分为三个具体步骤:验证,准备,解析
验证阶段(Verify):
比如:只有CAFEBABE开头的字节码文件才会被验证成功!
准备阶段(Prepare) :
注意:
带有默认初值的静态变量并不会在此时赋值,只会将变量的值赋值为初始值
但如果以final修饰该变量,则链接阶段就会显示的初始化该常量的值
public class HelloApp {
private static int a=1:;//链接阶段只会将a赋值为0
private static Date d = new Date();//连接阶段只会将d赋值为null
public final int b = 5;//连接阶段会直接将常量值初始化为5
public static void main(String[] args) {}
}
解析阶段(Resolve):
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
(此阶段在后续笔记中会讲到)
在编译时,编译器会自动将静态变量的赋值语句,静态代码块中的代码顺序编译成一个初始化方法< clinit >(),初始化过程会调用此方法。
注意:
举例说明: 请说明以下代码的执行结果
public class ClassInitTest {
public static int a = 1;
static {
a=2;
b=20;
}
public static int b = 10;
public static void main(String[] args) {
System.out.println("a:"+a);
System.out.println("b:"+b);
}
}
结果:
结果为a=2,b=10,因为< clinit>()是源文件中代码顺序决定,所以a=2的赋值语句会覆盖掉a=1的语句,而b=10的赋值语句,会覆盖掉a=20的赋值语句,所以出现以上结果!
此处注意:静态代码块中的b=10是编译器优化后的,所以他可以在代码声明前被赋值,使用其它操作则会报错(非法前向引用)!
private static int a=1;
链接阶段只会将a赋值为0,在初始化阶段才赋值为1
除上述情况外,都属于类的被动使用,不会对类进行初始化。
public class ClassInitTest {
public static void main(String[] args) {
Runnable r = ()->{
System.out.println("线程"+Thread.currentThread().getName()+"开始执行!");
new A();
};
Thread t1 =new Thread(r,"t1");
Thread t2 =new Thread(r,"t2");
t1.start();
t2.start();
}
}
class A
{
static {
if(true) {
System.out.println("类A被"+Thread.currentThread().getName()+"初始化!");
while (true);
}
}
}
输出结果:
也就是说,t1线程进行初始化操作,t2线程也准备对A进行初始化操作,但是由于线程锁的存在,t2线程此时无法访问初始化代码,这保证了初始化操作只能被执行一次!
Java规范中将类加载器分为两类,分别为:引导类加载器,自定义类加载器
其中:
引导类加载器(BootStrapClassLoader):
* 由C语言进行编写,负责加载系统核心类($JAVA_HOME中jre/lib/rt.jar和resource.jar下的类,或者sun.boot.class.path下的类),是JVM的一部分
* 他并不继承与ClassLoader,也没有上层类加载器
* 负责加载扩展类加载器和应用类加载器(这两个类加载器也是核心类)
* 出于安全考虑,Bootstrap启动类 加载器只加载包名为java、javax、sun等开头的类
自定义加载派生于ClassLoader抽象类,但是因为功能不同,所以我们将自定义类加载器分为三类:
扩展类加载器(ExtensionsClassLoader):
* 由Java语言编写,负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。
* 如果将我们自己编写的类放在指定补录下,也可以被扩展类加载器所加载
系统类加载器/应用类加载器(SystemClassLoader/APPClassLoader):
* 负责加载用户自定义的类(ClassPath类路径下的类)
* 该类是程序中默认的类加载器,负责加载程序中的基础类
* 用户自定义类加载器:如果我们有自定义类加载器的需求,可以继承ClassLoader抽象类,自定义我们的类加载器。
**注意:**引导类加载器,扩展类加载器,系统类加载器是由系统已定义好的,直接使用即可,而自定义类加载器需要自己实现加载时的代码逻辑,是自定义的。
下图为类加载器的继承关系:
类加载器之间的关系:
这几种类加载器之前并不是父子继承关系,而是包含关系,可以理解为上下级关系
其中: 可以使用ClassLoader的getParent()方法获取该加载器的上层加载器
public static void main(String[] args) {
ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader();
//获取系统类加载器
System.out.println(sysClassLoader);
ClassLoader extClassLoader = sysClassLoader.getParent();
//获取扩展类加载器
System.out.println(extClassLoader);
ClassLoader bsClassLoader = extClassLoader.getParent();
//获取引导类加载器
System.out.println(bsClassLoader);
}
额外补充: JVM如何判断Class对象是否是同一个类
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
也就是说:通一个字节码文件,在同一个JVM中,如果被不同的类加载器所加载,产生的类对象也不是相等的!
双亲委派机制:
自定义java.util.Date类,并随便写点内容:
package java.util;
public class Date {
@Override
public String toString() {
return "自定义的Date类";
}
}
定义测试类实例化并输出:
import java.util.Date;
public class ClassTest {
public static void main(String[] args) {
Date date =new Date();
System.out.println(date);
}
}
提出问题: 这个时候,我们自定义了一个java.util.Data类,而java核心类库中又包含了一个java.util.Data类,两个类的包名类名完全相同,我们在使用时,会加在哪个类呢?
结果:
测试发现,即使两个类类名包名完全相同,程序依旧不会报错,而是正常运行,并且正常输出系统核心类库中的Date类。
这是因为,JVM加载类时,遵循“双亲委派机制”,所以只会加载系统中的类。
双亲委派机制原理:
Java虛拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由上层加载器处理,它是一种任务委派模式。
这时通常会将加载请求顺序委派到最上层加载器(引导类加载器),如果引导类加载器可以加载,则加载指定目录下的类,如果不能加载,则会由下层加载器逐层加载。
也就是说:
在使用Date类时,JVM会判断该类是否已加载,如果未加载,则会使用默认加载器(系统类加载器)加载,但是系统类加载器有上层加载器,所以系统类加载器会向上委派,使用扩展类加载器加载该类,扩展类加载器也有上层类加载器,所以会继续向上委派,使用引导类加载器加载该类,而引导类加载器判断该类包名可以加载(以java开头),则执行加载逻辑,去jre/lib/rt.jar目录下寻找.class文件加载该类,所以即使包名和类名完全相同,JVM也只会加载系统自定义的Date类!
总结:
如果自定义的类如果包名和类名与系统类完全相同,此类则永远也不会被加载,相当于作废,这个机制可以保护系统核心类库不被私自篡改,所以这种机制也叫==“沙箱安全机制”==!
另一个例子:
自定义一个类com.wojiushiwo.Hello,该类在加载时,系统类加载器会直接将加载请求委派到引导类加载器,引导类加载器判断包名不符合规定(不以java,javax,sun等开头),所以由扩展类加载器加载,扩展类加载器也无法加载此类,系统类加载器才从ClassPath目录下加载该类,这是一个类加载器加载类的完整过程,这种加载时将类委派到上层加载器加载的机制叫双亲委派机制!
第三个例子:
自定义类java.util.Test类
package java.util;
public class Test {
public static void main(String[] args) {
System.out.println("测试!");
}
}
系统加载Test类时,会将家加载请求直接委派到引导类加载器加载,这时引导类加载器就会判断此类是否可以加在,因为以Java报名开头,所以引导类加载器会去jre/lib/rt.jar下加载该类,由于rt.jar下没有该类,所以会报错!
所以要注意:自定义类不要以java/javax/sun等等特殊包名开头!
问题: 我们为什么要自定义类加载器
如何自定义类加载器:
继承ClassLoader,重写需要自定义逻辑的代码,一般来说是findClass方法!
public class TestClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//在此处重写自己的逻辑
}
}
举例说明: 自定义类加载器,可以从指定目录加载相应的类
package com.wojiushiwo;
import java.io.File;
import java.nio.file.Files;
/**
* @author 我就是我500
* @date 2020-02-11 13:13
* @describe
**/
public class TestClassLoader extends ClassLoader{
String classPath;//要加载的目录
/**
* @param name 要加载类的全类名
**/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = null;
try {
classData = Files.readAllBytes(new File(classPath+name.replace(".","\\")+".class").toPath());
//将指定路径的文件读以字节流方式读取
}catch (Exception ex)
{
throw new ClassNotFoundException();
}
//将字节流转化为Class对象
return defineClass(name,classData,0,classData.length);
}
protected TestClassLoader(String classPath) {
this.classPath=classPath;
}
}
自定义测试类:
package com.wojiushiwo;
/**
* @author 我就是我500
* @date 2020-02-11 13:31
* @describe
**/
public class Test {
public static void main(String[] args) {
TestClassLoader testClassLoader = new TestClassLoader("C:\\Users\\13055\\Desktop\\test\\java\\");
try {
Class testClass = testClassLoader.loadClass("com.wojiushiwo.TestClass");
System.out.println(testClass);
}catch (Exception ex)
{
ex.printStackTrace();
}
}
}
将字节码文件放到指定位置
运行结果:
可以看到此时我们的自定义类加载器已经完成了对指定路径的类的加载!
除此之外,我们还可以通过多种方式加载类:从数据库加载,从网络加载,解密后加载等,只需完成相应的加载逻辑即可!
文章到此结束,下一章更新:运行输数据区,欢迎观看~