java类的加载与初始化总结
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 class Test8 { 2 3 public static void main(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 class Super{ 21 static String 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 static String getC(){ 31 return c; 32 } 33 34 static String getB(){ 35 return "b"; 36 } 37 }
2、‘单例模式’中静态成员初始化问题
1 public class Test9 { 2 3 public static void main(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 class Single{ 17 private static Single sin = new Single(); 18 public static int b; 19 public static int c = 0; 20 21 private Single(){ 22 b++; 23 c++; 24 } 25 26 public Single getInstance(){ 27 return sin; 28 } 29 }
3、构造方法内的多态对初始化的影响
1 public class Test10 { 2 3 public static void main(String[] args) { 4 new SubTest(); 5 6 //输出为:SuperTest() before draw() 7 // SubTest() ,i = 0 8 // SuperTest() after draw() 9 // SubTest() ,i = 1 10 11 //分析:因为没有静态成员,所以在用new创建子类对象时,先在堆中为该对象分配足够空间(内存空间全置二进制的0,即基本类型为默认值,引用类型为null), 12 //然后,调用父类构造方法(有实例变量会先初始化实例变量),但draw()调用的是子类的重写方法,那么问题是,这时候子类实例变量i只分配了内存空间(默认值为0), 13 //还没有初始化,所以输出的i为0。直到子类初始化实例变量时,i才被赋值为1,最后执行子类的构造方法,所以输出i为1。 14 } 15 16 } 17 18 19 class SuperTest{ 20 SuperTest(){ 21 System.out.println("SuperTest() before draw()"); 22 draw(); //调用子类的重写方法(多态) 23 System.out.println("SuperTest() after draw()"); 24 } 25 void draw(){ 26 System.out.println("super draw"); 27 } 28 29 } 30 31 class SubTest extends SuperTest{ 32 private int i = 1; 33 SubTest(){ 34 System.out.println("SubTest() ,i = "+i); } 35 @Override 36 void draw(){ 37 System.out.println("SubTest() ,i = "+i); 38 } 39 }
总结:类的加载与初始化顺序上面已经总结了。但实际判断时任然需要谨慎。
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),
这不就是“初始化”的意思嘛?而“执行赋值表达式”更像是用户根据自己的程序需要设置自定义的初始值,而不是分配内存空间后的默认值(这应该就是通常意义的“初始化”了吧)。而这个设置自定义初始值的行为,
即可以是在变量的定义处,也可以是在构造方法中,或者在需要时刻的方法调用中(惰性初始化)。而这种设置自定义初始值的行为的正确保证,就是上面总结的“类的加载与初始化顺序”的严格顺序执行。