JVM 手把手保姆级教程(3/3):类加载与字节码技术&内存模型

为了达到"傻瓜式"的阅读,方便更容易理解,本篇文章结合了两套视频进行总结的。
视频:黑马程序员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)

准备阶段,就是为类的静态变量分配内存,并将其初始化为默认值。
当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机会为这个类分配相应的内存空间,并设置初始值。

注意:

  1. 这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配内存了,准备阶段会显式赋值。
  2. 这里不会为实例变量分配初始化,类变量会分配在方法区,而实例变量会随着对象一起分配到堆中。

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被篡改

5. java内存模型

推荐视频

猜你喜欢

转载自blog.csdn.net/hpp3501/article/details/120881311