面试官:说一下类加载的过程

246次阅读  |  发布于3年以前

加载

当我们要使用一个类的时候,要通过ClassLoader将类加载到内存中

「类加载阶段主要完成如下三件事情」

  1. 通过全类名,获取类的二进制流
  2. 解析类的二进制流为方法区内的数据结构
  3. 创建一个java.lang.Class类的实例,表示该类型,作为方法区这个类的访问入口

「通过全类名,获取类的二进制流的方式有很多种」

  1. 从zip压缩包中获取
  2. 从网络中获取
  3. 运行时计算生成,如动态代理技术
  4. ...

「对于非数组类型的加载阶段,即可以使用Java虚拟机内置的类加载器去完成,也可以使用用户自定义的类加载器去完成」

链接

「链接这个阶段主要分为3个部分,验证,准备,解析」

验证

「验证阶段主要是确保Class文件的格式正确,运行时不会危害虚拟机的安全」

验证阶段的规则很多,但大致分为如下4个阶段 「具体详细的内容,我就不详细解释了,可以看《深入理解Java虚拟机》,本篇文章偏向于做一个总结,把握类加载的一个整体流程,而不对细节进行阐述」

准备

「准备阶段主要是为类的静态变量分配内存,并将其初始化为默认值」

常见的数据类型的默认值如下

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

「如果类静态变量的字段属性表中存在ConstantValue属性,则直接执行赋值语句」

那么什么情况下类静态变量的字段属性表中存在ConstantValue属性呢?

  1. 类静态变量为基本数据类型,并且被final修饰
  2. 类静态变量为String类型,被final修饰,并且以字面量的形式赋值

为了方便查看Class文件的字节码,我在IDEA中下载了一个插件jclasslib Bytecode viewer,非常方便。用如下代码通过字节码的形式验证一下

public class Person {

    private static int age = 10;
    private static final int length = 160;
    private static final String name = "name";
    private static final String loc = new String("loc");
}

「所以length和name属性在准备阶段就会赋值为ConstantValue指定的值」

「那么age和loc属性会在哪个阶段赋值呢?是在初始化阶段,后面会详细介绍哈」

解析

「将类,接口,字段和方法的符号引用(在常量池中)转为直接引用」符号引用:用一组符号来描述所引用的目标 直接引用;直接指向指向目标的指针

加入我写了一个如下的类

public class Student {

    private String name;
    private int age;

    public String getName() {
        return this.name;
    }
}

以字段为例,name和age对应的对象并不是直接指向内存地址,而是用字符串来进行描述(即符号引用)。解析阶段就是将这些描述转为直接指向目标的指针(即直接引用)

初始化

「执行类静态成员变量赋值语句和静态代码块中的语句」

我们把上面的Student代码改成如下形式

public class Student {

    private String name;
    private int age = 10;
    private static int gender = 1;

    {
        System.out.println("构造代码块");
    }

    static {
        System.out.println("静态代码块");
    }

    public Student() {
        System.out.println("构造函数");
    }

    public String getName() {
        return this.name;
    }
}

可以看到字节码中包含了3个方法,getName方法我们知道,方法里面执行了哪些逻辑? 从字节码的角度分析一波

方法」

从字节码可以看到方法的主要逻辑为

  1. 调用父类的方法
  2. 非静态成员变量赋值
  3. 执行构造代码块
  4. 执行构造函数

方法」 从字节码可以看到方法的主要逻辑为

  1. 执行静态变量的赋值语句
  2. 执行静态代码块中的语句
  3. 需要注意的一点是,「Java虚拟机会保证子类的方法执行前,父类的方法已经执行完毕」

「理解方法的作用还是很有必要的,因为经常有些面试题问静态代码块,构造代码块,构造函数的执行顺序。」

我这里就直接总结一下结论,大家可以写demo验证一下

「没有继承情况的执行顺序」

  1. 静态代码块和静态成员变量,执行顺序由编写顺序决定(只会执行一次哈)
  2. 构造代码块和非静态成员变量,执行顺序由编写顺序决定
  3. 构造函数

「有继承情况的执行顺序」

  1. 父类的静态(静态代码块,静态成员变量),子类的静态(静态代码块,静态成员变量)(只会执行一次哈)
  2. 父类的非静态(构造代码块,非静态成员变量),父类的构造函数
  3. 子类的非静态(构造代码块,非静态成员变量),子类的构造函数

卸载

垃圾收集不仅发生在堆中,方法区上也会发生。但是对方法区的类型数据回收的条件比较苛刻 以下图为例,想回收方法区中的Simple类

  1. 需要保证堆中的Sample类及其子类都已经被回收
  2. 加载Sample类的MyClassLoader已经被回收
  3. Sample类对应的Class对象已经被回收

可以看到对方法区的类型数据回收的条件比较苛刻,但是收效甚微,所以有些垃圾收集器不会对方法区的类型数据进行回收

总结

类加载过程

变量的赋值过程

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8