文章目录
目录
[1. 无关性基石](#1. 无关性基石)
[2. Class类文件结构](#2. Class类文件结构)
[magic- 魔数](#magic- 魔数)
[3. 类加载机制](#3. 类加载机制)
[4. 类加载器](#4. 类加载器)
1. 无关性基石
Java在刚刚诞生之时曾经提出过一个非常著名的宣传口号 "一次编写, 到处运行"。它的底气就是Oracle公司和其他虚拟机发布商发布的许多虚拟机都可以运行在各种不同的硬件平台和操作系统上的虚拟机, 这些虚拟机都可以载入一种平台无关的字节码, 从而实现 "一次编写, 到处运行" 。
实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不和包括Java在内的任何语言进行绑定, 它只与 "class文件" 这种特殊的二进制文件所关联。

2. Class类文件结构
Class文件是一组以8个字节为基础单位的二进制流, 各个数据项目严格按照顺序紧凑地排列在文件之中, 中间没有添加任何的分隔符,这使得整个Class文件中存储的内容几乎全是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上的数据项时,则会按照高位在前的方式分割成若干8个字节进行存储。
根据 《Java虚拟机规范》 的规定,Class文件格式采用一种类似于c语言结构体的伪结构体来存储数据, 这种伪结构体只存在两种数据类型: "无符号数" 和"表"。
- 无符号数属于基本的数据类型, 使用u1,u2,u4,u8分别表示1,2,4,8个字节。
- 表是由多个无符号数或者其他表为数据项构成的复合数据类型。为了便于区分, 所有的表都以 "_info"结尾。
Class文件格式
|----------------|---------------------|-----------------------|----------|
| 类型 | 名称 | 数量 | 释义 |
| u4 | magic | 1 | 魔数 |
| u2 | minor_version | 1 | 次版本号 |
| u2 | major_version | 1 | 主版本号 |
| u2 | constant_pool_count | 1 | 常量池个数 |
| cp_info | constant_pool | constant_pool_count-1 | 常量池 |
| u2 | access_flags | 1 | 访问标志 |
| u2 | this.class | 1 | 类索引 |
| u2 | super.class | 1 | 父类索引 |
| u2 | interfaces_count | 1 | 接口索引集合数量 |
| u2 | interfaces | interfaces_count | 接口索引集合 |
| u2 | fields-count | 1 | 字段表集合个数 |
| field_info | fields | fields-count | 字段表集合 |
| u2 | methods_count | 1 | 方法表集合个数 |
| method_info | methods | methods_count | 方法表集合 |
| u2 | attributes_count | 1 | 属性表集合个数 |
| attribute_info | attributes | attributes_count | 属性表集合 |
准备一段简单Java代码
java
public class TestClass1 {
private int m;
public int inc(){
return m+1;
}
public static void main(String[] args) {
}
}
使用jclss工具查看编译后的字节码文件
magic- 魔数
每个字节码文件的前四个字节被称之为魔数(Magic Number) ,它的唯一作用就是确定这个文件是否为一个能被虚拟机接受的Class文件。不仅是Class文件,很多文件格式标准中都有使用魔数来进行身份识别的习惯。

Class文件的魔数值为 0xCAFEBABE(咖啡宝贝)
主副版本号
紧接着魔数的4个字节存储的是Class文件的版本号,第5和第6个字节是次版本号,第7和第8是主版本号。

常量池
紧接着主次版本号之后的是常量池入口, 常量池可以比喻为Class文件里的资源仓库。
常量池中主要存放两大类常量: 字面量 和 符号引用, 字面量比较接近于java语言层面的常量概念, 入文本字符串, 被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:
被模块导出或开放的包(Package)
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点和动态常量
常量池中每一项常量都是一个表。


访问标志
在常量池结束之后,紧接着的2个字节代表访问标志,这个标志用来识别一些类或者接口层面的访问信息。

类索引,父类索引与接口索引集合

字段
当前类或接口声明的字段信息

方法
当前类或接口声明的方法信息

属性
类的属性,比如源码的文件名,内部类的列表。

3. 类加载机制
类的生命周期
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载,验证,准备,解析,初始化,使用和卸载七个阶段,其中验证,准备,解析三部分统称为连接。

关于什么时候开始类加载过程的第一个阶段"加载" ,《Java虚拟机规范》 中并没有进行明确规定,这点根据虚拟机自行把控。但是对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有六种情况必须立刻对类进行"初始化"。
- 遇到new, getstatic, putstatic或invokestatic 字节按指令时,下面是遇到的情况。
- 使用new关键字实例化对象
- 读取或者设置一个类型的静态字段(被 final修饰, 已在编译期把结果放入常量池的静态字段除外)的时候
- 调用一个类型的静态方法的时候
-
使用 Java.lang.reflect 包的方法对类型进行反射调用的时候, 如果类型没有进行过初始化,则需要先触发其初始化。
-
当初始化类的时候,发现其父类还没有进行过初始化, 则需要先触发其父类的初始化。
-
当虚拟机启动时, 用户需要指定一个要执行的类(包含main()方法的那个类), 虚拟机会先初始化这个主类。
-
当使用jdk7 新加入的动态语言支持时
-
当一个接口中定义了Jdk1.8 新加入的默认方法时,如果有这个接口的实现类发生了初始化,那该接口就要在其前进行初始化。
类加载过程
加载
- 加载(Loading) 阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。
2)类加载器在加载完类之后,java虚拟机会将字节码中的信息保存到方法区中。
3)类加载完成类之后,java虚拟机会将字节码中的信息保存到方法区中。
生成一个instanceKlass对象,保存类的所有信息,里面还包含实现特定功能比如多态的信息。

4)同时,java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.Class对象。
作用是在java代码中去获取类的信息以及存储静态字段的数据(JDK8及之后)。


连接
验证
连接(Linking)阶段的第一个环节是验证,验证的主要目的是检测Java字节码文件是否遵守了《Java虚拟机规范》中的约束。这个阶段一般不需要程序员参与。
准备
准备阶段为静态变量(static)分配内存并设置初始值。

准备阶段只会给静态变量赋初始值,而每一种基本数据类型和引用数据类型都有其初始值。
|---------|-----------|
| 数据类型 | 初始值 |
| int | 0 |
| byte | 0 |
| short | 0 |
| long | 0L |
| char | '\u0000' |
| double | 0.0 |
| boolean | false |
| 引用数据类型 | null |
解析
解析阶段主要是将常量池中的符号引用替换为直接引用。

直接引用不在使用编号,而是使用内存中地址进行访问具体的数据。

初始化
初始化阶段会执行静态代码块中的代码,并为静态变量赋值。
初始化阶段会执行字节码文件中clinit部分的字节码指令。

4. 类加载器
Java虚拟机的设计团队有意把类加载阶段中的 "通过一个类的全限定名来获取描述该类的二进制字节流" 这个动作放到Java虚拟机的外部去实现,以便让程序自己去决定如何去获取所需的类。实现这个动作的代码被称之为 "类加载器"。
类与类加载器
类加载器虽然只用于实现类的加载动作,但它在java程序中起到的作用却远远超过类加载阶段。对于任意一个类来说,都必须由加载它的类加载器和它本身共同确认其在java中的唯一性。简单来说就是完全相同的类经过不同的类加载器进行加载,那么这两个类就必定不相等。
类加载器的分类
类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。

启动类加载器
启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟 机提供的、使用C++编写的类加载器。
默认加载Java安装目录/jre/lib下的类文件,比如rt.jar, tools.jar,resources.jar等。


扩展类加载器
扩展类加载器(Extension Class Loader)是JDK中提供的、 使用Java编写的类加载器。 默认加载Java安装目录/jre/lib/ext下的类文件。


应用程序类加载器
扩展类加载器和应用程序类加载器都是JDK中提供的、使用Java编写的类加载器。
它们的源码都位于sun.misc.Launcher中,是一个静态内部类。继承自URLClassLoader。具备通过目录 或者指定jar包将字节码文件加载到内存中。

双亲委派模型
站在Java虚拟机的角度来看,只有两种不同的类加载器: 一种是启动类加载器(Bootstrap ClassLoading),这个类加载器使用C++语言实现, 是虚拟机自身的一部分。另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。

双亲委派模型的工作过程是: 如果一个类加载器收到了类加载的请求, 它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载器的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类), 子加载器才会尝试自己去完成加载。
双亲委派模型解决什么问题?
-
- 重复的类: 如果一个类重复出现在三个类加载器的加载位置,那应该由谁来进行加载? 会不会出现重复? 启动类加载器加载,根据双亲委派模型,它的优先级是最高的,也不会出现重复
-
- 避免了系统类被覆盖的问题: 在自己项目中创建一个java.lang.String类, 会被加载吗?
- 不能,会交由启动类加载器加载在rt.jar包中的String类
-
- 类加载器的关系
- 应用类加载器的父类加载器是扩展 类加载器,扩展类加载器没有父类 加载器,但是会委派给启动类加载 器加载
双亲委派机制的作用
- 1.保证类加载的安全性 通过双亲委派机制,让顶层的类加 载器去加载核心类,避免恶意代码 替换JDK中的核心类库,比如 java.lang.String,确保核心类 库的完整性和安全性。
- 2.避免重复加载 双亲委派机制可以避免同一个类被 多次加载,上层的类加载器如果加 载过类,就会直接返回该类,避免 重复加载。
打破双亲委派模型
打破双亲委派机制的三种方式

打破双亲委派机制--自定义类加载器
先来分析ClassLoader的原理,ClassLoader中包含了4个核心方法。 双亲委派机制的核心代码就位于loadClass方法中。


正确的去实现一个自定义类加载器的方式是重写findClass方法,这样不会破坏双亲委派机制。

案例
一个Tomcat程序中是可以运行多个Web应用的,如果这两个应用中出现了相同限定名的类,比如Servlet类, Tomcat要保证这两个类都能加载并且它们应该是不同的类。
如果不打破双亲委派机制,当应用类加载器加载Web应用1中的MyServlet之后,Web应用2中相同限定名的 MyServlet类就无法被加载了。

Tomcat使用了自定义类加载器来实现应用之间类的隔离。 每一个应用会有一个独立的类加载器加载对应的类。

打破双亲委派机制的第二种方法
JDBC案例
1、启动类加载器加载DriverManager。
2、在初始化DriverManager时,通过SPI机制加载jar包中的myql驱动。
3、SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。
这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制。

打破双亲委派机制的第三种方法
OSGi模块化
历史上,OSGi模块化框架。它存在同级之间的类加载器的委托加载。OSGi还使用类加载器实现了热部署的 功能。 热部署指的是在服务不停止的情况下,动态地更新字节码文件到内存中。

总结
以上就是这篇博客的主要内容了,大家多多理解,下一篇博客见!