类加载
加载
java数据类型分为基本数据类型和引用数据类型,
基本数据类型由虚拟机预先定义,引用数据类型才需要类的加载过程。
类的加载,就是将java类的字节码文件加载到内存中,并通过字节码在内存中构建出类的原型---类模板对象。
jvm把字节码中的常量池,类字段,类方法等信息存储到类模板中,这样jvm在运行期间才能获得类的全部信息。
反射也基于这一基础。
加载阶段主要做的事情:
首先通过类的全类名(包名加类名)找到类的字节码文件,有下面几种方法读入字节码
- 可以通过文件系统读入后缀为.class的文件
- 可以通过读入jar,zip包,提取字节码
- 可以读取数据库中的字节码
- 可以使用http协议传输字节码
- 可以使用运行时生成的字节码
解析字节码文件,生成内存中的该类的数据结构,也就是类的模板,如果这个字节码不符合规范会抛出classformaterror错误
类的模板存放在方法区中,方法区在元空间(使用本地内存)中
在堆中创建java.lang.class类的实例,指向上一步生成的类的模板,外部就通过堆中的这个实例来访问类中的各种信息。
java.lang.class的构造方法是私有的,只有jvm可以创建。
PS:数组类的加载有些特殊,数组类本身并不是由类加载器负责的,而是jvm在运行时根据需要直接创建的,但数组的元素类型仍然需要类加载器创建,
链接
验证:保证加载的字节码是规范的
格式检查:格式检查和字节码的加载一起执行,格式检查通过后,类加载器才会把类的字节码加载到方法区中。
(格式检查之外的检查会在类的字节码加载到方法区之后执行)
(格式检查举例包括,魔数检查,版本检查等)
语义检查
字节码验证
符号引用验证
准备:准备阶段就是为类的静态变量分配内存,并将其设置为默认值,并不是代码里的默认值,而是jvm给这些变量的默认值。
如果静态变量是static final修饰的基本数据类型,直接赋值常量,在准备阶段显式赋值,也就是赋予代码中的指定值。
而如果是static final修饰的String,使用字面量赋值,在准备阶段显式赋值,也就是赋予代码中的指定值。
注意这里只是静态变量,类的静态变量和类模板一起放在方法区,而类的实例变量则分配在堆中。
解析:将类,接口,字段,方法的符号引用转换为直接引用
符号引用(Symbolic Reference): 符号引用是一种用符号来表示引用目标的引用形式。在符号引用阶段,引用的目标并没有直接指定目标的内存地址,而是以符号的形式表示。符号引用可以是类名、字段名、方法名等,它们是一种抽象的引用。
直接引用(Direct Reference): 直接引用是指可以直接定位目标的引用形式。与符号引用不同,直接引用包含了目标的直接内存地址或偏移量,可以直接定位到目标。
方法区中本来存储的都是一些符号,比如我使用了某个类的某个方法,但是只存了类和方法的名字,仅有这些信息是没有办法执行的,还要把这些符号转成真正的地址。
PS:当java代码中直接使用字符串常量时,就会在常量池中生成constant_string,他表示字符串常量,并会引用constant_utf8的常量项,在常量池中会维护字符串常量池,保存所有出现过的字符串常量并且没有重复项。
初始化
初始化阶段,为类的静态变量赋值和执行静态代码块的过程,
类的初始化阶段才会执行java字节码,也就是java程序中的代码
类的初始化阶段最重要的工作是执行类的初始化方法()
该方法由编译器生成,jvm执行,程序员是无法调用的,也不能定义一个同名的方法。
这个方法的主要内容就是类的静态变量的赋值语句和静态代码块
另外,在尝试初始化一个类的时候,jvm总是会试图先加载该类的父类,
因此父类的()函数总是在子类的()函数之前调用,也就是说父类的static静态代码块优先于子类的静态代码块。
下面有一些特殊情况,有些类的字节码文件中不会产生()函数,
如果类中没有静态变量和静态代码块,自然就不会有()函数
如果类中有静态变量但是没有静态变量的赋值语句,也没有静态代码块,自然就不会有()函数
注意:以上可以看到有静态变量的赋值就会有clinit函数,但是有一些特殊情况,即使有静态变量的赋值也不会产生clinit函数,这是因为这些显式赋值在链接的准备阶段就做了。
如果静态变量是static final修饰的基本数据类型,并且直接赋值常量,那么在准备阶段就已经被显式赋值了,不会生成()函数
而如果是static final修饰的String,使用字面量赋值,那么在准备阶段就已经被显式赋值了,不会生成()函数。
()方法需要线程安全,如果有多个线程想去初始化同一个类也就是执行同一个()方法,那么应该只会有一个线程被允许去执行这个方法,其他线程都要被阻塞。
如果一个线程已经加载了类,那么其他线程都不应该重复加载这个类,当需要再次使用这个类时,虚拟机会直接返回已经准备好的信息。
类的主动使用
只有类的主动使用会调用方法,
主动使用包括,
创建一个类的实例的时候,包括使用new关键字,使用反射,克隆,序列化
调用类的静态方法的时候,
使用类或者接口的静态字段(注意有一些加final的常量在准备阶段就已经产生了,所以不需要调方法),
使用java.lang.reflect包中的方法反射类的方法的时候,比如使用class.forname("")
当初始化子类发现父类还没有进行过初始化,需要先触发其父类的初始化
这条规则对接口并不适用,初始化一个类的时候,并不会初始化它所实现的接口
初始化一个接口的时候,并不会初始化父接口
如果一个接口定义了default方法,那么当初始化直接或间接实现该接口的类时,会先触发接口的初始化。
当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类
当初次调用methodhandle实例时,初始化该Methodhandle指向的方法所在的类。
为什么需要自定义类加载器
首先介绍自定义类的应用场景:
(1)加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。
(2)从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。
(3)以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。