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

  • Post author:
  • Post category:其他


一、引入

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

一组 *.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 下的。所以不会加载我们自己写的。

这就是双亲委派的意义。



版权声明:本文为weixin_61567666原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。