[JVM系列]一、源码->类文件->JVM过程详解(类文件解读/类加载机制/类加载器)

708 阅读9分钟

什么是JVM

JVM是Java Virtual Machine(Java虚拟机)的缩写,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域等组成。JVM屏蔽了与操作系统平台相关的信息,使得Java程序只需要生成在Java虚拟机上运行的目标代码(字节码),就可在多种平台上不加修改的运行,这也是Java能够“一次编译,到处运行的”原因。

官网:docs.oracle.com/javase/spec…

可以看到,无论是JDK还是JRE,他们最底层都是依赖于JVM的。如果到这没理解JVM是啥,我们可以再换个角度解释JVM。

一个.java源文件的一生:

如图所示

  1. 源码到类文件

    • javac是java的编译器,把.java(高级语言) 翻译成.class(二进制文件)
  2. 类文件到JVM

    • 类加载机制
    • 类加载器
  3. JVM各种折腾(内部结构,执行方式,垃圾回收,本地调用等)

    • JVM运行时数据区
    • JVM内存模型
    • JVM垃圾回收
    • ......

总的来说,JDK里包含了JVM,不同平台的JDK不同,里的JVM也不同。JVM屏蔽了不同操作系统的不同指令集,也就实现了Java语言的 “write once run anywhere.“

源码到类文件

此过程会通过一个demo进行分析,先上demo

demo

class Person{ 
    private String name="Jack"; 
    private int age; 
    private final double salary=100; 
    private static String address; 
    private final static String hobby="Programming"; 
    private static Object obj=new Object(); 
    public void say(){ 
        System.out.println("person say...");
	} 
    public static int calc(int op1,int op2){ 
        op1=3; int result=op1+op2; Object obj=new Object(); return result;
	} 
    public static void main(String[] args){ calc(1,2);
	} 
}

将此Person.java文件编译。

Person.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> Person.class文件

得到Person.class文件

cafe babe 0000 0034 003f 0a00 0a00 2b
002c 0900 0d00 2d06 4059 0000 0000 0000
0900 0d00 2e09 002f 0030 0800 310a 0032
0033 0700 340a 000d 0035 0900 0d00 3607
0037 0100 046e 616d 6501 0012 4c6a 6176
612f 6c61 6e67 2f53 7472 696e 673b 0100
0361 6765 0100 0149 0100 0673 616c 6172
7901 0001 4401 000d 436f 6e73 7461 6e
......

类文件解读(.class解读)

参考:docs.oracle.com/javase/spec…
java8——JVM官方文档第四章,讲解class文件怎么解读。

1. 类文件的结构

官网给出的类文件组成结构:(u2代表上面demo类文件的4位16进制数,u4代表8为16进制数)

结合我们的案例来分析:

1.1 magic

the magic item supplies the magic number identifying the class file format; it has the value 0xCAFEBABE.

demo中 u4:cafebabe

0xCAFEBABE开头用于表示此文件为.class文件

1.2 minor_version, major_version

表示JDK的版本信息

demo中 u2+u2: 0000 + 0034 (34等于10进制的52,表示JDK8)

1.3 constant_pool_count

表示常量池中的常量总数量

The value of the constant_pool_count item is equal to the number of entries in the constant_pool table plus one.

demo中 u2:003f=63(10进制)

1.4 constant_pool

The constant_pool is a table of structures (§4.4) representing various string constants, class and interface names, field names, and other constants that are referred to within the ClassFile structure and its substructures. The format of each constant_pool table entry is indicated by its first "tag" byte.

The constant_pool table is indexed from 1 to constant_pool_count - 1. (所以,demo中的常量数为63-1=62个。)

常量池主要存储两方面内容:字面量(Literal)和符号引用(Symbolic References)

  • 字面量:文本字符串,final修饰等

  • 符号引用:类和接口的全限定名、字段名称和描述符、方法名称和描述符

4.4节中,给出了The Constant Pool,其每一个常量对应的格式是👇

cp_info {
    u1 tag;   //u1 代表两个16进制 -- 表示一个Constant Type的标识,
    u1 info[]; // 表示一个Constant的内容
}

各个的Constant Type的结构在官网4.4节中可以找得到,对应着前面那个Constant数量,可以读出全部的ConstantPool里面的东西。

于此同理,可以完整的解析class文件,进一步的解析过程,也就不再过多赘述,因为在JDK中有javap可以帮我们解析class文件,不用我们自己分析class文件,详情可以看官方文档4.4节。

类文件到JVM

类加载过程 (.class--->JVM)

类的加载说白了指的就是:将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,并对数据进行校验,转换解析和初始化,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。

类加载的过程,分为三个阶段:装载,链接,初始化,链接中又可分为验证,准备,解析这三个部分

装载

查找并加载类的二进制数据,装载阶段虚拟机需要完成一下三件事:

  1. 通过类的全限定类名,找到class文件,来获取类的二进制字节流

  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

  3. 在Java中生成一个代表这个类的 java.lang.Class对象,作为对方法区中这些数据的访问入口。

装载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个 java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

链接

1.验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

  • 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object之外。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:确保解析动作能正确执行

2.准备

为类的静态变量分配内存,并将其初始化为默认值(不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中)

例如 假设一个类变量的定义为:public static int a = 10;

那么变量a在准备阶段过后的初始值为0,而不是10,把a赋值为10的动作将在初始化阶段才会执行。

3.解析

将符号引用转变成直接引用

符号引用:只是符号,JVM能够认识,没有实际含义,不会占用物理机器中的地址

直接引用:直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄,要落地到真实的物理内存

初始化

给之前的准备完成的静态变量赋予实际的值

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(如 Class.forName()
  • 初始化某个类的子类,则其父类也会被初始化
  • Java虚拟机启动时被标明为启动类的类,直接使用 java.exe命令来运行某个主类

类加载器ClassLoader

类加载器有多种种类:各司其职,可分为4类👇

JVM类加载机制

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

  • 父类委托(双亲委派),先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。比如说,在我们的classpath目录下也定义了一个java.lang.String类,按正常来说,这个类应该由App ClassLoader加载。但实际上,为了防止java核心库里面的类被破坏,他会先尝试让父类加载,父类再让父类加载,如果父类加载不成功,再退回给自己加载。也叫双亲委派机制。

  • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

JVM系列文章:

[JVM系列]三、一文搞懂JVM垃圾回收

[JVM系列]二、一文彻底搞懂 JVM运行时数据区 和 JVM内存结构

[JVM系列]一、源码->类文件->JVM过程详解(类文件解读/类加载机制/类加载器)