为了达到"傻瓜式"的阅读,方便更容易理解,本篇文章结合了两套视频进行总结的。
视频:黑马程序员JVM完整教程、尚硅谷宋红康JVM全套视频
第一篇:JVM 手把手保姆级教程(1/3):内存结构
第二篇:JVM 手把手保姆级教程(2/3):垃圾回收
第三篇:JVM 手把手保姆级教程(3/3):类加载与字节码技术&内存模型
JVM最后一篇了,学的有点累,写的也有点累了。放弃很容易,但坚持到底一定会有所收获。
1. 类文件结构 【了解即可,不想看的直接跳过】
1.1 字节码
源代码经过编译后会产生一个字节码文件*.class,字节码是一种二进制的类文件。它的内容是JVM的指令,而不像C、C++那样编译后直接生成机器码。
解读字节码文件
- 方式一:Notepad++
Notepad++ 下载插件Hex-Editor可以打开*.class文件
- 方式二:Idea编辑器下载可视化插件jclasslib
代码经过编译后,光标放到需要查看的类上,通过 View->Show Bytecode With Jclasslib查看
- 方式三:通过javap指令:jdk自带的反解析工具
1.2 类文件结构
根据 JVM 规范,类文件结构如下:
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
类型 | 名称 | 说明 | 长度 | 数量 |
---|---|---|---|---|
u4 | magic | 魔数,识别Class文件格式 | 4个字节 | 1 |
u2 | minor_version | 副版本号(小号) | 2个字节 | 1 |
u2 | major_version | 主版本号(大号) | 2个字节 | 1 |
u2 | constant_pool_count | 常量池计数器 | 2个字节 | 1 |
cp_info | constant_pool | 常量池表 | n个字节 | constant_pool_count-1 |
u2 | access_flags | 访问标志 | 2个字节 | 1 |
u2 | this_class | 类索引 | 2个字节 | 1 |
u2 | super_class | 父类索引 | 2个字节 | 1 |
u2 | interfaces_count | 接口计数器 | 2个字节 | 1 |
u2 | interfaces | 接口索引集合 | 2个字节 | interfaces_count |
u2 | fields_count | 字段计数器 | 2个字节 | 1 |
field_info | fields | 字段表 | 2个字节 | fields_count |
u2 | methods_count | 方法计数器 | 2个字节 | 1 |
method_info | methods | 方法表 | 2个字节 | methods_count |
u2 | attributes_count | 属性计数器 | 2个字节 | 1 |
attribute_info | attributes | 属性表 | n个字节 | attributes_count |
根据上表按照顺序去对应字节码文件,下图是一个完整的对应详情。前四个字节是魔数,第五、六字节是副版本号等等。
总结一下类文件结构主要包括以下部分:
- 魔数
- Class的文件版本
- 常量池
- 访问标志
- 类索引、父类索引
- 接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
1.2.1 魔数(Magic Number)
- 每个Class开头的前四个字节(ca fe ba be)
- 它的唯一作用就是校验字节码文件的格式
- 使用魔数而不使用扩展名来校验文件格式,是出于安全考虑,因为文件扩展名可以随意改动。
1.2.2 Class文件版本号
- 紧接着魔数后面的四个字节存储的是Class文件版本号。第5、6字节是编译的副版本号(minor_version),第7、8字节是编译的主版本号(major_version)
- 它们共同组成了Class文件的版本号,比如副版本号为m,主版本号为M,那么该Class文件的版本号就是M.m
- 版本号和Java编译器的对应关系如下图:
下图的十六进制34对应的十进制是52,索引JDK可以推断出是1.8版本的。
1.2.3 常量池
1.2.3.1 常量池计数器
- 由于常量池的数量不固定,所以需要放置两个字节来表示常量池容量计数值。
- 常量池容量计数值:从1开始,表示常量中有多少项常量。即constant_pool_count=1表示常量池中有0项常量
1.2.3.2 常量池表
- constant_pool是一种表结构,以1到constant_pool_count-1为索引。
- 常量池主要存放两大常量类:字面量(Literal)和符号引用(Symbolic References)
- 它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第1个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte。
1.2.4 其他的自己百度去吧
2.类加载阶段
2.1 类的生命周期
在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。
按照Java虚拟机规范,从Class字节码文件加载到内存中,到类从内存中卸载为止,它的整个生命周期包括如下7个阶段:
- 加载(Loading)
- 链接(Linking)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
其中链接又分为三个阶段:验证、准备、解析。
2.2 加载(Loading)
概述:
加载,就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型—类的模板对象。所谓类模板对象,其实就是Java类在JVM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。
反射的机制就是基于以上逻辑。如果JVM没有将Java类的声明信息存储起来,则JVM在运行期也无法发射。
加载的流程:
加载阶段,简而言之就是查找并加载类的二进制数据,生成Class的实例。
在加载类时,Java虚拟机必须完成以下3件事情:
- 通过类的全名(包名+类名),获取类的二进制数据流。
- 解析类的二进制数据流为方法区内的数据结构(Java类模型)
- 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据访问的入口。
类模型的位置:
加载的类在JVM中创建相应的类结构,类结构会存储在方法区(JDK1.8之前:永久代,JDK1.8之后:元空间)。
Class实例的位置:
.class文件加载至元空间后,会在堆中创建一个java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象。
2.3 链接(Linking)
2.3.1 验证(Verification)
当类加载到系统后,就开始链接操作,验证就是链接的第一步。它的目的是保证加载的字节码是否复合JVM规范、安全检查。
2.3.2 准备(Preparation)
准备阶段,就是为类的静态变量分配内存,并将其初始化为默认值。
当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机会为这个类分配相应的内存空间,并设置初始值。
注意:
- 这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配内存了,准备阶段会显式赋值。
- 这里不会为实例变量分配初始化,类变量会分配在方法区,而实例变量会随着对象一起分配到堆中。
2.3.3 解析(Resolution)
解析阶段,就是将类、接口、字段和方法的符号引用转为直接引用(举例:符号引用类似菜谱,上面写着做菜的每一步。直接引用就是按照菜谱上的步骤具体的去做菜)。
2.4 初始化阶段
1. 描述
初始化阶段就是为类的静态变量赋予正确的默认值。类初始化阶段的重要工作就是执行类的初始化方法:<clinit>()方法。
类的初始化时类装载的最后一个阶段。如果前面的步骤都没问题,那么表示该类可以顺利装载到系统中。此时,类才会开始执行Java字节码。(到了初始化阶段,才开始执行类中定义的java程序代码)。
关于<clinit>方法:
- 该方法只能由Java编译器生成并由JVM调用,程序员无法自定义一个同名的方法,也无法去调用该方法。
- 它是由类静态成员的赋值语句已经static语句块合并产生的。
2. 说明
2.1 在加载一个类之前,虚拟机总是会试图先加载该类的父类,因此父类的<clinit>方法总是在子类的<clinit>方法前被调用。也就是说,父类的static块优先级高于子类。
口诀:由父及子,静态先行。
2.2 Java编译器并不会为所有类都产生<clinit>初始化方法,以下几种情况编译后的字节码文件里不会包含<clinit>方法
- 一个类中没有声明任何的静态类变量,也没有静态代码块
- 一个类中声明类变量,但是没有明确使用类变量的初始化语句,也没有静态代码块来执行初始化操作
- 一个类中包含static final修饰的基本数据类型的字段,这些字段初始化语句采用编译时常量表达式
3. 类加载器
- 除了顶层的类加载器外,所有的类都应有自己的“父类”加载器
- 上下级加载器不是继承的关系,而是包含的关系。下层加载器中包含上层加载器的引用。
3.1 启动类加载器
- 使用c/c++语言实现的,嵌套在JVM内部。
- 用来加载java核心类库($JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供JVM自身需要的类。
- 不继承自java.lang.ClassLoader,没有父加载器。
- 出于安全考虑,Boostrap启动类加载器只加载包名为java、javax、sun等开头的类。
使用-XX:+TraceClassLoading参数可以追踪类加载的过程。
3.2 扩展类加载器
- java语言编写。
- 继承自java.lang.ClassLoader。
- 父类加载器为启动类加载器。
- 负责加载$JAVA_HOME/jre/lib/ext目录下的类库或系统属性java.ext.dirs指定路径下的类库。
3.3 系统类加载器
- java语言编写
- 继承自java.lang.ClassLoader。
- 父类加载器为扩展类加载器。
- 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库。
- 应用程序中的类加载器默认是系统类加载器。
- 它是用户自定义类加载器的默认父加载器
- 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器。
3.4 用户自定义类加载器
- 在Java日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。
- 自定义类加载器可以实现应用隔离。比如Tomcat、Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义类加载器隔离不同的组件模块。
- 自定义类加载器通常需要继承ClassLoader类。
4. 双亲委派机制
4.1 定义
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存并生成class对象。而且加载某个类的class文件时,Java虚拟机采用的时双亲委派模式,即把请求优先交由父类处理,它是一种任务委派模式。
4.2 工作原理
先看个例子
首先在代码中创建一个java.lang包,然后再该包下创建String类,在自定义类(A)的main方法中调用String类,我们可以发现系统最终调用的不是我们自定义的String而是系统类中的java.lang.String。
以上例子就涉及双亲委派机制,首先我们自定义的类A使用的时系统类加载器,它加载String类的时候先委派给扩展类加载器,扩展类加载器又委派给启动类加载器,而启动类加载器恰恰时加载java开头的类的,所以A类中使用的String对象是由启动类加载的核心类库中的String,而不是我们自定义的String。
工作原理
- 如果一个类加载器收到了类加载请求,它并不会直接去加载该类,而是把这个请求委托个父类去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,直到请求最终达到顶层的启动类加载器;
- 如果父类加载器能完成加载操作就成功返回,否则的话,子类才会尝试去加载,这就是双亲委派机制。
4.3 优势
- 避免类的重复加载
- 保护程序安全,防止核心API被篡改