类加载过程

文章目录

  • JVM生命周期结束的方式
  • 类的加载,链接和初始化
  • 类的加载
    • 1.类加载器
    • 2.类加载方式
    • 3.加载结果
    • 4.加载时机
        • 注意:这里和初始化时类的主动调用不一样,类的加载就算不是初次主动调用也会加载。
  • 类的连接
    • 1. 验证
    • 2. 准备
        • 注意:之所以说是静态成员变量而不是实例变量,是因为我们在加载类的时候并还没有实例,自然就没有实例对象
    • 3. 解析
  • 3.类的初始化
        • 注意:所有类或者接口只有在类被首次主动调用的时候才进行初始化
  • 4.流程图
  • 类的调用方式
  • 下面的代码有什么不同吗?

JVM生命周期结束的方式

  1. 程序正常结束
  2. 调用System.exit()
    exit()的参数0表示正常退出,非0表示异常结束
  3. 程序执行过程中报错导致异常结束
    在程序运行过程中,如果出现错误,但是并没有处理,JVM接收到后就会退出,即我们常见的闪退。
  4. 操作系统异常导致JVM结束进程
    这个一般是底层系统异常导致JVM退出,通常见得少。

类的加载,链接和初始化

我们在编辑器编辑的java代码不能直接执行,首先要编译生成class文件,然后将class文件加载到内存中,通过连接、解析和初始化,才可以被使用。

类的加载

加载就是将类的.class文件中的二进制数据读入到内存中,将其放在运行时的数据区的方法区内,同时在堆区创建一个Java.lang.class对象,用来封装类在方法区中的数据结构,可以看到,不管我们创建多少个实例,class对象只有一个,在堆区。

graph LR
A(Java程序)-->|调用Class的方法比如newInstance|B(堆区内描述类的class对象)
B-->C(方法区中类的数据结构)

1.类加载器

  1. SystemClassLoader 系统加载器
  2. ExtClassLoader 扩展加载器
  3. BootClassLoader 根类加载器
    在api中查看classloader的getclassloader中可以得知,如果返回null表示该类是通过根加载器加载的。
  4. Java.Lang.ClassLoader的子类,用户自定义加载器

2.类加载方式

  1. 从本地系统直接加载
  2. 从网络地址加载
  3. 从数据库加载
  4. 从jar包加载
  5. 将java源文件动态编译成class文件

3.加载结果

经过加载器进行加载,最终我们得到的是位于堆区的Class对象,Class对象封装了类在方法区的数据结构并且向Java程序开发者提供了访问方法区的数据结构的接口

4.加载时机

类的加载不是开始全部加载,JVM有预判算法,在预料到某个类将要被使用的时候就进行预先加载,如果在预先加载过程中出现class损坏或不存在,则在类被首次主动使用的时候类加载器报LinkAgeError,(这个错误由JVM处理,我们一般接触不到)否则不报错。如果这个类一直没有被使用,那类加载器一直不报错。

注意:这里和初始化时类的主动调用不一样,类的加载就算不是初次主动调用也会加载。

类的连接

类的连接是指将加载到内存中的类的二进制数据合并到虚拟机的运行环境中去。

1. 验证

确保被加载类的正确性,验证点如下:
1. 类文件结构的检查
java的类文件结构从上到下是包名,import,类,入口Main,方法/变量
2. 语义检查
确保语法正确,如Final类没有子类
3. 字节码验证
确保字节码流能够被JVM正确的执行
4. 二进制兼容性验证
两个由不同版本的jdk编译生成的class则有可能存在二进制不兼容的问题

2. 准备

在准备阶段,JVM为类的静态成员变量分配内存,并设置默认值。

 //在准备阶段,下面这个Worker类,我们给age分配四字节内存,并赋予默认值 0
 public Class Worker{
     private static int age = 1;
     
     static {
         age = 100;
     }
     
 }
 //byte是1字节内存,默认值是0
 //short是2字节内存,默认值是0
 //int是4字节内存,默认值是0
 //long是8字节内存,默认值是0
 
 //float是4字节内存,默认值是0.0
 //double是8字节内存,默认值是0.0
 
 //boolean是1字节内存,默认值是false
 //char是1字节内存,默认值是'\u0000'
 

注意:之所以说是静态成员变量而不是实例变量,是因为我们在加载类的时候并还没有实例,自然就没有实例对象

3. 解析

在解析阶段,JVM会将类的二进制数据中的符号引用转换成直接引用.如下:

public Class Worker{
    public static int age = 0;
    public static Car car;
    public static void gotoWork(){
        car.run();//这段代码在Worker类的二进制文件中表示为符号引用
    }
}

在Worker类的二进制数据中,包含了对car的run方法的符号引用,他由run方法全名和操作符组成,在解析阶段,虚拟机会将符号引用转换成一个指针,该指针指向Car类run方法所在的内存位置,这个指针就是直接引用。

3.类的初始化

在初始化阶段,JVM为类的静态成员变成赋予正确的初始化值。

public Class Worker{
    public static int age = 0;
    static{
        age = 100;
    }
}

可以在声明变量的时候初始化,也可以在静态块中初始化。

注意:所有类或者接口只有在类被首次主动调用的时候才进行初始化

4.流程图

graph LR
加载-->链接
链接-->验证
链接-->准备
链接-->解析
解析-->初始化

类的调用方式

  • 主动调用
  1. 创建类的实例
public Class Worker{
    public static int age = 0;
    public static Car car;
    public static void gotoWork(){
        car.run();
    }
}
Worker worker = new Worker();
  1. 调用类的静态方法
Worker.gotoWork();
  1. 访问类或接口的静态变量,或者对静态变量赋值
Worker.age = 100;
  1. 反射获取类对象,Class.forname()方法
Class clazz = Class.forName("***.***.Worker");
  1. 初始化类的子类
public Class WoodWorker extends Worker{
    ...
}
WoodWorker woodWorker = new WoodWorker();
//这个时候Worker就算是主动调用
  1. Java虚拟机启动的时候被标记为启动类
//一般如果没有eclipse或者as,我们通常通过以下几步来编写代码
1.文本编写java文件
2.javac 命令生成class文件
3.java 入口类执行代码
在第三步,我们java Worker来执行Worker,那么Worker就算主动调用
  • 被动调用
    除了主动调用外的所有方式都是被动调用

下面的代码有什么不同吗?

public class TypicalCode {
    private static TypicalCode typicalCode = new TypicalCode();
    public static int a;
    public static int b = 0;
    
    // private static TypicalCode typicalCode = new TypicalCode();
    //不同地方,测试结果会不相同
    TypicalCode(){
        a++;
        b++;
    }

    public static TypicalCode getTypicalCode(){
        return typicalCode;
    }
}
    //测试代码
    @Test
    public void test(){
        TypicalCode typicalCode = TypicalCode.getTypicalCode();
        System.out.println(typicalCode.a+"");
        System.out.println(typicalCode.b+"");
    }
    
    //这就是在初始化阶段赋值导致的不同

你可能感兴趣的:(Java)