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 }
View Code

 

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 }
View Code

 

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 }
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),

      这不就是“初始化”的意思嘛?而“执行赋值表达式”更像是用户根据自己的程序需要设置自定义的初始值,而不是分配内存空间后的默认值(这应该就是通常意义的“初始化”了吧)。而这个设置自定义初始值的行为,

      即可以是在变量的定义处,也可以是在构造方法中,或者在需要时刻的方法调用中(惰性初始化)。而这种设置自定义初始值的行为的正确保证,就是上面总结的“类的加载与初始化顺序”的严格顺序执行。

 

参考:java中类的加载顺序介绍(ClassLoader)

          JAVA类加载和初始化

         Java系列笔记(1) - Java 类加载与初始化

 

posted @ 2017-07-28 23:01  衿沫青冥  阅读(184)  评论(0编辑  收藏  举报