我的JVM学习笔记:第二章——类加载子系统

我的JVM学习笔记:第二章——类加载子系统

感谢尚硅谷宋红康老师的JVM入门到精通课程,向每一个用心做免费教课程的老师致敬!
本套教程均为我学习课程之后的学习笔记,防止遗忘,并发送给大家分享,感谢大家查看~

本章包含知识点:类的加载过程,不同类加载器详解,双亲委派机制/沙箱安全机制,自定义类加载器!

一、类加载器概念

java中的类要加载到jvm中才能使用,那么负责把java类从硬盘或网络等加载到jvm中的工具,就是类加载器(ClassLoader)。

注意:

  • 类加载器加载的.class文件后,类的信息和常量池会被存放到方法区
  • 只有特定开头的标识.class文件会被加载,这个标识为CAFEBABE
  • 类加载器只负责加载.class文件,至于他是否可以运行,则由执行引擎所决定

举例说明:
演员(执行引擎)表演时,道具师(道具师)会提前将道具搬上舞台,而至于表演是否能正常进行,则由演员决定。

额外补充:常量池
在Java程序中,有很多的东西是永恒的,不会在运行过程中变化。比如一个类的名字,一个类字段的名字/所属类型,一个类方法的名字/返回类型/参数名与所属类型,一个常量,还有在程序中出现的大量的字面值。

public class ClassTest {
    
    
private String itemS ="我们 ";
private final int itemI =100 ;
public void setItemS (String para ){
    
    ...}
}

其中:ClassTest,itemS,我们,itemI,100,setItemS ,para就是常量池中的常量!

二、类加载的过程

类加载的过程有三步:

  • 加载(Loding):将字节码文件以二进制流的方式加载到内存,形成Class对象
  • 链接(Linking):验证字节码,对静态成员变量赋初值等
  • 初始化(Initalition):调用初始化方法对类进行初始化

类加载过程

注意:这三步并不是顺序一次性执行完成的
比如:HelloLoad类

Public class HelloLoad
{
    
    
	public static void main(String[] args){
    
    } 
} 

执行时JVM就会对HelloLoad类进行加载:

  1. JVM判断类是否已加载已加载
  2. 如果未加载,进行第一步,将类以二进制的方式加载到内存
  3. 如果未已加载,直接链接该类,之后执行初始化操作
  4. 最后执行main()方法执行程序

类加载过程

三、类加载第一步:加载

注意: 此时所说的加载并不是本章题目的记载,而是类加载过程分为三步,第一步也叫作加载而已

加载的过程:

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java. lang . Class对象,作为方法区这个类的各种数据的访问入口

其中,被加载的字节码文件可以以多种方式加载,比如:

  • 从本地系统中直接加载
  • 通过网络获取,典型场景: Web Applet
  • 从zip压缩包中读取,成为日后jar、war格式的基础
  • 运行时计算生成,使用最多的是:动态代理技术
  • 由其他文件生成, 典型场景: JSP应用
  • 从专有数据库中提取.class文件,比较少见
  • 从加密文件中获取,典型的防Class文件被反编译的保护措施

实现这些功能可以通过自定义类加载器实现

四、类加载第二步:链接

连接阶段分为三个具体步骤:验证,准备,解析

验证阶段(Verify):

  • 目的在子确保C1ass文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
  • 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

比如:只有CAFEBABE开头的字节码文件才会被验证成功!

准备阶段(Prepare) :

  • 为类变量分配内存并且设置该类变量的默认初始值,即零值(这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化)。
  • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

注意:
带有默认初值的静态变量并不会在此时赋值,只会将变量的值赋值为初始值
但如果以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 >(),初始化过程会调用此方法。
注意:

  • < clinit>()并不是类的构造方法,构造方法在编译后是< init>()方法
  • 如果类有父类,则子类的< clinit>()一定晚于父类的< clinit>()方法执行(必须保证父类已初始化完毕)
  • < clinit>()方法不需要定义,编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
  • < 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是编译器优化后的,所以他可以在代码声明前被赋值,使用其它操作则会报错(非法前向引用)!
非法的前向引用

  • < clinit>()方法针对的是静态变量,静态变量在链接过程中全部赋值为初始值,在初始化阶段才真正的赋初值
private static int a=1;

链接阶段只会将a赋值为0,在初始化阶段才赋值为1

  • 初始化过程只在第一次主动使用类的时候才发生,主动使用类情况如下:
    1. 创建类的实例(new)
    2. 初始化该类的子类
    3. 调用某个类的静态方法
    4. 访问某个类或者接口的静态属性,或对静态属性赋值
    5. 使用反射机制获取类的Class对象,比如Class.forName(“com.mysql.jdbc.Driver
      “)
    6. Java虚拟器标明为启动类的类
    7. JDK7以后使用动态语言机制解析的类

除上述情况外,都属于类的被动使用,不会对类进行初始化

  • 初始化代码只会被执行一次,即使是多线程环境下,编译器会自动为初始化代码进行加锁处理
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抽象类,但是因为功能不同,所以我们将自定义类加载器分为三类:

    1. 扩展类加载器(ExtensionsClassLoader):
      * 由Java语言编写,负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。
      * 如果将我们自己编写的类放在指定补录下,也可以被扩展类加载器所加载

    2. 系统类加载器/应用类加载器(SystemClassLoader/APPClassLoader):
      * 负责加载用户自定义的类(ClassPath类路径下的类)
      * 该类是程序中默认的类加载器,负责加载程序中的基础类
      * 用户自定义类加载器:如果我们有自定义类加载器的需求,可以继承ClassLoader抽象类,自定义我们的类加载器。

**注意:**引导类加载器,扩展类加载器,系统类加载器是由系统已定义好的,直接使用即可,而自定义类加载器需要自己实现加载时的代码逻辑,是自定义的。

下图为类加载器的继承关系:
类加载器之间的关系
类加载器之间的关系:
这几种类加载器之前并不是父子继承关系,而是包含关系,可以理解为上下级关系
类加载器之间的关系
其中: 可以使用ClassLoader的getParent()方法获取该加载器的上层加载器

  • 系统类加载器的上层加载器是扩展类加载器。
  • 扩展类加载器的上层加载器为引导类加载器,但是由于引导类加载器由C语言编写,所以获取到的结果为Null。
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对象是否为同一个类存在两个必要条件:

  • 类的完整类名必须一致, 包括包名。
  • 加载这个类的ClassLoader (指ClassLoader实例对象)必须相同。

也就是说:通一个字节码文件,在同一个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();
        }

    }
}

将字节码文件放到指定位置
字节码文件
运行结果:
运行结果
可以看到此时我们的自定义类加载器已经完成了对指定路径的类的加载!

除此之外,我们还可以通过多种方式加载类:从数据库加载,从网络加载,解密后加载等,只需完成相应的加载逻辑即可!

文章到此结束,下一章更新:运行输数据区,欢迎观看~

猜你喜欢

转载自blog.csdn.net/qq_42628989/article/details/104262244
今日推荐