Java 类加载机制
类从被加载到JVM
中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。
类的加载过程
加载
1、通过类加载器,加载.class
文件到内存中。
2、将读取到.classs
数据存储到运行时内存区的方法区。
3、然后将其转换为一个与目标类型对应的java.lang.Class
对象实例。这个Class
对象在日后就会作为方法区中该类的各种数据的访问入口。
链接
验证
确保被加载的类(.class
文件的字节流),是否按照java
虚拟的规范。不会造成安全问题
1、文件格式验证:
第一阶段要验证字节流是否符合 Class
文件格式的规范, 井且能被当前版本的虚拟机处理。这一阶段可能包括下面这些验证点:
- 是否以魔数
0xCAFEBABE
开头 - 主、次版本号是否在当前虚拟机处理范围之内 。
- 常量池的常量中是否有不被支持的常量类型(检査常量
tag
标志)。 - 指向常量的各种索引值中是否有指向不存在的常量或不符合装型的常量 。
-
CONSTANT_Utf8_info
型的常量中是否有不符合UTF8
编码的数据 -
Class
文件中各个部分及文件本身是否有被删除的或附加的其他信息
实际上第一阶段的验证点还远不止这些, 这是其中的一部分。只有通过了这个阶段的验证之后, 字节流才会进入内存的方法区中进行存储, 所以后面的三个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。
2、元数据验证
第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java
语言规范的要求,这个阶段可能包括的验证点如下:
- 这个类是否有父类(除了
java.lang.object
之外,所有的类都应当有父类) - 这个类的父类是否继承了不允许被继承的类(被finaI修饰的类)
- 如果这个类不是抽象类, 是否实現了其父类或接口之中要求实现的所有方法
类中的字段、 方法是否与父类产生了矛盾(例如覆盖了父类的final
字段, 或者出現不符合规则的方法重载, 例如方法参数都一致, 但返回值类型却不同等)
第二阶段的验证点同样远不止这些,这一阶段的主要目的是对类的元数据信息进行语义检验, 保证不存在不符合Java
语言规范的元数据信息。
3、字节码验证
第三阶段是整个验证过程中最复杂的一个阶段, 主要目的是通过数据流和控制流的分析,确定语义是合法的。符号逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,例如:
- 保证任意时刻操作数栈的数据装型与指令代码序列都能配合工作, 例如不会出现类似这样的情况:在操作栈中放置了一个
int
类型的数据, 使用时却按long
类型来加载入本地变量表中。 - 保证跳转指令不会跳转到方法体以外的字节码指令上
- 保证方法体中的类型转换是有效的, 例如可以把一个子类对象赋值给父类数据装型,这是安全的,但是把父类对象意赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、 完全不相干的一个数据类型, 则是危险和不合法的。
即使一个方法体通过了字节码验证, 也不能说明其一定就是安全的。
4、符号引用验证
最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候 , 这个转化动作将在连接的第三个阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用) 的信息进行匹配性的校验, 通常需要校验以下内容:
符号引用中通过字将串描述的全限定名是否能找到对应的类
在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段 。
符号引用中的类、字段和方法的访问性(private
、 protected
、 public
、 default
)是否可被当前类访问
符号引用验证的目的是确保解析动作能正常执行, 如果无法通过符号引用验证, 将会抛出一个java.lang.IncompatibleClassChangError
异常的子类, 如 java.lang.IllegalAccessError
、java.lang.NoSuchFieldError
、java.lang.NoSuchMethodError
等。
准备
主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值,此时的赋值是Java
虚拟机根据不同变量类型的默认初始值:
如8
种基本类型的初值,默认为0
;引用类型的初值则为null
;常量的初值即为代码中设置的值
1、final static temp = 100
,此时temp
就是赋值 100
。
2、String temp = “123456”
,此时temp
值就是null
。
3、int temp = 100
,此时temp
值就是0
。
解析
将类的二进制数据中的符号引用替换成直接引用(符号引用是用一组符号描述所引用的目标;直接引用是指向目标的指针)
可以认为是一些静态绑定的会被解析,动态绑定则只会在运行时进行解析;静态绑定包括一些final
方法(不可以重写),static
方法(只会属于当前类),构造器(不会被重写)
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
初始化
初始化,则是为标记为常量值的字段赋值的过程。
换句话说,只对static
修饰的变量或语句块进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
涉及问题
一个类的构造器,代码块,静态代码块,成员变量的 的执行顺序。
//父类
public class ParentClass {
private int p1 = getValue() ;
private static int p2 = getValue2();
public ParentClass(){
System.out.println("我是父构造器");
}
static {
System.out.println("我是父静态代码块1");
}
static {
System.out.println("我是父静态代码块2");
}
{
System.out.println("我是父代码块1");
}
{
System.out.println("我是父代码块2");
}
private int getValue(){
System.out.println("我是父成员变量p1");
return 1;
}
private static int getValue2(){
System.out.println("我是父静态成员变量p2");
return 1;
}
}
//子类
public class ChildClass extends ParentClass{
private int c1 = getValue() ;
private static int c2 = getValue2();
public ChildClass(){
System.out.println("我是子构造器");
}
static {
System.out.println("我是子静态代码块1");
}
static {
System.out.println("我是子静态代码块2");
}
{
System.out.println("我是子代码块1");
}
{
System.out.println("我是子代码块2");
}
private int getValue(){
System.out.println("我是子成员变量c1");
return 1;
}
private static int getValue2(){
System.out.println("我是子静态成员变量c2");
return 1;
}
public static void main(String[] args) {
ChildClass childClass = new ChildClass();
}
}
执行结果:
我是父静态成员变量p2
我是父静态代码块1
我是父静态代码块2
我是子静态成员变量c2
我是子静态代码块1
我是子静态代码块2
我是父成员变量p1
我是父代码块1
我是父代码块2
我是父构造器
我是子成员变量c1
我是子代码块1
我是子代码块2
我是子构造器
网友评论