JVM(四)类加载机制
1.静态绑定和动态绑定
静态绑定:即前期绑定,在程序执行前方法已经被绑定,此时由编译器或者其他连接程序实现,针对Java,可以理解为编译期的绑定,java中只有final、static、private和构造方法是前期绑定的。
动态绑定:即晚期绑定,也叫运行时绑定,在运行时根据具体的类型进行绑定,在java中几乎所有的方法都是后期绑定。
2.加载阶段:
①要做的三件事: a.通过一个类的全限定名来获取定义的二进制流文件(class文件、jar包、网络中、其他应用jsp)
b.将这个字节流的静态的存储结构转化为方法区的运行时
c.在java堆中生成一个代表这个类的java.lang.class对象,作为对方法区这些数据的访问入口。
②相对于类加载其他阶段而言,是可控性最强的, 可以使用系统提供的也可以定义自己的类加载器加载。
③加载阶段完成后,虚拟机外部的二进制字节流就会按照虚拟机需要的格式存储在方法区中,而且在java堆中也创建了一java.lang.class对象来访问方法区的数据。
3.类加载器:
① 对于任何一个类来说,都需要由它的类加载和这个类本身去确定其在虚拟机的唯一性,也就是说两个类来源于同一个Class文件,只要加载他们的类加载器不同,那这两个类就必定不相等。 这里的相等时包括。equals()、isAssingableFrom()、isInstance()等方法的返回结果,也包括instanceof的判定结果
② 站在虚拟机的角度,只存在两种类加载器
启动类加载器 ---- 虚拟机自身的一部分
所有其他的类加载器 ---- 独立于虚拟机之外,在启动类加载器加载到内存之后去加载的。
③ 站在开发人员的角度,大致可发三类
启动类加载器 JDK\jre\lib
扩展类加载器 负责加载JDK\jre\lib\ext中或者由java.ext.dirs系统变量指定的路径的所有类库(javax开头的)
应用程序类加载器 加载用户类路径下面的类,如果程序中没有定义过自己的类加载器,这个就是默认使用的。
④自己定义类加载 的好处
JVM自带的ClassLoader只懂得从本地文件系统加载标准的Class文件,
a.在执行非置信代码之前,自动验证数字签名
b.动态的创建符合用户特定需要的定制化构建类
c.从特定的场所取得class ,例如数据库和网络中。
⑤双亲委派模型是
工作流程是
如果一个类加载器收到了类加载的请求,它首先不会自己加载这个类,而是把请求委托给父类去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的类加载器中, 只有当前父加载器在它的搜索范围内没有找到所需的类时,即无法完成该加载,子类加载器才回去尝试。
优点
java类随着它的类加载一起具备了具有一种带有优先级的层次关系。
java.lang.Object在JDK\jre\lib的rt.jar中,因此无论哪一个类加载器要加载类,都保证会由启动类加载器加载,也保证了Object类在程序中各种类加载器都是同一个类
4.验证:
为了确保Class文件中的二进制流信息格式符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。主要有四个验证:文件格式验证,元数据格式验证,字节码验证,符号引用验证。
文件格式验证:验证字节流是否符合Class文件的规范,并且能被当前版本的虚拟机处理,该验证的目的是为了保证输入的字节流能够正确被解析存储到方法区里面。后面的 三个验证都是基于方法区结构的验证。
元数据验证:对类的元数据信息进行语义验证,(对类中各数据类型进行语法校验)
字节码验证:该阶段验证的主要工作是验证数据流和控制流,对类的方法体进行校验分析,保证类的方法在执行的时候不会对JVM做出危害行为。
符号引用验证:发生在将符号引用转化为直接引用的时候,对类自身以外的信息(常量池的各种符号引用)进行匹配性的验证。
5.准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配,有几点需要注意:
①这时候进行内存分配的仅包括类变量,实例变量在对象实例化时在堆中分配。
②这里设置是初始值,不是在类中被显示赋予的值。
a.基本数据类型:对于类变量和全局变量,如果不显式的赋值使用,系统会赋予默认值,对于局部变量必须赋值,否则编译不通过。
b.同时被static和final修饰的值, 必须在声明的时候显式的赋值,否则编译不通过,
c.被final修饰,成员变量:在类中声明或者在构造函数初始化的时候赋值,一旦赋值,不能更改
局部变量:在使用前赋值
(在类中声明或者在初始化的时候赋值,在使用前必须进行赋值,系统不会赋予默认的值)。
d.对于引用类型,数组引用和对象引用,没赋值,则系统赋null
e.数组元素没有被赋值,根据数组元素类型系统赋默认的值。
③如果是static和final修饰的,在准备阶段就会初始化指定的值。
在编译期放到了调用它的类常量池里面
6.解析
符号引用到直接引用的过程,可能开始于初始化之前和之后。JVM自己决定。主要针对:类或接口的解析、字段解析、类方法解析、接口方法解析。
①类或接口的解析:判断所要转化的是数组类型的,还是普通对象类型的,从而进行不同的解析。
②字段解析:会首先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,查找结束,如果没有,则会按照继承关系,从上往下递归搜索该类所实现的各个接口和他们的父接口。 还没有的话,搜从上往下递归搜索其父类,直至查找结束。
③类方法解析:和字段解析相差不多,只是多了判断该方法所处是类还是接口的步骤,先搜索父类,在搜索接口。
④接口方法解析:递归查找父接口。
7.初始化
初始化阶段是执行类构造器<clinit>方法的过程,按照程序员的主观计划去初始化类变量和其他资源。
①编译器自动收集类中的所有类变量的赋值动作和静态语句块合并产生的,编译器收集的顺序是源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,之后的变量,可以再静态代码块赋值,但是不能访问。
②<clinit>方法与实例构造器<init>方法不同,它不需要显式的调用父类构造器,虚拟机会保证子类的<clinit>的执行前,父类的<clinit>已经执行完成, 因此在虚拟机中第一个被执行的肯定是Object的<clinit>
③<clinit>对于类或接口来说不是必须的,如果一个类中没有静态语句块,也没有对类变量进行赋值操作,那么编译器可以不为这个类生成<clinit>方法
④接口中不能使用静态语句块,但仍然有类变量初始化的赋值操作,因此会生成<clinit>方法,但是与类不一样的是:不需要先执行父类的<clinit>,只有在使用父类定义的类变量时,才会触发, 另外,在接口的实现类在初始化时,不会执行接口的<clinit>方法
⑤虚拟机会保证一个类的<clinit>方法在多线程中是同步的和加锁的,如果多个线程同时去初始化一个类,那么只有一线程去执行,其他线程都需要阻塞等待。(屏障点线程)
总结
整个类加载过程,只有在类加载阶段,用户程序可以自动以类加载器参与,其余所有动作都由虚拟机主导完后,到了初始化才开始执行,也只限于<clinit>方法。主要是将class文件加载到虚拟机中,真正执行的操作,是在加载完成后才进行的。