类在虚拟机中的生命周期:加载(Loading)、验证(Verification)、准备(preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading) 共7个阶段
加载、验证、准备、初始化、卸载 这5个阶段有严格的先后顺序关系。其他阶段不一定。(为了支持java语言的运行时绑定)
加载由3部分组成:
- 这里是列表文本通过classloader获取该类的二进制字节流。
- 将字节流所代表的静态存储结构转化为运行时数据结构。
- 在内存中生成一个代表这个的java.lang.Class对象,作为方法去这个类的各种数据的访问入口。
第1点是非常灵活的,比如可以从jar包获取,可以从网络获取,可以动态生成(动态代理、数组类),可以由其他文件生成(比如jsp)等等。
class文件运行与JVM中。.class可以是.java文件通过javac编译而成,也可以是groovy文件通过groovyc编译而成,也可以是JRuby文件通过JRbubyc编译而成,也可以是其他语言程序(比如scala)通过对应的编译器编译而成。所以JVM对class文件的验证就显得非常重要,以防止对正在运行的JVM造成伤害。
验证包括一下几部分
- 文件格式验证:是否已魔法书0xCAFEBABE开头,主次版本号是否是当前JVM可以接受的,常量池中的常量是否有不被支持的常量类型,常量池中的各种索引值是否有指向不存在的常量或不符合类型的常量。。。。。等等
- 元数据验证:是否有父类,父类是否不允许被继承,是否实现了父接口的接口或者父抽象类的抽象方法,类的字段、方法是否与父类产生矛盾(父final变量不可覆盖,父final方法不可重写)。。。。等等
- 字节码验证。主要是对方法体内的代码进行验证,比如参数类型是否一致,类型转换是否有效。。。等等
- 符号引用验证。这一验证阶段配合第三阶段--解析阶段完成。主要验证引用类是否存在,类、字段、方法的访问性 等等。。。
准备:为类变量分配内存并设置默认初始值(0,false,null等)。如果为final,则分配内存并设置值。
解析:虚拟机将常量池内的编译器使用的符号引用替换为运行期使用的直接引用的过程(配合验证阶段--的第4步)。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。符号引用是因为在编译的时候,不能明确所引用的类或属性或方法在运行时的具体地址,所以就以符号引用来代替。
直接引用:在运行期能直接确定目标的指针、相对偏移量或间接定位的句柄。HotSpot采用指针。
JVM规范并未规定解析阶段发生的具体时间。只要求在16个特定的字节码指定执行之前,先对他们所使用的符号引用进行解析。所以虚拟机实现可以根据需要判断到底是在类呗加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用之前才去解析它。一般来说现在的虚拟机都选择后者,这样可以边使用边解析,提高访问速度(暂时不解析还没用使用到的)。
初始化:执行<clint>()方法 字节码中的<clint>()方法由编译器对类变量的赋值动作和静态代码块按代码编写顺序合并而来。如果一个类没有类变量也没有静态代码块,则该类编译成的字节码中没有<clint>()方法。 由虚拟机来保障在多线程的情况下,只有一个线程去执行这个类的<clint>()方法,其他线程阻塞等待直到<clint>()完成。
类加载器:
- 启动类加载器:Bootstrap ClassLoader 负责加载<JAVA_HOME>/lib目录下的文件(如rt.jar)。
- 扩展类加载器:Extension ClassLoader 负责加载<JAVA_HOME>/lib/ext目录下的问题件。
- 应用程序类加载器:Application ClassLoader 负责加载ClassPath上的指定的类库。
类加载器比较灵活,开发者可以继承java.lang.ClassLoader来实现自己的类加载器(比如spring、fastjson等都有类加载器的实现)。
双亲委派: 当碰到一个需要被加载的Class的时候,该加载器会委托父加载器加载。当父加载器范围未加载并且不能加载该类的时候,才有该加载器加载类。这样做的目的是为了保障加载到的class文件存放到内存中的java.lang.Class对象是同一个。父加载器与子加载器不是通过继承实现的,而是组合。 注意:不同ClassLoader加载同一个class文件,会生成不同的java.lang.Class对象,导致java代码中 instanceof 返回false
class文件以何种格式存储,类型何时加载、如何连接,以及虚拟机如何执行字节码指令都是有虚拟机直接控制的行为,用户编写的程序代码无法直接控制和改变。用户能通过程序进行操作的,主要是字节码的生成与类加载器这两部分。
需要对类进行初始化的场景:
- new对象、设置或读取静态变量(同时被被final修饰除外,因为编译期已经把结果放入常量池)、调用类的静态方法
- 对类进行反射调用的时候
- 如果父类没有初始化,需要先初始化父类
- 程序入口main方法所在的类
注意,
- 通过子类来调用父类的静态变量或方法,不会初始化子类;
- 定义数组的时候不会初始化类;
- 调用 fianl static 变量 不会初始化类。编译器已经将被调用的final值放在了调用者的常量池中。
- 只有在实际使用到父接口(比如使用父接口定义的常量)才会初始化父接口。(只通过java代码不好证明,可以配合使用jvmconsole等工具查看已加载类)
- 内部类会执行懒加载,第一次使用到的时候才执行初始化。(单例设计模式的一种)
获取class对象的三种方式:
- TestA.class;
- Class.forName("com.a.b.c.TestA.class");
- testA.getClass();
这三种方式中,第一种如果是首次加载只会触发TestA对应的class的加载、验证、准备,不一定会有解析,但一定不会初始化(即,static代码块不会执行)。第二种方式如果是首次加载会在第一种的基础上多执行一次初始化。第三种方式该类的对象都已经存在了,说明该类已经进行过来初始化。
参考资料:
- 《深入理解Java虚拟机》