一 概念:
1 类加载机制
虚拟机吧描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。就是虚拟机的类加载机制。
2 java类加载特点
- java语言是运行期类加载的,在java的语言里面,类的加载,连接和初始化过程都是在程序的运行期完成的。
2.1 运行时类加载的优点缺点
优点:为java应用程序提供了高度的灵活性。
缺点:类加载时稍微增加一些性能开销
ps:java是可以动态扩展的语言就是依靠运行期动态加载,动态链接这两个特点
二 类加载时机
1 类从被加载到虚拟机内存开始,到卸载出内存为止生命周期图如下:
image.png
- 加载-验证-准备-初始化-卸载这些步骤的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始。
(按部就班的开始而不是按部就班的进行或者完成的理解:这些阶段通常是相互交叉地混合式进行)
- 解析阶段则不一定,他在某种情况下可以初始化阶段之后在开始(为了支持java语言的运行时绑定(动态绑定或者晚期绑定))
2 什么情况下要开始类的第一阶段-加载
java虚拟机规范并没有强制约束这点可以交给虚拟机的具体实现自由把握
3 初始化
虚拟机规定了有且只有五种情况必须立即对类进行初始化
3.1 初始化触发的五种状况:
image.pngps:这五种场景中的行为成为对一个类进行主动引用,除此之外所有引用类的方式都不会触发初始化,被称为你被动引用
3.2 主动引用被动引用栗子(参考课本-深入理解jvm第七章类的加载机制)
三 类的加载过程
1加载
加载是类加载的第一个阶段虚拟机需要完成三件事情:
image.png
心得:jvm通过类全限定名(也就是包含包名的类)吧类(.class)文件,加载到方法区,同时也会在内存中生成一个Class 对象来记录这个类的信息也就是通过这个对象你就可以访问这个类的信息。
对类的全限定名来获取此类的二进制字节流的理解:
由于虚拟机规范没有明确的指明这个二进制字节流具体从哪获取,怎样获取,所以体现出了虚拟机的具体实现与具体应用的灵活度都是相当大的。也体现出了虚拟机设计团队的追求开放性,广阔平台性。
一些建立在这些基础上的java技术:
image.png
1.1 非数组类的加载:
既可以使用系统提供的引导类加载完成。也可以用户自定义类加载器完成,开发人员可以通过自定义类加载器去控制字节流的获取方式
收获: 控制字节流的获取方式,可以通过自定义类加载器控制。
1.2 数组类的加载
数组类本身不通过类加载器创建,它是由jvm直接创建。但是数组类与类加载器还是有密切的关系的。
数组类的元素类型最终要靠类加载器去创建,一个数组类的创建要遵循特定的规则。
PS:
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需要的格式,存储在方法区之中,然后再内存中实例化一个java.lang.Class 类的对象,这个对象将作为程序访问方法区中这些数据类型的外接口。
特别注意:Class对象比较特殊,他虽然是对象,但是放在方法区中。
2 验证
2.1验证的作用:
确保Class文件中的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
2.2 验证的四个阶段(如下图)
- 文件格式验证:字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
- 元数据验证:对字节码描述的信息进行语义分析,保证其描述的信息符合java语言规范的要求
- 字节码验证:通过数据流和控制流分析,确定程序的语义是否是合法的,符合逻辑的
-
符号引用验证:对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验, 这一阶段的校验发生在虚拟机将符号引用转换为直接引用的时候,这个动作发生在连接的第三阶段(解析阶段)中发生。
image.png
3 准备
准备阶段:
正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配
注意是类变量的分配(类的静态成员)
- 这个时候进行内存分配的只包括类变量(被static修饰的变量),并不包括实例变量,实例变量是在对象实例化时随对象一起分配在java堆中。
- 分配的初始值为零值,假设一个变量定义为:public static int value = 123;则设置变量的初始值应该为0, 而不是123. 把value赋值为123的putstatic指令是在程序被编译后,存放于类构造器<clinit>()方法之中,所以吧value赋值为123的动作将在初始化阶段才会执行。
- 如果字段属性表中存在ConstantValue属性,那么在准备阶段会将value赋值为ConstantValue属性所指定的值
例如:public static final int value = 123; 那么在准备阶段,则会将value赋值为123;()
心得:准备阶段static 变量会赋默认值,不会赋指定值(我们指定的数值)而static final类型会赋指定的值。
4 解析:将符号引用转换成直接引用的过程
-
符号应用:符号引用是一组以符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用是能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的Class文件格式中。符号引用在Class文件中以CONSTANT_Class_info(类或接口的符号引用)、CONSTANT_Fieldref_info(字段的符号引用)、
CONSTANT_Methodref_info(方法的符号引用)等类型的常量出现。 -
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接应用,那引用的目标必定已经在内存中存在
解析内容:
image.png
4 初始化:初始化阶段才真正开始执行类中定义的java程序代码(字节码)
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。
类的构造器:<clinit>()方法
实例构造器<init>() 方法
<clinit>()方法:
4.1<clinit>方法是有编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器手机的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在他之后的变量,在前面的静态语句块可以赋值,但不能访问:
书本例子:
public class Test
{
static
{
i = 0;//给变量赋值可以正常通过
System.out.println(i);//这句话会编译提示”非法向前访问“
}
}
4.2 <clinit>方法与类的构造器(或者说实例构造器<init>()方法)不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕,因此在虚拟机中的一个被执行的<clinit>()方法的类肯定是java.lang.Object。
4.3 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量的操作
4.4 <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法
4.5 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法,但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
4.6 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,知道活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能会造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。
ps:以上有参考课本以及这位大大的文章:https://blog.csdn.net/u010805617/article/details/77802739
四 类加载器
待续。。。
网友评论