本文在文章底部有配套学习视频
前言
在前面我们介绍了JVM的核心三部分:类加载器、字节码解释器、运行时数据区。
这一节我们来介绍下类加载器。
一、类加载过程
多个java文件经过编译打包生成可运行jar包,最终由java命令运行某个主类的main函数启动程序,这里首先需要通过类加载器把主类加载到JVM,主类在运行过程中如果使用到其它类,会逐步加载这些类。
注意:对于我们自己写的jar包里的类不是一次性全部加载的,是使用到时才加载。
类加载到使用整个过程有如下几步:
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载 :
(1)加载:在硬盘上查找并通过IO读入字节码文件,将class字节码文件加载到内存中,并将这些数据转换成方法区中的运行时数据(静态变量、静态代码块、常量池等),在堆中生成一个Class类对象代表这个类(反射原理),作为方法区类数据的访问入口。
注意:使用到类时才会加载,例如调用类的main()方法,new对象等。
(2)验证:验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全(验证字节码的正确性)。
(3)准备:为类的静态变量(static filed)在方法区分配内存,并赋默认初值(0值或null值)。如static int a = 100 ;静态变量a就会在准备阶段被赋默认值0。
对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。
另外,静态常量(static final filed)会在准备阶段赋程序设定的初值,如static final int a = 666; 静态常量a就会在准备阶段被直接赋值为666,对于静态变量,这个操作是在初始化阶段进行的。
(4)解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用(将类的二进制数据中的符号引用换为直接引用)。
(5)初始化:类的初始化的主要工作是为静态变量赋程序设定的初值。
如static int a = 100;在准备阶段,a被赋默认值0,在初始化阶段就会被赋值为100。
说明:而类的实例化是指在类完全加载到内存中后,创建对象的过程。
二、一个小栗子来理解类类加载过程
通过概念,我们对于类加载过程,可能还是有点不是很懂,接下来我们来看下一个类。
2.1 编写一个Math.java代码
编写一个简单的Math.java代码如下:
public class Math { public static int initData = 666; public int compute(){// int a = 1; int b = 2; int c = (a+b)*10; return c; } public static void main(String[] args) { Math math = new Math(); int rs = math.compute(); System.out.println(rs); }}
说明:上面这个代码很简单,就是定义了静态变量initData和compute()和main方法。
2.2 编译源码为字节码文件Math.class
这个我们使用的IDE会自己进行编译成Math.class文件,我们自己也可以使用命令编译javac Math.java。
2.3 执行字节码文件
使用java Math.class就可以执行字节码文件了,最终会输出一个结果,但在执行之前会有一个我们上面讲到的类加载过程。
2.4 第一步:加载
Math.class是在我们的磁盘上的,所以类加载器需要通过IO从磁盘读取Math.clss字节码文件,将Math.class字节码文件加载到内存,并将这些数据转换成方法区中的运行时数据(静态变量、静态代码块、常量池等),在堆中生成一个Class类对象代表这个。
2.5 第二步:验证
这个主要是验证字节码文件的正确性。
我们打开这个文件看下,我们可以使用一些工具打开Math.clss,注意使用十六进制的进行查看,要是直接打开是二进制文件,你是看不懂的哦。
在Mac下可以使用Hex Fiend打开二进制文件,下载地址:
https://github.com/ridiculousfish/HexFiend/releases
另外一种简单的方式就是使用vi进行打开(这是Mac哦),主要两步操作(相信玩Mac的都能看得懂):
(1)首先以二进制方式编辑这个文件
vi -b Math.clss
(2)使用xxd转换为16进制
:%!xxd
(window可以使用Notepad++,需要安装一个HEX-Editor插件)
这些十六进制的数据都是有意义的,是不能随意篡改的。
比如:
cafebabe(魔数) : 代表该文件是一个字节码文件,我们平时区分文件类型都是通过后缀名来区分的,不过后缀名是可以随便修改的
画外音:魔数这个词在不同领域代表不同的含义。在计算机领域,魔数有两个含义,一指用来判断文件类型的魔数;二指程序代码中的魔数,也称魔法值。
0000 0034:紧跟着魔数后面的4位就是版本号了,同样也是4个字节,其中前2个字节表示副版本号,后2个字节,表示主版本号。
前面两个字节是0x0000,也就是其值为0;
后面两个字节是0x0034,也就是其值为52.
所以上面的代码就是52.0版本来编译的,也就是jdk1.8.0。
002f:常量池计数器,0x002f 其值就是 47
在线进制转换工具:
https://www.sojson.com/hexconvert/64to10.html
2.6 第三步:准备
这一步就是分配内存空间、赋予默认值,相对于我们的Math.java代码就是给:
initData分配内存空间,并且赋予默认值,此时initData=0。
2.7 第四步:解析
将符号引用替换为直接引用。这个部分具体比较复杂,我们在后续会进行展开讲解。
2.8 第五步:初始化
对类的静态变量初始化为指定的值,执行静态代码块等。那么我们的Math中的initData对象此时数据就是666。
相关历史文章(阅读本文之前,您可能需要先看下之前的系列?)
JVM内存模型和性能调优:为什么要学习JVM - 第1篇
什么是Java虚拟机【JVM:基础入门】 - 第2篇
一图了解JVM核心组成【JVM:基础入门】 - 第3篇
JVM内存模型和调优实战课程
http://t.cn/A6wWMVqG
点击「阅读原文」快速查看学习:
学习的天平上,你在左盘付出的越多,那么在右盘得到的也会随着你的付出而增多。致敬大师,致敬未来的你。