1.类加载初始化过程
当程序主动使用某个类时,如果该类还没加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类初始化,完成三步骤统称类加载或类初始化。
(如下图)
加载:是将任何类的.class文件二进制数据读到内存中,将其放在运行时数据区的方法内,然后在堆空间创建一个java.lang.Class对象,用来封装该类在方法区内的数据结构。
类加载来源:①本地文件系统来加载class文件
②JAR包中加载class文件
③通过网络加载class文件
④把一个java源文件动态编译,并执行加载
⑤从专有数据库中提取.class文件
备注:①类加载通常无须等到首次使用该类时才加载,JVM规范允许系统预先加载某些类。
② 类的类加载最终产品是位于堆区中的Class对象。
③Class对象封装了类在方法区内的数据结构,并向java程序员提供了访问方法区内的数据结构的接口(就是反射的接口)
④JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载中遇到.class文件确实或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误),如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
验证:检验被加载的类是否有正确的内部结构,比如文件格式验证,元数据验证,字节码验证,符号引用验证。
准备:类的静态属性分配内存并设置默认初始值,如(static int a=1;这里负责a=0,a=1实在初始化阶段赋值的)。
解析:将类的二进制数据中的符号引用替换成直接引用。(类或接口的解析,字段解析,类方法解析,接口方法解析)
符号引用:一组符号来描述目标,可以是任何字面量.(Person类的run() ;run()全名和相关描述信息符组成)
直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。(相当于C语言的指针)
初始化:初始化阶段主要就是对静态属性或静态初始化块指定初始值。
JVM初始化3步骤 :①假如该类还没有被加载和连接,程序先加载并连接该类
②假如该类的直接父类还没初始化,则先初始化其直接父类
③假如该类中有初始化语句,则系统依次执行这些初始化语句。
备注: 当执行第二步骤时,系统对直接父类的初始化步骤也遵循此①②③步骤,如果该直接父类又有直接父类,系统再次重复这三个步骤来初始化这个父类....以此类推,所以JVM最先舒适化的总是java.lang.Object类, 也就是说,当系统使用某个类时,那么该类的 直接父类或者间接父类都会被初始化。
2.类初始化的时机
java程序对类的使用方式可分为两种:
①主动使用
②被动使用
备注:所有的java虚拟机实现必须在每个类或接口被java程序"首次主动使用"时才初始化该类。
系统初始化该类或接口6种方式(主动使用)
①创建类的实例 (new操作符创建实例、反序列化的方式来创建实例) |
②调用某个类的静态属性 |
③访问某个类或接口的静态属性,或为该静态属性赋值 |
④使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。 如Class.forName("Person") |
⑤初始化某个类的子类,那么该类的所有父类都会初始化 |
⑥直接使用java.exe命令来运行某个主类 (java Test) |
除了以上六种情况,其他使用Java类的方式被看作是对类的被动使用,都不会导致类的初始化。
还有两种特殊情况:
①使用final型修饰静态属性,那么在编译时就得到属性值,则该属性可当成编译时常量,此时系统对该类的被动使用,不会导致该类的初始化
class MyTest{ static{ System.out.println("MyTest类的静态初始化块"); } static final String str = "Hello,World"; } public class FinalDemo { public static void main(String[] args) { System.out.println(MyTest.str); } }
运行结果:
分析结果:
只有输出 Hello,World 没有(MyTest类的静态初始化块) 可见没有初始化MyTest类
反之,如果该类final类型的静态属性的值不能在编译时得到,必须等到运行时才可以确定该属性的值,通过该类访问该静态属性,则可以认为主动访问使用该类,将会导致该类初始化。
class MyTest{ static{ System.out.println("MyTest类的静态初始化块"); } //只有运行后才可也确定str的值 static final String str = System.currentTimeMillis()+""; } public class FinalDemo { public static void main(String[] args) { System.out.println(MyTest.str); } }
运行结果:
分析结果:
调用某个类的静态属性,则该类会初始化
②loadClass() --只加载 forName()--加载并初始化
ClassLoader --- loadClass()方法 | 只加载该类不会初始化该类 |
Class --- forName()方法 | 强制初始化该类 |
package com.reflect; class Tester{ static{ System.out.println("Tester 类的静态初始化块...."); } } public class ClassLoaderTest { public static void main(String[] args) throws ClassNotFoundException{ ClassLoader classLoader = ClassLoader.getSystemClassLoader(); classLoader.loadClass("com.reflect.Tester");//该方法仅仅是加载Tester类 System.out.println("系统加载Tester类"); Class.forName("com.reflect.Tester");//此方法才会初始化Tester类 } }
运行结果:
分析结果:
当程序执行到调用loadClass()方法时,并没有打印Tester类的静态初始化,而是执行到forName()方法才会初始化Tester类。
3.接口初始化时机
当java虚拟机初始化一个类时,要求它的所有父类都已经初始化,但是这条规则不适用于接口,
在初始化一个类时,并不会先初始化它所实现的接口,
在初始化一个接口时,并不会初始化它的父接口,
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。
只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
4.初始化时机分析案例
案例一:
1 class Parent2{ 2 static int a = 3; 3 static{ 4 System.out.println("Parent static block"); 5 } 6 } 7 class Child2 extends Parent2{ 8 static int b = 5; 9 static{ 10 System.out.println("Child static block"); 11 } 12 } 13 public class MyTest2 { 14 static { 15 System.out.println("MyTest2 static block"); 16 } 17 public static void main(String[] args) { 18 System.out.println(Child2.b); 19 } 20 }
运行结果:
分析结果:Child2.b 主动调用Child2类的静态属性,那么该类的父类也就完成初始化,所以先打印父类静态块,再打印子类静态块
案例二:
1 class Parent1{ 2 static int a = 1; 3 static{ 4 System.out.println("Parent1 static block"); 5 } 6 } 7 class Child1 extends Parent1{ 8 static int b = 66; 9 static{ 10 System.out.println("Child static block"); 11 } 12 } 13 public class MyTest1 { 14 static{ 15 System.out.println("MyTest1 static block"); 16 } 17 public static void main(String[] args) { 18 Parent1 parent;//声明引用并没有创建实例(不是主动使用) 19 System.out.println("---------------"); 20 parent = new Parent1(); 21 System.out.println(parent.a); 22 System.out.println(Child1.b); 23 } 24 }
运行结果:
分析结果:
程序中对子类的"主动使用"会导致父类被初始化,但对父类"主动使用"并不会导致子类初始化。
(不可能生成一个Object类的实例就导致系统中所有子类都会被初始化)
案例三:
1 class Parent3{ 2 static int a = 3; 3 static { 4 System.out.println("Parent3 static block"); 5 } 6 static void doSomething(){ 7 System.out.println("do Something..."); 8 } 9 } 10 class Child3 extends Parent3{ 11 // static int a = 6; 12 static{ 13 System.out.println("Child3 static block"); 14 } 15 } 16 public class MyTest3 { 17 public static void main(String[] args) { 18 System.out.println(Child3.a);//没有主动使用Child3类不会导致该类初始化 19 Child3.doSomething(); 20 } 21 }
运行结果:
分析结果:只有当程序访问静态变量或者静态方法确实在当前类或接口中定义时,
才认为是对类或接口的主动使用
5.小结
1.类加载过程:
【加载】 ---->【连接(验证-准备-解析)】---->【初始化】---->【使用】----->【卸载】
2.类的初始化时机分析是否
-主动使用 才会初始化它们
-被动使用
3.final修饰静态属性时,分析该变量是在编译时就可以得到值;还是在程序运行时得到值,如果在运行时获取该变量的值,那么就会初始化该类或接口。
4.接口初始化是发生在程序首次使用接口的特定静态属性时,会初始化该接口