[JVM]类加载的过程(学习总结)

发布于:2022-08-10 ⋅ 阅读:(312) ⋅ 点赞:(0)

一、引入

我们所写的代码就相当于:

一组 *.java 文件 + 一组资源文件(多么媒体文件、配置文件)+ 相关存储中的数据(MySQL、Redis...)

我们现在的程序数据放在硬盘中,要让运行时电脑运行我们的程序,实际就是让该电脑的CPU 运行我们程序中的指令数据。

但CPU 无法直接和硬盘(IO设备)做直接的数据交换

CPU 只能和内存中的数据打交道,现在数据又放在硬盘中

所以我们应该先把数据从硬盘中读取到内存中。

这个过程是以类为单位(某个*.class 文件)进行读取的

一次一个类文件的加载,按需进行加载(用到哪个加载哪个)

这个过程就被称为 类的加载(Class Load)。这个加载过程也得依赖某些指令来执行,这些指令(程序)就被称为类的加载器(ClassLoader)。

这些指令是属于 JVM 程序的一部分。换言之, JVM 中有进行必要的类的加载的职责。

二、类文件中的数据是怎么组织的(什么东西放在哪)

javac.exe 编译器是 A 电脑上进行的,靠着 JVM 的规范来约束

java.exe   JVM(运行期)是 B 电脑上进行

1.JDK

用于给 Java 开发人员使用的小包裹

  1. 提前准备好的程序(*.exe / *.dll)编译器、调试工具:编译器、调试工具、运行时的分析工具
  2. 官方提供的所有人都能使用的类文件 *.class

2.JRE:Java运行时的环境

用于给一般用户运行好别人写好的 Java 程序(以 *.class 为代表的数据文件,可以是*.jar / *.war...)的一组环境

  1. JVM(Java Virtual Machine) Java虚拟机:java.exe
  2. 运行期间支持运行的一组官方类文件 *.class

开发人员也会用到 JRE (不可能光开发,不去运行测试)

三、类文件中主要有哪些部分的数据

class file 格式一直在更新

javac.exe 5编译的类能不能用java.exe 17运行

<maven.compiler.source>1.8  :让编译器按照1.8(Java8)的标准检查 java 文件语法

<maven.compiler.tareget>1.8  :编译出来的类文件里,写明是 1.8(Java8)版本

常量池(很多常量)、类的基本属性、方法(构造、静态构造代码块)、签名 + 指令(语句)、字符串(字面量)、数字、符号

类文件的数据 = 类的信息 + 常量池 + 方法(方法的信息 + 指令(字节码形式))

ClassLoader 要加载一个类,主要就是要加载这些数据到内存中。 

四、JVM 会按需进行类的加载,什么情况下会加载一个类?

大前提:用到了一个类,但是这个类还不在内存中,就需要加载(如果已经在内存中,就没必要进行第二次加载)

什么叫用到:

  1. 使用一个类,进行对象的实例化时(构造对象时)
  2. 使用一个类时,会触发使用这个类的父类(父类:类的继承、接口的实现、接口的继承)
  3. 使用静态的内容(静态属性、静态方法)

class Main { psvm() { ... } }

java Main 

以 Main 类作为主类启动 JVM 。需要加载 Main 类,因为用到了 Main 类的静态方法(main 方法)

Object类 也需要加载,因为 Object 是 Main 的父类

五、类加载期间,ClassLoader 主要做了哪些工作?

类、接口、枚举、注解都在这块称为“类”

1.Loading(加载)

根据要加载的类名,找到对应的类文件( *.class)

验证类文件合法,没有错误(还得考虑安全问题,有可能写恶意的类也加载进来)

解析数据(按照规范格式)

2.Linking(链接)

类里面用到很多用字符串字面量写死的数据,比如“java/lang/Object”

但实际程序(JVM)执行中,需要的java.lang.Object 在内存中对应的数据

所以要把 com.lingqi.demo.Main 和 java.lang.Object 根据字面量“链接”起来

3.Initializing(初始化)

将类放到内存的指定位置后,进行类里的必要数据的初始化(主要是静态属性)

执行类的初始化工作

站在Java开发者的角度:我们的代码中的哪些东西是在这个阶段执行的?静态属性的初始化(赋值操作)、静态代码块 static { ... }

class Main {
    static int a = 10;  // = 10 是赋值的操作,是类的初始化阶段要做的

    static int b = callStaticMethod();  // = callStaticMethod() 操作,是类的初始化阶段要做的
                                        // 导致这个阶段,会去调用 callStaticMethod 方法

    static {    // 静态代码块/静态构造代码块
        System.out.println("hello");    // 这里的所有语句,是类的初始化阶段要做的
    }

    static int callStaticMethod() {
        return 2;   // 这个方法会被执行,只是因为 b = callStaticMethod() 导致的
    }

    static int someStaticMethod() {
        return 1;   // 这个方法不会被执行
    }

    public static void main(String[] args) {
        String s = "hello";
        System.out.println(s);
    }
}

 一定是先执行父类的初始化完成之后,才会进行子类的初始化。

六、关于类

1.类名:

俗称 :Main

权威类名:com.lingqi.demo.Main。更深一层就是 JVM 进行类的加载时,保证一个类只会在内存中存在一份(粗略地可以当成类只被加载一次(类是可以被卸载的))

JVM 内部:类加载器 + 权威类名,确定一个类是否存在

2.默认情况下有那些类加载器?

不同的类,由不同的类加载(因为类和类的地位的不平等)

Boostrap ClassLoader(启动类加载器):加载 SE 下的标准类(java.lang.String、java.util.List、java.io.InputStream)

Extenttion ClassLoader(扩展类加载器):加载 SE 下的扩展类

Application ClassLoader(应用类加载器):我们写类、我们通过 maven 或者其他工具引入的第三方类

举个例子:

如果有人提个要求,说让我加载一个类,那么我们怎么知道要加载的这个类的.class文件放在哪里呢?(比如说 rt.jar)

ClassLoader 肯定输入 JRE,所以知道自己被安装在哪 

                                                                                C:\Program Files\Java\jdk1.8.0_131\jre

所以去固定位置去找C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar

对于应用类加载器Application ClassLoader来说:

必须告诉它去哪里找,通过 ClassPath 变量告诉。java-classpath=路径,或者放在环境变量 ClassPath 中。  路径1/路径2/路径3。  ClassLoader 先去路径1下找,没找到再去 2 下,没找到再去 3,最后没找到,就会报异常(ClassNotFound,NoClassDef)

3.类的数据被加载到内存的什么位置了?

JVM 会管理一片内存(这片内存,JVM 是哪弄来的?JVM 是普通进程,从OS 申请过来的)

 内存分的区域:

  1. PC 寄存器(每个线程存在自己下一条指令的地址)
  2. JVM 栈(Java Virtual Machine Stacks)
  3. 本地方法栈 Native Method Stacks(和JVM 栈一起称为栈区)
  4. 堆(Heap)
  5. 方法区(Mathod Area)
  6. Run-Time Constant Pool(运行时常量池)

不同的JVM 实现中,可以有自己的实现。比如我们现在使用的都是 Oracle 提供的 Hotspot JVM。

除了上述的区域,还有一部分直接管理的内存,一般称为原生内存


类中的数据:类的信息、方法、常量池数据。

类的信息和方法组织成一个东西,叫做类信息,常量池其实也是类的信息。逻辑上,类信息被存到了方法区。常量池存在了运行时的常量池中,它也是类,实际上逻辑上也可以理解为,这也是方法区的一部分。

那么,我们简单理解就是,整个类的数据,都被放在了方法区之中了(基本信息、方法(所有的方法)、常量数据、静态属性)。但是这个阶段是不讨论具体的JVM 实现的。不同版本的 JVM 的具体实现实际上是不同的。所以深入探讨的时候,还需要根据实际情况来具体的分析。


七、小结

1.类文件放在哪里?

硬盘中,以文件的形式出现最为常见

2.类文件是怎么来的

经过编译器,将 java 源文件编译而来的

3.类文件中的主要数据有?

按照规范,主要有 基本信息(静态属性)、方法(方法信息、指令(字节码))、常量

4.为什么进行类的加载?

按照冯诺依玛体系,CPU 无法直接读取硬盘中的数据,需要先加载到内存中

5.为什么类文件要按需加载,并且以类为单位加载?

相对来说,节省内存空间,实现方便

6.类名是什么

①权威类名 = 包名 + 类名             ② 类加载器 + 权威类名

7.什么时候会去加载类(什么时候用到了一个类)?

实例化对象、访问静态属性、调用静态方法、子类调用到父类

8.类在内存中只会存在一份

正确

9.类的加载过程

加载、链接、初始化

10.类的初始化时会执行我们的哪些代码?

①属性的初始化赋值    ②静态构造代码块    父类的初始化一定在子类之前完成  按照书写顺序

11.类被加载到内存的什么位置?

逻辑上,放在方法区。但不同 JVM 的实现,可以有进一步讨论空间

12.默认的类加载器有哪些?

启动类加载器、扩展类加载器、应用类加载器

13.加载时,ClassLoader 怎么知道一个类对应的类文件所在?

启动、扩展类加载器根据固定位置找。应用类加载器,根据class path 的路径依次查找

14.如果加载时,一个类不存在,会出现异常(ClassNotFound、NoClassDef...)

八、双亲委派机制(⭐)

默认的三个类加载器之间遵守一个规范:

1.三个类加载器之间存在

2.Application ClassLoader  需要加载类的时候,先委派给双亲去加载

如果 parent 加载成功这个类了,你就不用加载了;否则自己再去加载

3.目的是放置加载进来用户写好的恶意代码

前提:一个类(A类)用到了其他类(B C D类),则其他的这些类(B C D )的加载动作,默认是由当时加载 A类的加载器来加载。

比如说com.lingqi.demo.Main 用到了 java.lang.String 类

public static void main(String[] args){
    String s = "hello";
    System.out.println(s);
}

 但是我还定义了一个自己的String 类

 那么我用的时候用的是 rt.jar 下的java.lang.String 还是我自己定义的?

那么双亲委派机制是如何解决这个问题的

1.com.lingqi.demo.Main 类被哪个类加载?—— ApplicationClassLoader 去加载

2.Main 用到了 java.lang.String ,则默认让 ApplicationClassLoader 去加载。

3.如果没有双亲委派,加载的就是我们自己写的 java.lang.String

4.如果有双亲委派,则 ApplicationClassLoader 优先让 BootStrapClassLoader 去加载,能加载到是 rt.jar 下的。所以不会加载我们自己写的。

这就是双亲委派的意义。

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到