深入理解虚拟机执行子系统——Class文件的结构

本文详细介绍了Java虚拟机中的Class文件结构,包括魔数、版本号、常量池、访问标志、类索引、字段表集合、方法表集合以及属性表集合等关键组成部分。通过对Class文件的深入理解,可以更好地掌握Java字节码和虚拟机执行子系统的工作原理。
摘要由CSDN通过智能技术生成

再谈无关性

Java语言诞生时所喊的口号是“一次编写,到处运行”(Write Onece,Run Anywhere),足以看见当时的开发者们对冲破平台限制的渴望。Java虚拟机屏蔽了各个平台的差异,使得这个口号成为现实。从Java诞生至今已经有20多年,相比当初它已经强大了很多,以至于我们不得不重新审视它的平台无关性。如今Java虚拟机已经不是专门用于执行Java语言了,一切符合Java虚拟机规范的语言都可以被编译成.class文件被Java虚拟机加载执行,比如Groovy、Kotlin、Scala、JPython等。如今使用这些语言的人相对还不多,谁能保证日后Java虚拟机在语言无关性上的优势不会赶上甚至超越它在平台无关性上的优势呢?
不论是平台无关性还是语言无关性,它们的基础仍然是虚拟机和字节码存储格式。Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。

Class文件的组成

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
**从内容的角度来看, Class文件主要包含的内容是无符号数和表。**后面的解析都要以这两种数据类型为基础,所以先解释清楚这两个概念。
无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。
为了更直观的解析class文件,使用NotePad++ (安装HexEditor插件)后打开.class文件如下图:
二进制的class文件

魔数与Class文件的版本

每个class文件的头4个字节被称为魔数。它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。从上图我们得知class文件的头4个字节是“ca fe ba be”(咖啡宝贝?!)。这个魔数似乎也预示着日后“Java”这个商标名称的出现……
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。从上图得知,我们的主版本号是16进制下的34换算成10进制等于52. Java的主版本号是从45开始(45=JDK1.1),因此可以得知我们的主版本号是JDK1.8。次版本号为0,因此当前对应的Java版本号是JDK1.8.0。我们验证一下:
Java版本号

注意,_201是1.8.0版本下的补丁版本号。

常量池

紧接着主、次版本号之后的是常量池入口。常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它还是在Class文件中第一个出现的表类型数据项目
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不同,这个容量计数是从1而不是0开始的,从上图得知常量池的容量为0x0016=十进制下的22,得出当前常量池中有21项常量。

在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。

常量池中主要存放的数据是:字面量和符号引用。
1.字面量:比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。
2.符号引用:属于编译原理方面的概念,主要包含这些常量:被模块导出或者开放的包(package)、类或接口的全限定名、字段的名称和描述符、方法的名称和描述符、方法句柄和方法类型、动态调用点和动态常量(JDK1.8的lambda表达式还记得嘛?)

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接(具体见第7章)。也就是说,在Class文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态连接的内容,在介绍虚拟机类加载过程时再详细讲解。

常量池中每一项常量都是一个子表,最初常量表中共有11种结构各不相同的表结构数据,后来为了更好地支持动态语言调用,额外增加了4种动态语言相关的常量,为了支持Java模块化系统(Jigsaw),又加入了CONSTANT_Module_info和CONSTANT_Package_info两个常量,所以截至JDK 13,常量表中分别有17种不同类型的常量。
这17类表都有一个共同的特点,表结构起始的第一位是个u1类型的标志位,代表着当前常量属于哪种常量类型。17种常量类型所代表的具体含义如下表所示:
常量池类型
我们使用javap命令反编译一下看一下class文件的常量池到底长啥样:
源码:

package com.leon.util.json;

import javax.xml.ws.RequestWrapper;

/**
 * @author created by leon on 2020-04-10
 * @since v1.0
 */
public class XmlUtil {
   

    private String strVal;
    private int intVal;
    private int[] intArr;

    private static final String CONSTANT_STR = "leon";

    private static int STATIC_INT_VAL = 1;

    @RequestWrapper
    public void method1() {
   
        int count1 = 10;
        double dcount2 = 5.0D;
    }

    public void method2(String str) {
   

    }

    public String method3() {
   
        return new String("leon");
    }

    public String method4(String str) {
   
        return null;
    }

    public void method5(int count) {
   
        if (count < 1) {
   
            System.out.println(count);
        } else {
   
            count -- ;
            method5(count);
        }
    }

    public void method6() {
   
        this.method5(5);
    }


    public static void main(String[] args) {
   
        System.out.println("main method.");
    }

    class InnerClass {
   
        private int innerCount1;
        private String innerCount2
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值