类加载的时机
其中解析可能在初始化之后再进行。这是为了支持java语言的运行时绑定特性(也叫动态绑定或晚期绑定)。有且只有下列六种情况下,必须立刻对对象进行初始化(当然,加载 验证 准备已经完成了)
一:遇到new getstatic putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型java代码有
1.使用new关键字实例化对象的时候
2.读取或设置一个类型的静态变量(被*final修饰的静态变量在编译阶段就把结果存入常量池了,因此final变量除外)
3.调用一个类型的静态方法的时候。
二:对类型进行反射调用,如果类型没有初始化,则需要立即进行初始化
三:当初始化类时,如果他的父类还没初始化,则需要先触发父类的初始化
四:当虚拟机启动时,需要指定一个要执行的主类(main方法),虚拟机会先初始化这个类
五:当使用JDK7新加入的动态语言支持时,如果一个MethodHandle实例最后的解析结果为REF_getStatic REF-putStatic REF_invokeStatic REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先进行初始化(逐渐不知道在说什么QAQ)
六:当一个接口中定义了JDK8新加入的默认方法(default关键字修饰),如果有这个接口的实现类发生初始化时,那该接口要在其之前进行初始化。
上面六种方式称为主动引用,除此之外的所有引用类型的方式都不会触发初始化,被称为被动应用。
被动引用的例子
public class Test1 {
static {
System.out.println("父类初始化");
}
public static final int finalStaticVal = 123;
public static int staticVal = 123;
public static void fun(){
System.out.println("父类静态方法");
}
}
public class Test2 extends Test1{
static {
System.out.println("子类初始化");
}
}
测试1
public class Test3 {
public static void main(String[] args) {
System.out.println(Test2.staticVal);
}
}
通过子类调用父类的静态变量,并没有导致子类初始化
测试2
public class Test3 {
public static void main(String[] args) {
System.out.println(Test2.finalStaticVal);
}
}
调用父类的final static变量,父类和子类都没有进行初始化
测试3
public class Test3 {
public static void main(String[] args) {
Test2.fun();
}
}
调用父类的静态方法,子类并没有初始化
类加载过程
加载
虚拟机在加载阶段需要完成下列三件事
1.通过一个类的全限定名来获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化成方法去的运行时数据结构
3.在内存中生成一个代表这个类的Class对象,最为方法去这个类的各种数据的访问入口
虚拟机并没有指定必须从.class文件中获取类的二进制字节流,确切的说是根本没有指明要从哪里获取,如何获取。例如
可以从ZIP压缩包中获取,这很常见,最终成为日后jar EAR WAR格式的基础
从网络中获取
运行时计算生成,这种场景时用的最多的就是动态代理技术
由其他文件转换成(JSP-》class文件)
从数据库中读取
等等。。
加载阶段既可以使用虚拟机内置的类加载器完成,也可以由用户自定义的类加载器去完成。
加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区中了。
验证
验证时连接阶段的第一步,这一阶段的目的是确保Class文件的额字节流中包含的信息符合《Java虚拟机规范》的全部约束要求。保证这些信息被当作代码运行后不会危害虚拟机自身的安全
java语言本身是相对安全的编译语言(java代码无法跨界访问数组、不能把一个对象随意赋值等)。但是这些操作都可以通过字节码层面上实现。而Class文件又可以通过多种方式生成(如,手打出来),如果虚拟机不检查输入的字节流,对其完全信任的话,很可能会因为载入了错误或者有恶意企图的字节码流而导致系统说到攻击。验证阶段大致上分为下列4个步骤
1.文件格式验证
该验证阶段主要目的是保证输入的字节流能正确地解析并存储到方法区,格式上符合描述一个java类型信息的要求。
如:是否以魔数0XCAFEBABE开头
主、次版本号是否在当前java虚拟机接受范围内
常量池的常量中是否又不被支持的常量类型
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
等等。
只有通过文件格式验证的字节流才被允许进入java虚拟机内存的方法区中进行存储,所以后面的三个阶段全部是基于方法区的存储结构上进行的,不会再读取、操作字节流了
2.元数据验证
第二阶段是对字节码描述的信息进行语义分析
如这个类是否有父类
父类是否可以被继承
非抽象类是否实现了接口和父类中的抽象方法
等等。
这一阶段主要目的是对类的元数据信息进行语义校验,保证不存在与《Java语言规范》定义相悖的元数据信息
3.字节码验证
第三阶段是验证过程中最复杂的阶段,主要的目的是通过数据流分析和控制流分析。确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体,进行校验分析,保证不存在危害虚拟机的行为。
如:
把凭证任何跳转指令都不会跳转到方法体以外的字节码指令上
保证方法体中的类型转换是有效的
4.符号引用验证
最后一个阶段的校验行为发生在虚拟机将符号引用转换成直接引用的时候,这个转化动作将在连接的第三个阶段——解析阶段完成。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配行校验,通俗来说就是,该类是否缺少或者被禁止访问他一来的某些外部类,方法 字段等资源。
如:
符号引用中通过字符串描述的全限定名是否能找到对应的类
在指定类中是否存在符合方法字段描述符及简单名称所描述的方法和字段
符号引用中的类 字段 方法的客房卫星是否可被当前类访问
符号引用验证主要是为了保证解析阶段能够正常执行。
验证阶段虽然重要但是不是不可缺少的。
准备
准备阶段是为那些静态变量赋初值。如果是非final静态变量则会赋值成相应类型的0值,如果是final类型,则会直接赋值成代码中,输入的值。
解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可
直接引用:直接引用是可以直接指向目标的指针、相对偏移量、或者是一个能间接定位到目标地句柄。如果有了直接引用,奶奶用的目标必定已经在虚拟机的内存中存在了。
初始化
初始化阶段是类加载过程的最后一个步骤,进行准备阶段时静态变量已经赋过一次0值,而在初始化阶段则会根据程序员通过程序编码指定的主观计划去初始化类变量和其他资源。也可以说初始化阶段就是执行()方法地过程。这个方法是在编译时自动生成的。他会收集类中所有静态变量地复制动作和静态语句块。
类加载器
类加载器虽然只是用来实现类得加载动作,但他在java程序中起到地作用却远远超过加载阶段。对于任意一个类都必须由加载他的类加载器和这个类本身一起共同确立其它java虚拟机中的唯一性。如只有相同类加载器加载的同一个类才是相等的。即使两个类来源于同一个clss文件,如果他们的类加载器不同,那么他俩也不想等。
双亲委派机制
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给他的父类加载器去完成,每一个层次的类加载器都是如此。因此所有的类加载请求最终都应该传送到最顶层的启动类加载器,只有当父类加载器反馈自己无法完成这个加载请求(他的搜索范围中没有找到所需的类)时,自家在其才会尝试自己去完成加载。
优点:java中的类随着他的类加载器一起具备了一种带有优先级的层次关系。如java.lang.Object存在于rt.jar之中,无论哪一个类加载器要加载这各类,最终都是委派给处于模型最顶端的启动类加载器加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。如果没有使用双亲委派机制的化,都由各个类加载器自行去加载的话。如果用户编写了一个名为java.lang.Object的类,并放在ClassPath目录下,那系统中就会出现多个不用的Object类。