类加载和对象的初始化过程

类的初始化和对象初始化是两个不同的概念。类的初始化是发生在类加载过程,是类加载过程的一个阶段,该阶段并不调用类的构造器。而对象的初始化是在类加载完成后为对象分配内存,实例变量的初始化,实例变量的赋值及调用类构造器完成对象的初始化过程。对象初始化也称为对象实例化。本文主要是探索和分析类的加载过程及对象的实例化过程,主要参考《java编程思想》和《深入理解java虚拟机》,文章有错误之处还希望大家批评指正。

类加载

每个类编译后都对应一个独立的class文件,该文件只有在需要使用时才会被加载。一般来说,Class文件在初次使用时才加载,即一般情况下只加载一次。类的加载过程包括几个阶段:加载,连接(包括:验证,准备,解析),初始化,使用,卸载。这些阶段只是按顺序的开始并不代表一个阶段完成之后接着执行下一个阶段

类加载发生在以下几种情况:
1)new生成新的对象实例。
2)使用java.lang.reflect包的方法对类进行发射调用时。
3)当子类进行加载或初始化时。当加载一个类时,如果发现其存在父类并且未被加载则会继续加载父类。
4)虚拟机启动时,用户指定的执行主类(包含main()的执行入口类),虚拟机会加载加载该类。
5)调用类的变量(静态字段但非静态常量),类方法

注意
1. 调用类的静态字段,只有直接定义这个字段的类才会被加载和初始化。通过其子类来引用父类中定义的字段,只会触发父类的初始化而不会触发子类的初始化。
2.调用类的静态常量是不触发类的加载过程。如果在A类中调用B类的静态常量,那么在编译阶段会将该静态常量放到A的Class文件的静态常量池中,所以对该常量的调用不涉及B的加载。

class SuperClass{
    static{
        System.out.println("SuperClass 类初始化");
    }
    static int a=3;
    static final int b=4;
}
class SubClass extends SuperClass{
    static{
        System.out.println("SubClass 类初始化");
    }
}
public class Test {
    public static void main(String[] args) {
        System.out.println(SubClass.a);
    }
}

结果是:

SuperClass 类初始化
3

这里调用了父类的静态变量a,父类进行了初始化,但子类并没有进行初始化。
如果执行:

System.out.println(SubClass.b);

那么结果是:

4

两个类都没有进行初始化,因为此时b是类常量。

加载阶段

加载阶段虚拟机主要完成以下三件事:
1)根据类的路径,定位并获取类的class文件
2)通过加载器加载class文件,并将class文件里所代表的静态存储结构转化为方法区的运行数据结构
3)在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

java虚拟机内存可以分为五部分:方法区,堆,程序计数器,java虚拟机栈,本地方法栈。方法区和堆是所有线程共享的内存区域。程序计数器,java虚拟机栈和本地方法栈都是线程私有的,即每个线程有用自己的这三块内存。
方法区:是虚拟机的一块内存,主要是存储类信息,编译后的代码等数据
:主要是放对象实例,分为新生代和老年代
程序计数器:是当前线程所执行的字节码行号的指示器
java虚拟机栈:java方法(字节码)的内存模型,每个方法被执行时都会同时创建一个栈帧用于存储局部变量表,操作数栈,方法出口等信息。
本地方法栈:功能与java虚拟机栈相同,但是为本地(native)方法服务。

验证阶段

确保Class文件的字节流包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身安全。该阶段包括四个部分:
1.文件格式验证:验证字节流是否符合Class文件格式的规范
2.元数据验证:对字节码描述的信息进行语义分析,以确保其描述的信息符合java语言规范要求
3.字节码验证:进行数据流和控制流分析,保证校验类的方法在运行时不会做出危害虚拟机安全的行为。
4.符号引用验证:在虚拟机中将符号引用转换为直接引用

准备阶段

正式为类变量分配内存并设置类变量的默认值,这些内存在方法区中进行分配。内存分配仅针对类变量(static变量),不包括实例变量,实例变量是在对象实例化时和对象一起在堆中分配内存。
类变量的默认值的设置和为了保证变量使用的安全性在对象实例化过程中虚拟机自动地对实例变量进行设置默认值是一样的。默认值的设置如下:

数据类型 默认值
int 0
long 0L
char ‘\u0000’
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null

解析

将常量池内的符号引用替换为直接应用的过程

初始化

初始化是执行类构造器() (在准备阶段提供的代码,即执行所有类变量的赋值动作及静态代码块内容)。在执行类构造器()时保证父类的()已经执行完毕。即一般我们会看到先执行父类的static方法块内容和static变量的赋值,然后再执行子类的static方法块内容和static变量赋值。

注意:接口与类不同,执行接口的静态变量初始化操作是不需要先执行执行父类的静态变量初始化操作(接口不含有静态方法块)。只有父类接口中定义的变量被子类调用时父接口才会被初始化。接口的实现类在初始化时不一定会执行接口的静态变量初始化操作。

class A{
    A(String str){
        System.out.println("A 在"+str+" 中被创建");
    }
}
interface SuperInterface{
    static A a=new A("SuperInterface");
}
class SuperInterfaceImp implements SuperInterface{
    static A a=new A("SuperInterfaceImp");
}
public class Test {
    public static void main(String[] args) {
        new SuperInterfaceImp();
    }
}

该代码运行结果是:

A 在SuperInterfaceImp 中被创建

所以接口的实现类在初始化时不一定会执行接口的静态变量初始化操作。

对象的实例化

对象实例化是new一个实例,当然也可以通过放射机制得到一个实例化对象。该过程会触发类加载过程。在类完成加载后,才进行对象的初始化其他操作,对象初始化过程包括类加载过程(如果之前已经加载过了,就不含该过程),对象内存分配,设置默认值,赋值操作,执行构造器。

创建对象的初始化过程
1)类加载过程和上面是一样的。如果在加载当前类(假设为C)过程中发现,其存在父类B,加载B,如果发现B还有父类继续加载其父类,直到Object。在完成Object的整个加载过程,包括类的初始化。然后进行Object的子类进行类加载过程,递归的执行该操作直到类C完成加载过程,完成类的初始化。(该过程的前提是这些类都没有被加载过)
3)然后进行对象初始化的其他操作(4,5,6,7步奏),该步骤先从基类开始,然后到其子类进行这些操作,直到类C完成这些操作。
4)首先在堆上对象分配足够的存储空间。
5)对这块存储空间进行清零,即自动的为对象中的所有基本类型都设置为默认值,引用设置为null。
6)执行所有出现于字段定义处的初始化动作,即属性的赋值操作(相当于定义 int i=2; 这块空间被赋值了两次了,第一次是空间清零,i赋值为0。然后根据字段的赋值将i赋值为2.)
7)执行构造器。

为了直观地体会对象的创建过程,可以看下面这段代码:

//基类
class A{
    D d=new D("A");
    static{
        System.out.println("load A");
    }
    public A(){
        System.out.println("Create A");
    }
}
//B是A的子类
class B extends A{
    //字段,用于查看字段的初始化时间
    D d=new D("B");
    static{
        System.out.println("load B");
    }
    public B(){
        System.out.println("Create B");
    }
}
class C extends B{
    D d=new D("C");
    static{
        System.out.println("load C");
    }
    public C(){
        System.out.println("Create C");
    }
}
class D{
    static{
        System.out.println("load D");
    }
    D(String str){
        System.out.println("D在类"+str+"中初始化");
    }
}
public class Test{
    public static void main(String[] args) {
        new C();//实例化了对象C 
    }
}

A是B的基类,B是C的基类。并且它们都有自己的static静态方法块,构造器,即字段D;本例主要是创建C的对象,来观察在继承关系中的类加载和初始化过程。以上代码的运行结果如下:

load A
load B
load C
load D
D在类A中初始化
Create A
D在类B中初始化
Create B
D在类C中初始化
Create C

从结果中我们可以知道,步骤如下:
一、类加载过程
1. 当创建C对象时,加载器加载C的class文件。在这过程中会发现它存在基类B,那么加载器继续加载B类的class文件。加载B时,发现B还有一个基类A,那么就会继续加载A的class文件。
2. 从基类开始一直到C完成整个加载过程(加载,验证,准备,解析,初始化)
所以显示 load A,load B ,load C,load D。发现D属于多个类的类变量,但只被加载一次。

二,进行初始化
1.对象初始化的其他过程从基类A开始,一直到当前类C。
2.在堆上为A分配内存,并为所有字段设置为默认值。在这里讲d设置为null。
3.根据字段的初始化代码,对字段进行赋值初始化。 (此时调用了D类的class文件,并调用static静态方法块。虽然D在A,B,C中都有出现,但仅加载一次D的class文件和执行一次static静态方法块代码。然后调用D的构造器,构造器每次创建对象时都会调用)。
4.调用基类A的构造。
5.接着就对A的导出类B执行和A相同的操作,分配内存,字段的初始化,调用构造器。
6.最后是对C执行相同操作,当完成C构造器调用后,整个初始化过程就完成了。
(这回D对象被多处实例化)

对于类加载过程中的使用阶段,在类是实例化过程就是对class的一个使用,属于使用阶段。类被加载、连接和初始化后,它的生命周期就开始了。在虚拟机的生命周期中,始终不会被卸载。生命周期的结束意味着当前类对象或类成员没有引用指向它们,则虚拟机开始调用垃圾回收机制,清理类对象和类信息,类卸载完成。

阅读更多
个人分类: Java
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭