本篇中会对涉及到的知识点皆做出描述:
首先,我们先了解先虚拟机的类加载机制:
虚拟机把描述类的数据从Class 文件中加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 java 类型,这便是虚拟机的类加载机制。
也就是说,一个文本.java 文件要运行起来:它首先要经过编译成为 Class 文件(字节码文件),然后被虚拟机加载读入内存,接着虚拟机首先对其中的数据进行校验
一个类从被加载的虚拟机内存中开始到卸载出内存为止(java 类的卸载并不能人为主动卸载,只能通过 JVM 的垃圾回收来卸载。没有像 C 中析构函数那样的东西)经历的有:
加载
加载阶段虚拟机完成三件事情:
1.通过一个类的全限定名称来获取定义此类的二进制字节流
2.将这个字节流所代表的静态储存结构转化为方法区的运行时数据结构。
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证
验证阶段大致完成4个阶段的检验动作:
1.文件格式验证
该验证阶段时基于二进制字节流的,通过这个阶段验证后字节流才会进入内存的方法区中进行存储。而后面的3个验证阶段全部是基于方法区的存储结构进行的。
2.元数据验证:比如校验类的继承关系是否合法
3.字节码验证:例如保证跳转指令不会跳转到方法体之外的字节码指令上。
4.符号引用验证:例如符号引用中通过字符串描述的全限定名称是否能找到对应的类
准备
正式为类变量分配内存并设置类变量初始值,变量所使用的内存都将在方法区中进行分配。(这个时候进行内存分配的仅包括类变量既被 static 修饰的变量而不包括实例变量。实例变量会在对象实例话时随着对象一起分配在 java 堆中)
这时分配的初始值往往都时零值,实际代码中定义的值在初始化阶段才会赋予。(final修饰的除外,会在此阶段赋值)
解析
解析阶段是将常量池内的符号引用替换为直接引用的过程。
1.类或接口的解析
比如当前所在A类,在A类中引用了B类且B类从未被解析过。
(1)如果B不是一个数组类型,那虚拟机会把代表B的全限定名传递给A类的加载器去加载类B。
(2)如果B是一个数组类型,并且数组的元素为对象。那么虚拟机会按(1)中的方式去加载数组的元素类型。接着虚拟机会生成一个代表此数组维度和元素的数组对象。
(3)如果上面的步骤没有异常,那么B在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前还需进行符号引用验证,确认A是否具备对B的访问权限。如不具备则抛出java.lang.IllegalAccessError 非法访问异常。
2.字段解析
(1)如果A类本身包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(2)否则,如果在A中实现了接口,将会按照继承关系从下往上递归搜索各个接口的它的的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(3)否则,如果A不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类。如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回字段查找结束。
(4)否则,查找失败。
如果查找成功了还会对该字段进行权限验证,如发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常。
3.类方法解析
4.接口方法解析
初始化
进行初始化的条件:
1.当遇到 newgetstaticputstatic或invokestatic这4调字节码指令时,如没有进行过初始化则需要出发初始化。
既 当你用 new 实例化一个对象时 读取或者设置一个类的静态字段时 (这里有一种静态字段除外:用 final 修饰,既已在编译期把结果放入常量池的静态字段除外) 调用一个类的静态方法的时候 (同样,也是被 fianl 修饰的除外)
总结来说,第一点就是 当用 new 实例化一个对象时 以及 用到没有被 final 修饰的类的静态字段或方法时 就会触发初始化
2.使用 反射包的方法对类进行反射调用的时候
这个更好理解,反射调用类的方法是需要类的实例的,得到类的实例便要经过初始化过程。
3.当初始化一个类时,如果发现其父类还未初始化,则要先触发其父类的初始化(接口稍有区别,只有在真正使用到父类接口的时候才会去初始化)
4.当虚拟机启动时会先初始化主类(既包含main() 方法的那个类)
5.当使用JDK1.7的动态语言支持时。如果 java.lang.incoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
(了解java.lang.incoke.MethodHandle)
有且只有这五种情况才会触发初始化。这五种为主动引用。除此之外所有引用类的方式都不会触发初始化,这些称为被动引动。
一下为被动引用:
1.通过子类引用父类的静态字段。
2.通过数组定义来引用类,不会触发此类的初始化
3.常量在编译阶段存入调用类的常量池中,本质上并没有直接引用到定义常量的类,所以不会触发定义常量的类的初始化。
卸载
这七个阶段。其中,验证、准备、解析三个部分又被称为连接。
这几个阶段的顺序:
首先 加载、验证、准备、初始化和卸载这五个过程是确定的(使用不用多说)。
1.加载------>2.验证------>3.准备------->............?初始化.........6.使用------->.7.卸载
既解析和初始化这两个的顺序是不确定的。某些情况下解析可以在初始化之后再开始,以支持java 的运行时绑定,既动态绑定。(而动态绑定最为容易想到的例子就是多态)所以,可以说java 多态也是因为解析步骤可以在初始化之后。
而确定的这五个过程也并不是等一个完成才进行下一个的,这些阶段通常都是互相交叉混合的进行的,通常会在一个阶段执行的过程中调用、激活另一个阶段。