概念
Class文件中描述的各类信息,最终都需要加载到虚拟机中之后才能被运行和使用。而虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟机后会发生什么变化?将会给大家详细讲解。
首先类加载机制作用:
将我们Class文件加载到内存,并对数据进行校验,解析,初始化等操作。最后形成可供jvm所使用的Java类型,称为类加载机制。 在java语言中类的加载,验证,准备,解析,初始化都是在运行时期加载完成的,这种情况可能会有一些性能开销,但确为java应用提高了极高的可扩展性和灵活性 (java极高的扩展性就是依赖运行时加载和动态连接这个特点)。
类加载器的生命周期
注意:共有7个阶段 加载-》验证-》准备-》解析-》初始化-》使用-》卸载 其中 验证-》准备-》解析-》统称为 连接。
注意:在 加载-》验证-》准备-》初始化-》卸载 这五个顺序是确定的,类加载按这个顺序去加载,而解析阶段不一定,在某些情况下可能先初始化在解析,为了支持java运行时绑定。这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。
在什么情况下需要对类进行加载?
《JAVA虚拟机规范》中并没有明确定义,强制约定而是交由虚拟机自我判断。但确明确规定有6中情况必须立即对类进行初始化操作。
-
使用 New 关键字实例化对象的时候。
-
读取或设置一个类的静态字段的时候。
-
调用一个类的静态方法的时候。
-
通过java.lang.reflect包中的方法对类进行反射调用的时候。
-
当初始化一个类时,发现其父类还没有进行初始化,则需要先触发其父类初始化。
-
当虚拟机启动时,用户需要指定一个要执行的包含 main 方法的主类,虚拟机会初始化这个主类。
类加载第一阶段-加载
加载阶段虚拟机需要做那三件事情?
-
通过全限定名来获取这个类二进制字节流,也就是加载字节码文件
-
将这个字节码文件对应的静态存储结构化为方法区的运行时数据结构
-
在内存生成一个代表当前加载类的java.lang.Class对象,作为方法区这个类访问数据的入口
注意
通过全限定名来获取二进制流文件来说,并没有规定我们必须从某个Class文件获取,准确来说没有规定我们如何获取,所以获取二进制流方式并不是唯一性可以通过压缩文件中获取,网络获取,其他文件生成,数据库读取......。
小提示
在类加载阶段是开发人员可控性最强的一个阶段,加载我们二进制字节流文件可以使用java虚拟机所提供的引导类加载器进行加载,也可以通过自定义类加载器去实现,开发人与通过自己的类加载器去控制字节流的获取。
类加载第二阶段-验证
验证字节码文件是否符合《Java虚拟机规范》约束和要求,保证这些代码在运行过程中不会损坏虚拟机自身安全。 验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击,从代码量和耗费的执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载过程中占了相当大的比重。
验证分为如下四个阶段:
文件验证 :是否符合Class文件规范,例如魔术,主次版本号是否在虚拟机接受范围内,常量池是否有不被支持的常量类型,常量中是否有不符合utf-8编码数据等等。
元数据验证:对自己吗描述的信息进行语义分析,以保证符合《java虚拟机规范》要求,是否有父类除Object,这个类是否继承了不允许继承的类(final修饰),类中的字段方法是否与父类产生了矛盾等等。
字节码验证:通过数据流和控制流分析,确定程序是合法的,符合逻辑的。 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,保证任何跳转指令都不会跳转到方法体以外的字节码指令上,保证方法体中的类型转换总是有效的等等。
符号验证:校验行为发生在虚拟机在将符号引用转化为直接引用。 符号引用验证的主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机将会抛出一个java.lang.IncompatibleClassChangeError(不兼容的类更改错误)的子类异常,典型的如:java.lang.IllegalAccessError(非法访问错误)、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
类加载第三阶段-准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
类加载第三阶段-解析
将常量池内的符号引用替换为直接引用的过程。
符号引用 :符号引用以一组符号来描述所引用的目标,符号可以是任何 形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用 :直接引用是可以直接指向目标的指针、相对偏移量或者是一个能 间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在虚拟机 的内存中存在。
小提示
在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,多以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
类加载第三阶段-初始化
直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。初始化阶段就是执行类构造器()方法的过程。
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。
clinit()方法
clinit()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物。
-
clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,否则会出现非法前向引用变量。
-
clinit()方法与类的构造函数(即在虚拟机视角中的实例构造器init()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的clinit()方法执行前,父类的clinit()方法已经执行完毕因此在Java虚拟机中第一个被执行的clinit()方法的类型肯定是java.lang.Object由于父类的clinit()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
-
clinit()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成clinit()方法
-
Java虚拟机必须保证一个类的clinit()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程执行完毕clinit()方法。如果在一个类的clinit()方法中有耗时很长的操作,那就可能造成多个进程阻塞。