掘金 后端 ( ) • 2024-04-18 10:56

一、前言

当我们完成了Java源代码编写并通过编译和打包流程生成了一个包含主类(其中含有main函数)的JAR文件后,执行java -jar XXX.jar命令时,JVM加载和执行操作以启动应用程序。在不关心JVM内部执行过程的前提下,那么这个过程主要流程是这个样子的:

image.png
上图展示了一个主要流程,其实这个主要流程就是jvm的类加载机制,所谓的类加载机制,就是将class文件加载到内存中,形成可以被JVM可以直接使用的java类型。由这个图我们衍生出来个问题:

  • 加载这个class文件都产生了什么操作?

我们带着问题继续往下看。

二、类的加载过程:

类的加载过程,主要有以下几步,加载->验证->准备->解析->初始化。

加载:就是从我们的磁盘文件或者其他存储介质中(如数据库),读取.class文件,获取二进制字节流,二进制字节流转化为方法区的运行时数据结构,并在内存中生成一个class对象,作为访问方法区这个类的各种数据访问入口。 (备注:方法区主要存放类型信息,常量、静态变量等信息),下面用一段简单的demo来解释下这句话什么意思。

public class Demo {
    private String address ;
    private int age;

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
       //加载类Demo.class文件,并在内存中生成一个Class对象,clazz就是这个对象的引用,代表的是Demo类
        Class<?> clazz = Class.forName("com.leetcode.jvm.Demo");
        //通过clazz对象访问方法区中的属性、创建对象,间接访问方法区中的类数据
        Field[] declaredFields = clazz.getDeclaredFields();
        for (int i = 0; i < declaredFields.length; i++) {
            System.out.println(declaredFields[i].getName());
        }
        Demo instance = (Demo) clazz.newInstance();
        System.out.println(instance);
    }
}

验证:验证这个class文件是否符合JVM的数据格式规范(包括文件格式等)

准备:给类静态变量分配内存空间,并给类的静态变量赋零值(基本数据类型为零值,这个零值不一定代表的是0,基本数据类型的默认值和引用类型的默认值),但是如果是final修饰的静态常量,则会赋给定的值,然后放在常量池中。
解析:将类的符号引用替换成直接引用。这个东西说的比较抽象,这里稍微解释下。符号引用是出现在编译阶段的,符号引用包括全限定类名、字段的名称、方法的名称等。比如在我这个类DemoClass中,有个静态方法fly(),那么在DemoClassB中如果引用了这个方法,那么在DemoClassB的字节码文件中,不会存储这个方法的内存地址,只是存储这个方法的引用,即全限定类名+方法描述(类似这种形式com.jvm.classload.DemoClass#fly())。直接引用是出现在解析阶段的,可以直接指向类、字段或者方法在内存中的物理位置的一种引用方式。类加载器会根据DemoClassB这个符号引用(com.jvm.classload.DemoClass#fly(),类似这种)找到类DemoClass,并确定这个符号引用对应的内存物理地址。以后每当访问fly方法的时候,可以直接拿着这个内存地址进行访问,而不是拿着符号引用进行查找,这就是符号引用就被替换成了直接引用。
初始化:执行静态代码块和给静态变量赋指定初始值。
以上就是类加载的主要过程的一个阐述,但是不过要注意的是,类加载过程,是一个懒加载的过程,只有这个类被真正用到的时候,才会被加载,比如访问类的静态方法,new一个对象的时候等,下面用一个简单的demo来演示下这个问题:

public class DemoClass {
  static {
      System.out.println("DemoClass的静态代码块执行了");
  }
  public DemoClass() {
      System.out.println("DemoClass的构造方法");
  }
}
public class DemoClassOther {
    static {
        System.out.println("DemoClassOther的静态代码块执行了");
    }
    public DemoClassOther() {
        System.out.println("DemoClassOther的构造方法");
    }
}
public class ClassLoadDeep {
    public static String name = "lisi";

    static {
        System.out.println("主类静态代码块的执行");
    }

    public static void main(String[] args) throws Exception {
        DemoClass demoClass = new DemoClass();
        DemoClassOther other = null;
    }

}

代码的运行结果如下:

image.png

三、类加载器

上面那个类的加载过程,是由什么去执行的呢?这个时候,我们就要引出一个比较重要的东西,类加载器

在jvm中,除了引导类加载器、扩展类加载器、应用类加载器,还可以定义类加载器。引导类加载器是JVM自身实现中的一部分,由C++语言实现,扩展类加载器和应用类加载器是由java语言实现,java语言实现的类加载器有一个公共的父类ClassLoader,这个看一下类的关系图实现就清楚了。那么这几种类加载器的作用肯定是加载类的,但是他们的区别是什么,我们先看一下他们的定位或,或者说是加载的内容。

  • 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar。
  • 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下,ext扩展目录中的JAR 类包。
  • 应用类加载器:看到应用这两个词,就是加载我们自己应用的类文件的,即我们打包过后classpath路径的类文件。
    jvm实例创建的时候,会去实例化引导类加载器,然后这个引导类加载器去加载rt.jar中com.misc.Launcher这个类,在加载Launcher这个类的时候,会去创建扩展类加载器和应用类加载器的实例,流程大概是下面这个样子的:

image.png

我们从源码中细看一下:

image.png 从图中我们可以看到,Launcher在被加载的时候,由于是静态变量,即类变量,然后在类加载过程中的初始化阶段的时候,就已经赋初始值了,调用自己的构造函数,在构造函数中,实例化了扩展类加载器和应用类加载器,是不是清晰点了呢。
我们再去验证下,这三种类加载器加载的内容具体是什么,写一个简单的demo如下:

public class DemoClassLoader {
    public static void main(String[] args) {
        //引导类加载器加载的类文件 Integer位于rt包中
        System.out.println(Integer.class.getClassLoader());
        //扩展类加载器加载的类文件
        System.out.println(com.sun.crypto.provider.AESKeyGenerator.class.getClassLoader().getClass().getName());
        //应用类加载器加载的类文件-我们自己写的类
        System.out.println(DemoClassLoader.class.getClassLoader().getClass().getName());

    }
}

三、结束语

如上,如有不对的地方,还请各位大佬批评指正。毕竟还是一只成长中的小菜鸟。下一篇,浅谈JVM类加载机制-类加载器的双亲委派模型。