1、触发类加载的原因(主动调用与被动调用):
六种主动调用:
1)、创建类的实例(new操作、反射、cloning、反序列化)
2)、调用类的静态方法
3)、使用或对类/接口的static属性赋值(不包括static final的与在编译期确定的常量表达式(包括常量、字符串常量))
4)、调用API中的反射方法,Class.forName()等。
5)、子类被初始化
6)、被设定为JVM启动时的启动类(含main方法的主类)
其它都为被动引用:被动引用不会触发类的初始化操作(只会加载、链接),如仅申明一个类的引用、通过数组定义引用类等。
2、类的加载的完整生命过程
加载、链接(验证、准备、解析)、初始化、使用、卸载
1)、加载
i)、java编译器加载类的二进制字节流文件(.class文件),如果该类有基类,向上一直加载到根基类(不管基类是否使用都会加载)。
ii)、将二进制字节码加载到内存,解析成方法区对应的数据结构。
iii)、在java逻辑堆中生成该类的java.lang.Class对象,作为方法区中该类的入口。
类加载器:分默认加载器和用户自定义加载器
Bootstrap ClassLoader:顶层加载器,由c++实现。负责JVM启动时加载JDK核心类库以及加载后面两个类加载器。
Extension ClassLoader:继承自ClassLoader的类,负责加载{JAVA_HOME}/jre/lib/ext目录下的所有jar包。
App ClassLoader:上面加载器的子对象,负责加载应用程序CLASSPATH目录下的class文件和jar包。
Customer ClassLoader:用户继承自ClassLoader类的自定义加载器,用来处理特殊加载需求。如Tomcat等都有自己实现的加载器。
类加载器采用双亲委托(自底向上查询)来避免重复加载类,而加载顺序却是自顶向下的。
2)、链接
i)、验证:字节码完整性、final类与方法、方法签名等的检查验证。
ii)、准备:为静态变量分配存储空间(内存单元全置0,即基本类型为默认值,引用类型为null)。
iii)、解析(这步是可选的):将常量池内的符号引用替换为直接引用。
类的加载和链接只执行一次,故static成员也只加载一次,作为类所拥有、类的所有实例共享。
3)、初始化
包括类的初始化、对象的初始化。
类的初始化:
初始化静态字段(执行定义处的赋值表达式)、执行静态初始化块。
注:有父类则先递归的初始化父类的。
对象的初始化:
如果需要创建对象,则会执行创建对象并初始化:
i)、在堆上为创建的对象分配足够的存储空间,并将存储单元清零,即基本类型为默认值,引用类型为null。
i)、初始化非静态成员变量(即执行变量定义处的赋值表达式)。
ii)、执行构造方法。
注:如果有父类,则先递归的初始化父类成员,最后才是本类。
4)、使用
5)、卸载
对象的引用(栈中)在超出作用域后被系统立即回收;对象本身(堆中)被gc标记为垃圾,在gc下次执行垃圾处理时被回收。
总结:
一个类最先初始化static变量和static块;
然后分配该类以及父类的成员变量的内存空间,再赋值初始化,最后调用构造方法;
在父类与子类之间,总是优先创建、初始化父类。
即:(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器,其中基类总是优先于子类的。
详细分析例题:
1、static静态成员初始化细节
1 public classTest8 {2
3 public static voidmain(String[] args) {4 System.out.println(Super.a);5 System.out.println(Super.b);6 System.out.println(Super.bb);7 System.out.println(Super.c);8 System.out.println(new Super("cc").c);9 //对于“初始化”的意思,应该包括初始化和执行赋值表达式(如果有的话)。例如static成员的初始化实际上包括两步:准备阶段(JVM链接)中的内存分配(全置0,即基本类型成默认值,引用类型为null)和初始化中的static成员赋初值操作(即如果有的话,执行static字段定义处的赋值表达式)10 //对于a、b 因为有赋初值的表达式,故会得到自定义的初始值。对于c则采用准备阶段的null。11 //只有创建对象才会调用构造方法(执行构造方法中的动作)。b的值被重新设置。
12
13 Super sup2 = new Super("ccc"); //相当于每次新建对象都对'实例所共享、类所有的'b重新设值(是重新给b赋值,不是新建)。
14 System.out.print(Super.c);15 }16
17 }18
19
20 classSuper{21 staticString a;22 static String b =getB();23 static String bb =getC();24 static String c = "c";25
26 Super(String s){27 c =s;28 }29
30 staticString getC(){31 returnc;32 }33
34 staticString getB(){35 return "b";36 }37 }
View Code
2、‘单例模式’中静态成员初始化问题
1 public classTest9 {2
3 public static voidmain(String[] args) {4 System.out.println(Single.b);5 System.out.println(Single.c);6 //在Single类加载的链接阶段静态字段都置默认值(基本类型为默认,引用为null),所以sin、b、c首先为null、0、0。7 //然后按照定义的顺序执行初始化赋值,先执行sin的赋值,因为用new创建对象,所以会执行构造方法,然后b=1,c=1。这时因为static字段只加载一次,所以b、c只是做赋值操作(有赋值表达式的话),所以b没操作,c重新赋值为0。8
9
10 //那么,如果交换静态字段sin和c的位置,上面输出?
11 }12
13 }14
15
16 classSingle{17 private static Single sin = newSingle();18 public static intb;19 public static int c = 0;20
21 privateSingle(){22 b++;23 c++;24 }25
26 publicSingle getInstance(){27 returnsin;28 }29 }
View Code
3、构造方法内的多态对初始化的影响
1 public classTest10 {2
3 public static voidmain(String[] args) {4 newSubTest();5
6 //输出为:SuperTest() before draw()7 //SubTest() ,i = 08 //SuperTest() after draw()9 //SubTest() ,i = 110
11 //分析:因为没有静态成员,所以在用new创建子类对象时,先在堆中为该对象分配足够空间(内存空间全置二进制的0,即基本类型为默认值,引用类型为null),12 //然后,调用父类构造方法(有实例变量会先初始化实例变量),但draw()调用的是子类的重写方法,那么问题是,这时候子类实例变量i只分配了内存空间(默认值为0),13 //还没有初始化,所以输出的i为0。直到子类初始化实例变量时,i才被赋值为1,最后执行子类的构造方法,所以输出i为1。
14 }15
16 }17
18
19 classSuperTest{20 SuperTest(){21 System.out.println("SuperTest() before draw()");22 draw(); //调用子类的重写方法(多态)
23 System.out.println("SuperTest() after draw()");24 }25 voiddraw(){26 System.out.println("super draw");27 }28
29 }30
31 class SubTest extendsSuperTest{32 private int i = 1;33 SubTest(){34 System.out.println("SubTest() ,i = "+i); }35 @Override36 voiddraw(){37 System.out.println("SubTest() ,i = "+i);38 }39 }
View Code
总结:类的加载与初始化顺序上面已经总结了。但实际判断时任然需要谨慎。
i)、对于很多书上说的和大家挂在嘴边的“初始化”一词,如‘初始化‘静态变量、‘初始化’实例变量。这里的初始化我的更细入的理解是,‘初始化’包括“分配内存空间”和“执行赋值表达式”两步。
ii)、“分配内存空间”,即将获取到的内存单元全部置为二进制的0(对于基本类型自然就是默认值,对于引用类型都为null),而这一步是不管变量定义处的赋值表达式的。如int a ; int b =1; 在这一步都是一样置为二进制的0的。
“执行赋值表达式”,即是在变量“分配内存空间”后对变量的赋值操作。如 int a;int b =1; 在这一步a没有赋值操作,b就有赋值操作了,然后a依然还是分配内存空间后的默认值,而b就重新赋值为1了。
iii)、“初始化”即先分配内存空间,再对变量执行赋值表达式(如果有的话)。这样分先后的意义保证了对变量的赋值前,变量已经获取到了正确的初始内存空间。如static变量的初始化,实际上在’准备阶段‘就分配好内存单元,
在’初始化阶段‘的第一步才执行定义处的赋值表达式。这就是例一中考察的重点,在分配内存空间后与执行定义处的赋值操作后得到的值不一样。又如实例变量的初始化,他的所谓“初始化”也是分两个阶段的,不过他的两个
阶段间相隔的操作不多,所以当作一个概念通常不会出问题,但遇到例三的情况就出问题了。参考《Thinking In Java》中的建议就是“尽量在构造方法中慎用非final或private(隐式为final)方法”
iiii)、对于我的理解把“初始化”细化为“分配内存空间”和“执行赋值表达式”两步,其实也挺纠结的。’分配内存空间’即包括内存空间的初始分配,然后变量也自然得到初始值了(对于基本类型自然就是默认值,对于引用类型都为null),
这不就是“初始化”的意思嘛?而“执行赋值表达式”更像是用户根据自己的程序需要设置自定义的初始值,而不是分配内存空间后的默认值(这应该就是通常意义的“初始化”了吧)。而这个设置自定义初始值的行为,
即可以是在变量的定义处,也可以是在构造方法中,或者在需要时刻的方法调用中(惰性初始化)。而这种设置自定义初始值的行为的正确保证,就是上面总结的“类的加载与初始化顺序”的严格顺序执行。