Java的类加载机制详解

如果别人问你精通java吗?谈一下类加载机制的理解。

你回答:首先将java编译成承认的class字节码文件,然后运行...

这样的答案,别人听了肯定不会满意的。

就比如,好多类一级一级的继承,加上静态代码块,重写无参构造方法,最后来一个测试类,让你判断输出的顺序,想必这样的题目,准确来讲应该是笔试题,绝大多数是笔试题,你又能是否答对。

现在呢,我们就好好理解一下java里面的类加载机制

当我们的java代码编译完成后,会生成对应的class文件,接着我们运行java Demo命令的时候,我们其实已经启动了jvm虚拟机执行class字节码文件的内容。而JVM虚拟机执行class字节码文件的过程大致分为六个阶段:加载,验证,解析,初始化,卸载。

下面针对这6个阶段好好理解一下:

1.加载:

加载阶段是类加载过程的第一个阶段。在这个阶段,jvm的主要目的是将字节码从各个位置(网络,磁盘等地方)转化为二进制字节流加载到内存中,接着会为这个类在JVM的方法区创建一个对象的Class对象,这个class对象就是这个类各种数据的请求入口。

其实加载阶段用一句话来描述就是:把代码数据加载到内存中。

2.验证:

当JVM加载完成class字节码文件并在方法区创建对应的Class对象之后,JVM便会启动对该字节码流的校验,只有符合JVM字节码规范的文件才能被JVM正确执行。其中校验分为以下几步:

JVM规范校验:JVM会对字节流进行文件格式校验,判断其是否符合JVM规范,是否能被当前版本的虚拟机处理。

代码逻辑校验:JVM会对代码组成的数据流和控制流进行校验,确保JVM运行该字节码文件后不会出现致命错误。

3.准备:

当完成字节码文件的校验之后,JVM便会开始为类变量分配内存并进行初始化。这里需要注意两个关键点:即内存分配的对象以及初始化的类型。内存分配的对象,java中的变量有[类变量]和[类成员变量]两种类型,[类变量]指的是被static修饰的变量,而其他所有类型的变量都属于[类成员变量]。在准备阶段JVM只会为【类变量】分配内存,而不会为【类成员变量】分配内存。【类成员变量】的内存分配需要等到初始化阶段才开始

比如以下的代码在准备阶段,只会为age属性分配内存,而不会回website分配内存。

    public static int age=3;
    public String website="HelloWorld";
    //eg:下面的代码在准备阶段之后,age的值将是0,而不是3
    public static int age1=3;
    //但是如果一个变量是常量(被final static修饰的话),那么在准备阶段,属性便会被赋予用户希望的值。
    //eg:例如下面的代码在准备阶段就已经被赋予希望得到的值
    public final static int age2=3;
    //这其中的道理我们想下就明白了,两个语句的区别是有没有final关键字修饰,
    // 被final修饰的常量,是不允许再次发生改变的,所以在初始化的时候就进行了赋值操作

4.解析:

当通过准备阶段之后,JVM针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符7类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中直接引用。

5.初始化使用:

到了初始化阶段,用户定义的java程序代码才会真正开始执行。在这个阶段,JVM会根据语句执行顺序对类对象进行初始化,一般来说,当JVM遇到下面的情况的时候才会触发初始化:遇到new,getstatic,putstatic,invokestatic这四个字节码指令时,如果类没有进行初始化,则需要先触发其初始化,生成这4条指令的最常见java代码场景就是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

6:卸载:
当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象,最后负责运行的JVM也退出内存。

【实战分析】

了解java的类加载机制之后,下面我们通过几个简单的例子来测试一下:

/**
 * Created on 2019/4/1.
 * Title: Simple
 * Description:超类
 * Copyright: Copyright(c) 2018
 * Company: 
 *
 * @author wy
 */
public class GrandFather {
    static {
        System.out.println("GrandFather在静态代码块");
    }
}
/**
 * Created on 2019/4/1.
 * Title: Simple
 * Description:父类
 * Copyright: Copyright(c) 2018
 * Company: 
 *
 * @author wy
 */
public class Father extends GrandFather {
    static {
        System.out.println("Father在静态代码块");
    }

    public static int age = 25;

    public void father() {
        System.out.println("我是father~");
    }
}
/**
 * Created on 2019/4/1.
 * Title: Simple
 * Description:普通类
 * Copyright: Copyright(c) 2018
 * Company: 
 *
 * @author wy
 */
public class Son extends Father {
    static {
        System.out.println("Son在静态代码块");
    }
    public void son(){
        System.out.println("我是Son~");
    }
}
/**
 * Created on 2019/4/1.
 * Title: Simple
 * Description:测试类
 * Copyright: Copyright(c) 2018
 * Company: 
 *
 * @author wy
 */
public class Test {
    public static void main(String[] args) {
        System.out.println("Father的岁数"+Son.age);
    }
}

test结果:

也许会有人问为什么没有输出【Son在静态代码块】这个字段串?

这是因为对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),因为通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

【分析】

对面上面的这个例子,我们可以从入口开始分析:

首先程序到main方法这里,使用标准化输出Son类中的age变量,但是Son类中并没有这个类成员变量,于是往父类中去找,我们在Father类中找到了对应的类成员变量,于是触发了Father的初始化。

但根据我们上面说到的初始化情况,我们需要先初始化Father类的父类,于是我们会先初始化Grandfather类,并输出【GrandFather在静态代码块】,在初始化Father类输出【Father在静态代码块】。

最后所有父类都初始化完成之后,Son类才能调用父类的静态变量从而输出【Father的岁数:25】

 

类会在首次被“主动使用”时执行初始化,为类(静态)变量赋予正确的初始值。在Java代码中,一个正确的初始值是通过类变量初始化语句或者静态初始化块给出的。而我们这里所说的主动使用 包括:
1. 创建类的实例
2. 调用类的静态方法
3. 使用类的非常量静态字段
4. 调用Java API中的某些反射方法
5. 初始化某个类的子类
6. 含有main()方法的类启动时

初始化一个类包括两个步骤:
1、 如果类存在直接父类的话,且直接父类还没有被初始化,则先初始化其直接父类
2、 如果类存在一个初始化方法,就执行此方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值