-
加载 loading
-
验证 verification
-
准备 preparation
-
解析 resolution
-
初始化 initialization
-
使用 using
-
卸载 unloading
2.主动引用(需要立即对类进行初始化)
-
遇到new、get static、put static (读取或获取类的静态属性)、invoke static(调用类的静态方法)这四条指令时候
-
反射
-
当需要初始化一个类时,如果其父类没有初始化,则需要先初始化其父类
-
有main()方法的类
3.被动引用
-
通过某个类的子类调用它的静态属性,不会导致子类初始化
-
常量会在编译阶段存入调用类的常量池,即调用某个类的常量不会触发该类的初始化
-
创建某个对象数组,并不会触发类的初始化
4.类加载过程
(包括加载、验证、准备、解析、初始化五个阶段,在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,这个顺序指的是按顺序开始,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始-->支持java语言的运行时绑定)
java绑定:把方法的调用与所在的类关联起来
1).静态绑定:前期绑定,程序调用前方法已经被绑定,java中前期绑定方法 构造方法、private + static + final关键字修饰方法
2).动态绑定: 晚期绑定,也叫运行时绑定。在运行时根据具体对象的类型进行绑定。java中绝大部分方法都是运行时绑定
动态绑定
为了理解多态的本质,下面讲一下Java调用方法的详细流程。
1) 编译器查看对象的声明类型和方法名。
假设调用 obj.func(param),obj 为 Cat 类的对象。需要注意的是,有可能存在多个名字为func但参数签名不一样的方法。例如,可能存在方法 func(int) 和 func(String)。编译器将会一一列举所有 Cat 类中名为func的方法和其父类 Animal 中访问属性为 public 且名为func的方法。
这样,编译器就获得了所有可能被调用的候选方法列表。
2) 接下来,编泽器将检查调用方法时提供的参数签名。
如果在所有名为func的方法中存在一个与提供的参数签名完全匹配的方法,那么就选择这个方法。这个过程被称为
重载解析(overloading resolution)
。例如,如果调用 func("hello"),编译器会选择 func(String),而不是 func(int)。由于自动类型转换的存在,例如 int 可以转换为 double,如果没有找到与调用方法参数签名相同的方法,就进行类型转换后再继续查找,如果最终没有匹配的类型或者有多个方法与之匹配,那么编译错误。
这样,编译器就获得了需要调用的方法名字和参数签名。
3) 如果方法的修饰符是private、static、final(static和final将在后续讲解),或者是构造方法,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式 称为
静态绑定(static binding)
。
与此对应的是,调用的方法依赖于对象的实际类型, 并在运行时实现动态绑。例如调用 func("hello"),编泽器将采用动态绑定的方式生成一条调用 func(String) 的指令。
4)当程序运行,并且釆用动态绑定调用方法时,JVM一定会调用与 obj 所引用对象的实际类型最合适的那个类的方法。我们已经假设 obj 的实际类型是 Cat,它是 Animal 的子类,如果 Cat 中定义了 func(String),就调用它,否则将在 Animal 类及其父类中寻找。
每次调用方法都要进行搜索,时间开销相当大,因此,JVM预先为每个类创建了一个
方法表(method lable)
,其中列出了所有方法的名称、参数签名和所属的类。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。在上面的例子中,JVM 搜索 Cat 类的方法表,以便寻找与调用 func("hello") 相匹配的方法。这个方法既有可能是 Cat.func(String),也有可能是 Animal.func(String)。注意,如果调用super.func("hello"),编译器将对父类的方法表迸行搜索。
1).在该阶段jvm做的事
1.通过一个类的全限定名来获取其定义的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
2).类加载器分类
(1).jvm角度
-
启动类加载器
-
其他类加载器 (需要由启动类加载器加载到内存中)
(2).java开发人员
-
启动类加载器 Bootstrap ClassLoader
它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。
-
扩展类加载器 Extension ClassLoader
它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
-
应用程序类加载器 Application ClassLoader
它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
-
自定义类加载器
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
1)在执行非置信代码之前,自动验证数字签名。
2)动态地创建符合用户特定需要的定制化构建类。
3)从特定的场所取得java class,例如数据库中和网络中。
3).类加载器的双亲委派模型
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
使用双亲委派模型来组织类加载器之间的关系,有一个很明显的好处,就是Java类随着它的类加载器(说白了,就是它所在的目录)一起具备了一种带有优先级的层次关系,这对于保证Java程序的稳定运作很重要。例如,类java.lang.Object类存放在JDK\jre\lib下的rt.jar之中,因此无论是哪个类加载器要加载此类,最终都会委派给启动类加载器进行加载,这边保证了Object类在程序中的各种类加载器中都是同一个类。
2.verification
验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证
-
文件格式的验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
-
元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
-
字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
-
符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。
3.preparation
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
1、这时候进行内存分配的仅包括静态变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
public class demo { private static demo demo1 = new demo(); private static int a = 0; private static int b; static { System.out.println("执行静态代码块"); } private demo() { a++; b++; } public static void main(String[]args) { System.out.println(a); System.out.println(b); } }
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。
5.initialization
类初始化是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有静态变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。