类加载子系统

image-20230207110025295
  1. 通过类加载子系统(Class Loader)加载字节码文件(.class)
  2. 通过执行引擎来执行
  3. 如果执行过程中需要调用本地方法(native), 则通过本地方法栈和本地方法接口交互

image-20230207110257999

类的声明周期(类的加载过程)

  1. 加载Loading

  2. 链接Linking

    1. 验证
    2. 准备
    3. 解析
  3. 类初始化Initialization

  4. 使用Using

    • 实例化对象
    • 静态方法
    • ……
  5. 卸载Unloading

    有些类可以卸载, 但有些类不可以卸载

image-20230207141007939

Loading加载阶段

从各种源(class文件, zip压缩包, 动态代理运行时计算)到内存, 并在内存中构建出Java类的原型类模板对象

加载阶段具体任务

  1. 通过类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流解析转换为方法区的运行时数据结构(Java类模板)
  3. 在堆空间中生成一个代表这个类的java.lang.Class对象, 指向Java类模板, 作为方法区中这个类的各种操作的访问入口

Class文件的本质

Class文件本质上是一个二进制流, 保存为(.class)文件在磁盘上存储只是其一种保存形式. 如果通过网络接收到一个符合Class文件要求的二进制比特流, 也可以将其作为某个类加载到内存中. 常见的二进制流(Class文件)的获取方式有:

  • .class后缀的文件
  • jar包, zip等压缩包
  • 存放在数据库中的二进制数据
  • 通过http之类的网络协议传输的二进制数据

什么是类模板对象

类模板对象是Java类在JVM中的一个快照, JVM将从字节码文件中解析出来的常量池, 类字段, 类方法等信息存储到类模板中, 这样JVM在运行期便能通过类模板来获取Java类的任意信息, 能够对Java类的成员变量进行遍历, 也能进行Java方法的调用. 这也是Java反射机制的基础, 不需要创建对象, 就可以查看加载类中的方法, 属性等等信息

image-20230212165529375

Linking链接阶段

image-20230207144457659

验证Verify

验证阶段虽然使得拖慢了整个类加载速度, 但是却避免了字节码在运行时还需要进行各种检查. 正所谓磨刀不误砍柴工

  • 格式检查

    格式检查实际上会和加载loading同时进行

    魔数检查0xCAFEBABE

    版本检查

    长度检查

  • 语义检查

    是否继承final类或者重写了final方法

    是否有父类

    抽象方法是否实现

    是否存在不兼容的方法, 比如仅仅返回值不同的方法签名

  • 字节码验证

    跳转指令是否指向正确位置

    操作数类型是否合理

  • 符号引用验证

    符号引用的直接引用是否存在

准备Prepare

为类中的静态变量分配内存, 并设置默认值0

  • final修饰的类变量会在编译期间被优化, 本质上相当于一个常量的符号引用, 编译期简单替换即可

  • 方法区中为类变量(static修饰)分配内存, 并设置默认初始值(广义0)

  • 准备阶段不会为实例变量在方法区中分配内存, 实例变量随着对象一起分配在

public class HelloWorld {

    public HelloWorld() {
        //instanceVariable: 10->30, 无论先后
        this.instanceVariable = 30;
    }

    private int instanceVariable = 10;

    // 类变量, 在类加载过程的准备阶段preparation, classVariable01, classVariable02 都会被初始化为0
    static {
        // 可以为y赋值, 但是不可以调用y
        // 之所以可以先赋值, 后声明, 是因为在prepare阶段已经为该变量设置了默认值为0.
        // 也就是说只有在initialization初始化阶段才整合static代码块, 此时只需要重新按顺序赋值即可
        // classVariable02: 0->20->10
        classVariable02 = 20;
        // System.out.println(classVariable02);        //报错: 非法的前向引用, 除非static变量声明在前面
    }

    static int classVariable01 = 100;

    static int classVariable02 = 10;
    //final static常量, 在编译的时候确定
    final static int XIONG = 100;


    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = a + b;
        System.out.println("hello world");
    }
}

解析Resolve

将常量池中的符号引用转换为直接引用的过程

Initialization初始化阶段

image-20230207150314047

  • 执行类构造器方法<clinit>()的过程, 该方法不需要定义, 是javac编译器自动收集类中所有的static相关的代码合并而来, 按照源文件中的顺序执行.

  • <clinit>()只会执行一次, 且虚拟机会为该过程加锁, 不需要在源代码中显式加锁

  • <clinit>()不同于类构造器, 类构造器在字节码文件中作为<init>()

  • 父类clinit<>() -> 子类clinit()

    class Father {
        public static int x = 1;
    }
    
    public class Son extends Father {
        // 初始化顺序: Father.x -> Son.x -> Son.y
        public static int y = x;
    
        public static void main(String[] args) {
            System.out.println(Son.y);
        }
    }

    image-20230207160341522

clinit的线程安全问题

可能发生死锁的现象描述:

线程A先加载 ClassA, 线程B先加载 ClassB, 而在 ClassAclinit<>() 方法中需要加载 ClassB, 但在 ClassB<clinit>() 方法中需要加载 ClassA

类的主动使用和被动使用

代码中出现的类都会被加载, 但是却不一定都进行初始化, 只有符合主动使用的类才会被初始化, 即被动使用的类只加载但不初始化.

主动使用

被动使用

Using使用阶段

Unloading卸载阶段

变量默认初始化和显示赋值总结

public class Application {

    // static final + 字面量 => 编译阶段初始化赋值, prepare阶段显式赋值
    public static final int INTSta = 1;
    public static final int NUM = new Random().nextInt(10);
    public static int anIntSta = 2;

    public static final Integer INTEGERSTA = 10;
    public static Integer integerSta = 20;

    public static final String s0 = new String("hello world 0");
    public static final String s1 = "hello world 1";
    public static String s2 = new String("hello world 2");
    public static String s3 = "hello world 3";
}

最终结论:

static final + 字面量赋值外的类变量 static final + 字面量 实例变量 final 实例变量
默认初始化 prepare阶段 编译阶段 实例对象内存空间开辟后
显式赋值 initialization阶段, 即字节码的<clinit>()方法 prepare阶段 构造器方法中首行, 即字节码的<init>()方法中

只要赋值涉及到构造器或者类静态方法的调用, 都需要在<clinit>()中进行赋值

并不是使用static final修饰的量即为常量, 但不使用static final修饰的一定不是常量. 除字面量方式赋值static final String s = "hello world";的形式外, 其他涉及引用类型变量的任何形式, 都不是常量, 包括static final String s = new String("hello world");

image-20230212195546805

image-20230212202408504

Integer和int

  • Integer和Integer比较: 直接比较地址
  • Integer和int比较: Integer调用intValue()进行自动拆箱, 然后和int进行值的比较
  • Integer的valueOf()方法, 对于值在[-128, 127]之间的, 通过缓存获取同一个内存对象, 否则才新建对象

源代码

public class IntegerAndInt {
    public static void main(String[] args) {
        Integer x1 = 5;
        Integer x2 = 5;
        int y = 5;

        Integer z1 = 128;
        Integer z2 = 128;
        int w = 128;

        System.out.println(x1 == x2);//true
        System.out.println(x1 == y); //true

        System.out.println(z1 == z2);//false
        System.out.println(z1 == w); //true
    }
}

字节码

image-20230208015735331

实例变量的初始化过程

  • 类变量: 在Prepare阶段设置默认值, 在Initialization阶段顺序赋值覆盖

  • 实例变量(实例字段): 创建对象时先在堆空间开辟空间, 设置默认值, 然后开始调用构造函数. 在构造器被调用后对字段赋值覆盖

class Person {
    int x = 10;

    public Person() {
        print();
        x = 20;
    }

    public void print() {
        System.out.println("Person.x : " + x);
    }
}


class Student extends Person {
    int x = 30;

    public Student() {
        print();
        x = 40;
    }

    @Override
    public void print() {
        System.out.println("Student.x : " + x);
    }
}

public class ExtendMain {
    public static void main(String[] args) {
        // 1. 测试父类 <- 父类
        Person p = new Person();
        System.out.println(p.x);
        System.out.println("##################");

        // 2. 测试父类 <- 子类, 用父类接受子类对象
        Person person = new Student();
        System.out.println("##################");

        // 属性不存在多态性, 即在Student对象中, Person.x和Student.x是共存的
        System.out.println(person.x);
        // 方法被重写, Student对象中只剩下一个print()方法, 无论通过person来调用还是强转为student来调用, 都会执行Student类中的print()
        person.print();
        System.out.println("##################");

        // 强制类型转换
        Student sp = (Student) person;
        System.out.println(sp.x);
        System.out.println("##################");

        // 3. 测试子类 <- 子类
        Student student = new Student();
        System.out.println(student.x);
        System.out.println("##################");
    }
}

image-20230208121515912

变量的初始化和赋值流程

设置默认值 -> 显式初始化 -> 构造器中初始化 -> 对象.属性赋值

  • 默认初始化: 随着对象在堆空间中分配内存时, 设置默认值
  • 显式初始化: 在实例变量(实例字段)的变量声明是显式使用private int x = 10;
  • 构造器初始化: 在构造器中进行初始化x = 20;
  • 赋值: 此时不属于初始化过程, 在对象创建完成后, 通过Obj.x = 90;进行赋值

final修饰的不可变量

使用final修饰的不可变量

  • 可以在初始化过程中修改值, 但是不可以对其进行赋值. 即对象完成构建后不能修改
  • 必须在显式初始化和构造器初始化中二选一, 并且只能进行一次初始化(不考虑设置默认值)
public class AddMain {
    // static final 修饰的NUM即常量10的符号引用, 是一个常量, 必须在声明的时候指定
    private static final int NUM = 10;

    // final修饰的COUNT是一个不可变量, 会在实例对象创建时
    // 1. 先分配堆空间内存, 并设置默认值
    // 2. 再对COUNT进行显式初始化或者构造器初始化
    private final int COUNT = 20;
    private int number = 2;
    private int num = 1;

    public AddMain() {
        // COUNT = 10; // COUNT显式初始化和构造器初始化二选一
    }


    public int add() {
        num = num + 1;
        return num;
    }

    public int addOne() {
        num++;
        return num;
    }

    public int addOne2() {
        ++num;
        return num;
    }

    public static void main(String[] args) {
        AddMain app = new AddMain();
        // app.COUNT = 10; //报错, final变量不可以赋值
        app.num = 10;
    }
}

面试题

image-20230212154244899


   转载规则


《》 熊水斌 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
使用数据库连接池来获取连接 使用ResultSetHandler来处理获取查询返回对象 使用QueryRunner来执行一些通用查询 使用DbUtils工具类来关闭数据库连接
2023-05-17
下一篇 
常量池表的解读 创建MethodrefInfo类刻画CONSTANT_Methodref_info public class MethodrefInfo { // 类信息(其实也只有类名信息) ClassInfo clas
2023-05-17
  目录