Java的类是怎样在虚拟机中加载的?详细阐述JVM的加载、验证和解析过程

ops/2024/9/23 0:04:42/

导航:

【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/黑马旅游/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码

目录

一、类加载过程概述

二、加载

2.1 基础概念

2.1.1 类加载

2.1.2 类的Class对象 

2.1.3 类加载子系统

2.1.4 双亲委派模型  

2.1.4.1 JVM三个默认类加载器

2.1.4.2 双亲委派模型的工作过程

2.2 类加载的具体过程

2.2.1 获取类的字节码文件

2.2.2 静态结构转运行时结构

2.2.3 生成类的Class对象

三、链接:验证、准备、解析

四、初始化


 

一、类加载过程概述

类加载过程:加载、链接(验证、准备、解析)、初始化。

  • 加载:生成类的Class对象,作为这个类各种数据的访问入口。
  • 链接:将类的二进制数据合并到JRE(即Java运行环境,等于JVM+Java程序运行所需的类库)中。
  • 初始化:给类中的类变量赋初值、执行静态语句块。

二、加载

2.1 基础概念

2.1.1 类加载

类加载:类加载就是Java 虚拟机(JVM)将类的字节码加载到内存中,并对其进行初始化的过程。此阶段最终会生成类的Class对象,作为这个类各种数据的访问入口

我们都知道,每个执行过的Java文件都会经历编译、运行两个阶段,这两个阶段分别对应JDK的bin目录下javac.exe、java.exe两个文件:

  • javac.exe:将我们写的类编译成.class文件(即字节码文件);
  • java.exe:运行这个.class文件。

通过javac.exe、java.exe编译、运行Java文件:

1.准备

准备测试类 :

java">/*** @Author: vince* @CreateTime: 2024/04/20* @Description: 测试类:Hello world* @Version: 1.0*/
public class Test {public Test() {}public static void main(String[] args) {System.out.println("hello~~~~~~~");}
}

2.编译

打开cmd窗口,使用javac命令编译成字节码文件

java">javac Test.java

3.运行

运行class文件:

java">java Test

2.1.2 类的Class对象 

类的Class对象:在运行期间,JVM虚拟机会把.class文件中的类信息(变量、方法等信息)加载进内存中,并解析生成类的Class对象。通过这个类的Class对象,我们可以获取到类的各种信息。

类的字节码文件和Class对象的区别:

  • 类的class字节码文件是编译时生成的,类的class对象是运行时生成的。
  • 类的字节码文件是一个存储在电脑硬盘中的文件,例如Test.class;类的Class对象是存放在内存中的数据,可以快速获取其中的信息;
  • 两者都存储类的各种信息;

 类的字节码文件详解:

JDK编译生成的.class字节码文件是什么?从底层结构到代码验证,深度解析Java字节码文件-CSDN博客

2.1.3 类加载子系统

整个加载过程是在JVM中的类加载子系统完成的。 

类加载子系统:通过类加载机制加载类的class文件,如果该类是第一次加载,会执行加载、验证、解析。只负责class文件的加载,至于是否可运行,则由执行引擎决定。

JVM中,类加载过程是在类加载子系统完成的。

类加载过程:加载 --> 链接(验证 --> 准备 --> 解析) --> 初始化

类加载子系统详细参考:什么是JVM的内存模型?详细阐述Java中局部变量、常量、类名等信息在JVM中的存储位置-CSDN博客

类加载子系统采用了双亲委派模型,通过一系列的类加载器来实现类加载任务。 

2.1.4 双亲委派模型  

双亲委派模型:当一个类加载器接收到加载类的请求时,它首先会将这个请求委派给其父类加载器处理,只有在父类加载器无法完成加载任务时,才会由该类加载器自己去加载类。 

2.1.4.1 JVM三个默认类加载器
  • 启动类加载器BootStrapClassLoader(最顶端):
    • 加载内容:负责加载java的核心类库,包括java.lang包中的类等。底层使用C++实现(不会继承ClassLoader),是虚拟机自身的一部分。
    • 不能被直接引用:因为是C++实现的,所以无法被Java程序直接引用,只能加载委派过来的请求。这些类库存放在 JAVA_HOME\lib(具体解释看下文) 目录下,或者被 -Xbootclasspath 参数指定的路径中。(启动类加载器主要加载java的核心类库,即加载lib目录下的所有class)
  • 扩展类加载器ExtClassLoader:
    • 加载内容:负责加载JAVA_HOME\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有类库。
    • 可以被直接引用:它可以直接用来加载类,也可以通过委派加载类。Ext是Extract缩写,译为扩展、提取。
  • 应用程序类加载器AppClassLoader(最低端):
    • 加载内容:负责加载类路径的所有类库,在大多数情况下,我们编写的 Java 程序都是由这个类加载器加载的。
    • 可以被直接引用:可以直接在代码中使用这个类加载器

JAVA_HOME\lib:

是 JDK(Java Development Kit)安装目录下的一个子目录,其中包含了 Java 核心类库,包括一些 Java 的基础类和工具类等。这些类库是 Java 编程语言的基础,为 Java 程序的运行提供了必要的支持。

一些常见的在 JAVA_HOME\lib 目录下的重要文件包括:

  • rt.jar:Java 运行时的核心库,包含了 Java 核心类库的大部分内容,如 java.lang、java.util 等。
  • charsets.jar:包含了字符集支持的类库。
  • jfxrt.jar:JavaFX 运行时的核心库。
  • tools.jar:包含了一些 Java 开发工具的类库,如编译器、调试器等。
  • dt.jar:包含了 Java 开发工具包的类库,如图形界面工具等。

在编译和运行 Java 程序时,这些类库会被 JVM 的 Bootstrap ClassLoader 加载,以便程序能够使用 Java 核心类库提供的功能。

 

JAVA_HOME\lib\ext:

是 JDK(Java Development Kit)安装目录下的一个子目录,用于存放 Java 的扩展类库。这些类库提供了一些 Java 平台的扩展功能,如 XML 解析、网络协议、加密解密等。

在 JAVA_HOME\lib\ext 目录下,通常会包含一些 JAR 文件,这些文件是扩展类库的实现。一些常见的扩展类库包括:

  • dnsns.jar:DNS 名称服务提供者实现。
  • jaccess.jar:Java 访问桥实现。
  • ldapsec.jar:LDAP 安全实现。
  • sunjce_provider.jar:Sun 的 JCE(Java Cryptography Extension)提供者实现。
  • sunpkcs11.jar:Sun 的 PKCS#11 提供者实现。

这些扩展类库提供了一些 Java 平台的高级功能,但并不是所有的 Java 运行时环境都会使用到。通常情况下,如果你需要使用这些扩展功能,你可以将相应的 JAR 文件添加到类路径中,以便 Java 程序能够访问到这些功能。

2.1.4.2 双亲委派模型的工作过程

工作过程:

  1. 检查父类加载器是否已经加载过这个类:JVM 会首先询问父类加载器是否已经加载了该类。如果已经加载过了,直接返回该类的 Class 对象。如果没加载过,则:

  2. 委派给父类加载器加载:如果父类加载器没有加载过该类,那么 JVM 将委托给父类加载器进行加载。每一层都是这样继续委派,直到达到最顶层的启动类加载器

  3. 尝试加载类:如果父类加载器无法加载该类(即所有的父类加载器都无法加载),那么 JVM 将尝试使用自己的类加载器来加载类。

实际流程:

JVM在加载一个类时,会调用应用程序类加载器的loadClass()方法来加载这个类,不过在这方法中,会先使用扩展类加载器的loadClass()方法来加载类,同样扩展类加载器的loadClass()方法中会先使用启动类加载器来加载类;

如果启动类加载器加载到了就直接成功,如果启动类加载器没有加载到,那扩展类加载器就会自己尝试加载该类,如果没有加载到,那么则会由应用程序类加载器来加载这个类。 

双亲委派模型的作用:

  • 避免类的重复加载:无论哪一个类加载器要加载某类,最终都是委派最顶端的启动类加载器
  • 防止核心API被篡改:如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。

类路径:

classpath:类路径classpath是编译之后的target文件夹下的WEB-INF/class文件夹。内容等同于打包前的src.main.java和src.main.resource下的目录和文件

classpath* :不仅包含class路径,还包括jar文件中(class路径)进行查找. 

2.2 类加载的具体过程

2.2.1 获取类的字节码文件

类加载过程中的第一步:通过类的全限定名获取定义此类的二进制字节流(即类的.class文件)。

  • 类的全限定名:即"包名.类名",例如Object类的全限定名是java.lang.Object 。包名的各个部分之间,包名和类名之间, 使用点号分割。 
  • 类的二进制字节流:即类的字节码文件,是一组以8个字节(64位)为基础单位的二进制流,各个单位内部及之间都排列紧凑,中间没有添加任何分隔符和空隙,这使得整个Class文件中存储的内容几乎都是程序运行的必要数据。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8个字节进行存储,保证每个基础单位只有8个字节。

类的字节码文件详解:

JDK编译生成的.class字节码文件是什么?从底层结构到代码验证,深度解析Java字节码文件-CSDN博客

2.2.2 静态结构转运行时结构

类加载过程中的第二步:将这个.class字节码文件静态存储结构,转化为方法区的运行时数据结构

  • 静态存储结构:二进制文件,存储内容,存储内容包括魔数、版本号、常量池、访问标识、类索引、字段表、方发表、属性表
  • 运行时数据结构:存储在内存中的JVM内存模型中的运行时数据区-方法区,存储内容是类常量池、运行时常量池、字符串常量池,存储形式是永久代(JDK7及之前)和元空间(JDK8及之后)。

此步骤中,JVM会将类常量池的部分符号引用放入运行时常量池。

字节码文件的静态存储结构:
JDK编译生成的.class字节码文件是什么?从底层结构到代码验证,深度解析Java字节码文件-CSDN博客

运行时数据区:在程序运行时,存储程序的内容(例如字节码、对象、参数、返回值等)。

运行时数据区包括本地方法栈、虚拟机栈、方法区、堆、程序计数器。

在运行时数据区中,只有方法区和堆是各线程共享的进程内存区域,其他运行区都是每个线程可以独立拥有的。

图示:

详细参考:什么是JVM的内存模型?详细阐述Java中局部变量、常量、类名等信息在JVM中的存储位置-CSDN博客

2.2.3 生成类的Class对象

在内存中生成类的java.lang.Class对象,作为方法区这个类各种数据的访问入口。

类的 Class 对象:类的 Class 对象是 Java 中用于表示类的元数据信息的对象,它包含了关于类的结构、字段、方法等各种信息,同时也提供了一些方法操作这些信息。Class 对象在 Java 反射和动态代理等场景中被广泛使用,可以在代码运行期间,动态获取和操作类的信息。

常用方法:

  • 获取类名、包名:通过 getName() 方法获取类的全限定名,通过 getPackage() 方法获取类所在的包信息。
  • 获取类的字段、方法:通过 getFields() 和 getDeclaredFields() 方法获取类的字段信息,通过 getMethods() 和 getDeclaredMethods() 方法获取类的方法信息。
  • 获取类的父类和接口信息:通过 getSuperclass() 方法获取类的父类信息,通过 getInterfaces() 方法获取该类所实现的接口信息。
  • 获取类的对象:通过 newInstance() 方法实例化类,获取类的对象。
  • 获取类的类型:通过 isInterface() 方法判断类是否是接口,通过 isArray() 方法判断类是否是数组类型,通过 isPrimitive() 方法判断类是否是基本数据类型等。

获取Class对象的三种方法:

  • 类名 .class 字面量:在程序中直接使用类的 .class 字面量可以获取该类的 Class 对象。例如:Class<?> clazz = MyClass.class;
  • 已有对象的 getClass() 方法:如果已经有该类的对象实例,可以通过调用对象的 getClass() 方法来获取该类的 Class 对象。例如:Class<?> clazz = obj.getClass();
  • Class.forName(类全限定名) :可以通过类的全限定名使用 Class.forName() 方法来获取该类的 Class 对象。例如:Class<?> clazz = Class.forName("com.example.MyClass");

注意类的class对象是运行时生成的,类的class字节码文件是编译时生成的。

类的字节码文件和Class对象的区别:

  • 生成时机:类的class字节码文件是编译时生成的,类的class对象是运行时生成的。
  • 本质:类的字节码文件是个文件,类的Class对象是个对象。文件存储在电脑硬盘中,例如Test.class;对象存放在内存中,可以快速获取其中的信息;
  • 存储内容:两者都存储类的各种信息;

三、链接:验证、准备、解析

链接:将类的二进制数据合并到JRE中。该过程分为以下3个阶段:

验证:确保代码符合JAVA虚拟机规范和安全约束。包括文件格式验证、元数据验证、字节码验证、符号引用验证。

  • 文件格式验证:验证字节码文件是否符合规范。
    • 魔数:是否魔数0xCAFEBABE开头。魔数是每个Class文件的头4个字节。唯一作用是确定这个文件是不是一个能被虚拟机接受的Class文件。cafeBabe可以翻译为咖啡宝贝。
    • 版本号:版本号是否在JVM兼容范围。JVM可以通过版本号判断这个字节码文件所对应的JDK版本。版本号从45开始,JDK1.1之后每个大版本的主版本号向上加1,例如JDK2.0的主版本号46,JDK8的版本号52(转为16进制是0x34 hex)。高版本的JDK能够向下兼容更低版本的Class文件,但不能运行更高版本的Class文件。所以虚拟机会拒绝执行超过其版本号的Class文件。
      • 次版本号(Minor Version):第5和第6个字节。
      • 主版本号(Major Version):第7和第8个字节。数据类型为u2,即占两个字节
    • 常量类型:类常量池里常量类型是否合法
    • 索引值:索引值是否指向不存在或不符合类型的常量。
  • 元数据验证:元数据是字节码里类的全名、方法信息、字段信息、继承关系等。
    • 标识符:验证类名接口名标识符有没有符合规范
    • 接口实现方法:有没有实现接口的所有方法
    • 抽象类实现方法:有没有实现抽象类的所有抽象方法
    • final类:是不是继承了final类。
  • 指令验证:主要校验类的方法体,通过数据流和控制流分析,保证方法在运行时不会危害虚拟机安全。
    • 类型转换:保证方法体中的类型转换是否有效。例如把某个类强转成没继承关系的类
    • 跳转指令:保证跳转指令不会跳转到方法体以外的字节码指令上;
    • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。
  • 符号引用验证:确保后面解析阶段能正常执行。
    • 类全限定名地址:验证类全限定名是否能找到对应的类字节码文件
    • 引用地址:引用指向地址是否存在实例
    • 引用权限:是否有权引用

准备:为类变量(即static变量)分配内存并赋零值。

解析:将方法区-运行时常量池内的符号引用(类的名字、成员名、标识符)转为直接引用(实际内存地址,不包含任何抽象信息,因此可以直接使用)。

  • 符号引用:符号引用,顾名思义,就是一个符号,符号引用被使用的时候,才会解析这个符号。如果熟悉linux或unix系统的,可以把这个符号引用看作一个文件的软链接,当使用这个软连接的时候,才会真正解析它,展开它找到实际的文件。
  • 直接引用:实际内存地址。当一个类被加载时,该类所用到的别的类的符号引用都会保存在常量池,实际代码执行时,首次遇到某个别的类时,JVM会对常量池的该类的符号引用展开,转为直接引用,这样下次再遇到同样的类型时,JVM 就不再解析,而直接使用这个已经被解析过的直接引用。

示例:

java">        String s1="test1";String s2="test2";// s1,s2相当于符号引用:在编译时,JVM并没有解析s1;// 在运行时第一次加载这个类时,JVM会将s1和s2这两个符号引用解析为直接引用System.out.println(s1+s2);// "test1","test2"相当于直接引用:直接能访问到两个字符串的内存地址System.out.println("test1"+"test2");

四、初始化

初始化步骤:

  • 类变量赋初值:声明类变量时,直接初始值,例如public static int age=23;
  • 执行静态语句块:
    • 检查执行父类静态代码块:​​​​​假如该类的直接父类还没有被初始化,则先初始化其直接父类
    • 执行静态代码块:假如类中有初始化语句,则系统依次执行这些初始化语句

类变量赋值的两种方式:

java">/*** @Author: vince* @CreateTime: 2024/04/23* @Description: 类变量赋初值* @Version: 1.0*/
public class User {// 声明类变量并直接赋初值public static int age=23;// 在静态代码块中指定初始值public static int weight;static {weight=23;}}

类什么情况下会被初始化?

只有当对类的主动使用的时候才会导致类的初始化。

类会被初始化的场景:

  • new实例化:当通过 new 关键字创建类的实例时,该类会被初始化。这包括了显式地使用构造器创建对象,以及通过反射机制调用类的构造器来创建对象。
  • 访问类的静态成员:当访问类的静态字段(类变量)或静态方法时,如果该类还没有被初始化,则会触发类的初始化。
  • 反射:通过 Class.forName() 方法加载类时,如果指定了 initialize 参数为 true,则会触发类的初始化。
  • 子类初始化:当初始化一个类的子类时,如果该子类的父类还没有被初始化,则会先触发父类的初始化。
  • 启动类:Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类


http://www.ppmy.cn/ops/14509.html

相关文章

maven compile无效的标记: --release

看了好几篇文章&#xff0c;都没有提到这个解决办法&#xff0c;决定写一篇文章。 如果你的项目是多模块的&#xff0c;可以参考下&#xff0c;这个问题应该是切换JDK版本导致&#xff0c;maven编译时依赖了不符合运行时的JDK版本。解决办法也很简单&#xff0c;找到maven sett…

js面试---闭包、作用域及作用域链、执行上下文

1、什么是闭包 闭包是指有权访问另一个函数作用域中变量的函数&#xff0c;创建闭包的最常见的方式就是在一个函数内创建另一个函数&#xff0c;创建的函数可以访问到当前函数的局部变量。 闭包的作用&#xff1a; a、使我们在函数外部能够访问到函数内部的变量。通过使用闭包…

微带线设计细节的模拟仿真分析

微带线设计在很多PCB设计场景中被应用&#xff0c;但是&#xff0c;有些工程师往往并不注重设计细节&#xff0c;导致最后的设计指标与预期相差甚远&#xff0c;比如设计中&#xff0c;会将丝印、散热金属等放在走线的上方&#xff0c;设计检查时会有人产生质疑&#xff0c;至于…

Spring Boot集成zipkin快速入门Demo

1.什么zipkin Zipkin是一款开源的分布式实时数据追踪系统&#xff08;Distributed Tracking System&#xff09;&#xff0c;基于 Google Dapper的论文设计而来&#xff0c;由 Twitter 公司开发贡献。其主要功能是聚集来自各个异构系统的实时监控数据。Zipkin默认支持Http协议&…

全国各省市建设工程类专业职称评审要求总结(欢迎补充完善、沟通交流)

全国各省市建设工程类专业职称评审要求汇总统计如下&#xff0c;总体来说北京最难&#xff0c;经济欠发达、偏远地区评审要求相对简单&#xff0c;每个地方的要求存在一定的相似性&#xff0c;但又都各具特色&#xff0c;基本上来说论文是评审的必备条件&#xff0c;但是各个地…

网络初识

网络 局域网 一个区域的网 广域网 相对概念&#xff0c;没有绝对的界限&#xff0c;全世界现在最大的广域网&#xff0c;就教做TheInternet&#xff0c;万维网 路由器 交换机和路由器&#xff0c;都是用来组建网络的重要设备 交换机 上网的设备&#xff08;电脑/手…

Okapi Framework

文章目录 关于 OkapiRainbowCheckMateRatelTikalFilters Plugin for OmegaTLonghorn 关于 Okapi 官网&#xff1a;http://okapiframework.org源码&#xff1a;https://bitbucket.org/okapiframework/okapi/src文档&#xff1a;http://okapiframework.org/wiki/index.php?titl…

Python重点数据结构基本用法

Python重点数据结构用法 运算符描述[] [:]下标&#xff0c;切片**指数~ -按位取反, 正负号* / % //乘&#xff0c;除&#xff0c;模&#xff0c;整除 -加&#xff0c;减>> <<右移&#xff0c;左移&按位与^ < < > >小于等于&#xff0c;小于&#…