导航:
【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.准备
准备测试类 :
/*** @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命令编译成字节码文件:
javac Test.java
3.运行
运行class文件:
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 双亲委派模型的工作过程
工作过程:
-
检查父类加载器是否已经加载过这个类:JVM 会首先询问父类加载器是否已经加载了该类。如果已经加载过了,直接返回该类的 Class 对象。如果没加载过,则:
-
委派给父类加载器加载:如果父类加载器没有加载过该类,那么 JVM 将委托给父类加载器进行加载。每一层都是这样继续委派,直到达到最顶层的启动类加载器。
-
尝试加载类:如果父类加载器无法加载该类(即所有的父类加载器都无法加载),那么 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 就不再解析,而直接使用这个已经被解析过的直接引用。
示例:
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;
- 执行静态语句块:
- 检查执行父类静态代码块:假如该类的直接父类还没有被初始化,则先初始化其直接父类
- 执行静态代码块:假如类中有初始化语句,则系统依次执行这些初始化语句
类变量赋值的两种方式:
/*** @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命令来运行某个主类