Java类加载过程和对象实例化详解

Java虚拟机类加载过程

  • 类加载时机
  • 类加载过程
    –加载
    –验证
    –准备
    –解析
    –初始化

1、类加载时机

        类从被加载虚拟机内存中开始,到卸载出内存为止,他的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载。其中验证、准备、解析3个阶段统称为连接。

类的生命周期

        对于初始化阶段,虚拟机规范则严格规定了“**有且只有**”5种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前开始):

  1. 使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用Java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
  3. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时,用户需要制定一个主类,虚拟机会先初始化这个主类
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic 、REF_invokeStatic的方法的句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化。

这5中情况称为对一个类的主动引用,除此之外,所有引用累的方式都不会触发其初始化,称为被动引用。

  1. 通过子类引用父类的静态字段、不会导致子类初始化
  2. 通过数组定义来引用类,不会触发此类的初始化
  3. 常亮在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
注意:接口与类在初始化时有很大区别,当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在正真使用到父接口的时候(如引用接口中定义的常量)才会初始化。

2、类的加载过程

一、加载

        “加载”是“类加载”的一个阶段,在加载阶段虚拟机要完成如下三件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

        非数组类的加载可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器来完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass方法)

        对于数组类而言,数组类本身不通过类加载器创建爱你,他是有Java虚拟机直接创建的。但是数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终要靠类加载器去创建,一个数组类的创建爱过程遵循如下规则:

  1. 如果数组的组件类型(指该数组去掉一个维度的类型)是引用类型(包括类、接口、数组、枚举、标注),那就递归使用类加载过程去加载这个组件类型,数组将在加载该组件类型的类加载器的类名称空间上被标识
  2. 如果数组的组件类型不是引用类型(例如int[]),Java虚拟机会把数组标记为与引导类加载器关联
  3. 数组类的可见性与他的组件类型的可见性一致,如果组件类型不是引用类型,那数组的可见性将默认为public
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法去中的数据存储格式由虚拟机实现,然后在内存中实例化一个java.lang.Class累的对象(并没有明确规定实在Java堆中,**对于HotSpot虚拟机而言,Class对象比较特殊,他虽然是对象,但是存储在方法区里**),这个对象将作为程序访问方法去中的这些类型数据的外部接口。

二、验证
  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证
三、准备

        准备阶段是正事为类变量分配并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个时候进行内存分分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

         通常情况下是数据类型的零值,String类型的null值,Boolean的false等,如:
public static int value = 123;
那变量value在准备阶段过后的初始值为0而不是123。
但是如果类的字段被final修饰,那么该value就会被初始化为被指定的值,如: public static final int value = 123;
编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会将其值设置为123.

四、解析

         解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可;直接引用可以使直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。解析主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

  • 类或接口的解析
  • 字段解析
  • 类方法解析
  • 接口方法解析

五、初始化

         在准备阶段变量已经付过一次系统要求的初始值了,而在初始化阶段,则根据程序员制定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器(clinit)的过程。

  • clinit方法是有编译器自动收集类中的所有类变量的复制动作和静态语句块(static{})中的语句结合产生的,编译器手机的顺序是有语句在原文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在他之后的变量,在前面的静态代码块可以赋值,但是不能访问

public class Test{
    staic{
        i = 0; //给变量赋值可以正常编译通过
        System.out.println(i); //这局编译器会提示“非法向前引用”
    }
    static int i = 1;
}
  • clinit方法与类的构造函数(或者说示例构造函数init)不同,他不需要显示的调用父类构造器,虚拟机会保证在子类的clinit方法执行之前,父类的clinit方法已经执行完毕。因此在虚拟机中第一个被执行的clinit方法的类肯定是java.lang.Object
  • 由于父类的clinit方法先执行,也就意味着父类重定义的静态代码块要优于子类的变量赋值操作
  • static class Parent{
        public static int A = 1;
        static{
            A = 2;
        } 
    }
    static class Sub extends Parent{
        public static int B = A;
    }
    public staic void main(){
        System.out.println(Sub.B);
    }
    
    字段B的值将会是2而不是1
  • clinit方法对于类或接口来说并不是必需的,如果一个类中没有静态代码块,也就没有对变量的复制操作,那么编译器可以不为这个类生成clinit方法
  • 接口中不能使用静态代码块,但仍然有变量初始化的复制操作,因此接口与类一样都会生成clinit方法。只有接口与类不同的是,执行接口clinit方法不需要先执行父接口的clinit方法。只有当父类接口中定义的变量使用的时候,父接口才会初始化,另外,接口的实现类在初始化时也一样不会执行接口的clinit方法
  • 虚拟机会保证一个累的clinit方法在多线程环境中被正确加加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这类的clinit方法其他线程都会阻塞等待,直到活动线程执行clinit完毕。

  • 具有父类的对象实例化过程

             说了这么多来点实际的。。。。。。

    public class Fu {
        private static int age = 100;
        private String sex;
        private int fuid = 1;
        //静态代码块
        static{
            System.out.println("Fu static code block run, age:"+age);
        }
        //代码块
        {
            System.out.println("Fu:id:"+fuid);
            System.out.println("Fu code block run");
        }
        public Fu(){
            System.out.println("Fu constructor run");
            func();
        }
        public Fu(String sex){
            this.sex = sex;
        }
        public void func(){
            System.out.println("Fu func run,sex:"+this.sex+"  fuid:"+fuid);
        }
    }
    
    public class Zi extends Fu {
        private String sex = "man";
        private int ziid = 2;
        //静态代码块
        static{
            System.out.println("Zi static code block run");
        }
        //代码块
        {
            System.out.println("Zi:id:"+ziid);
            System.out.println("Zi code block run");
        }
        public Zi(){
            System.out.println("Zi constructor run");
            func();
        }
        public Zi(String sex) {
            super();
            this.sex = sex;
        }
        public void func(){
            System.out.println("Zi func run,sex:"+this.sex+"  Ziid:"+ziid);
        }
    }
    
    public class Test{
        public static void main(String[] args) {
            Fu obj = null;
            System.out.println("+++++++++++++++++");
            obj = new Zi();
        }
    }

    输出结果

             这里在new Zi()对象的时候,虚拟机会发现方法区中并没有该类信息,就会先去加载该类,在加载的时候发现该类有父类,则会先加载该类的父类,然后进行父类类变量的初始化(包括准备阶段的默认初始化和初始化阶段的显示初始化),这里会先输出父类静态代码块的信息Fu static code block run, age:100,然后进行子类的初始化,输出Zi static code block run,类变量初始化完毕。接着进行对象的创建,进入子类构造函数,在子类构造函数第一行会递归调用父类构造函数,进入父类构造函数之前会先进行父类实例变量初始化,即给非静态成员变量初始化,并运行代码块,输出Fu:id:1和Fu code block run,然后进入父类构造函数,输出Fu constructor run,当运行func方法时,发现子类对其进行了覆盖,那么运行子类的func方法Zi func run,sex:null Ziid:0,由于子类还没有进行实例变量的显示初始化,只能输出null和0 ,父类构造函数运行结束返回到子类构造函数,此时先会进行子类实例变量的显示初始化,运行了子类代码块,输出Zi:id:2 Zi code block run,接着运行子类构造函数,输出Zi constructor run,接着运行func,这时子类实例变量都进行了显示初始化,输出Zi func run,sex:man Ziid:0

  • 6
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值