class字节码文件组成(1)

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情

java如何运行

java文件由程序员编写,但是不能直接运行,需要经历如下阶段才可以运行。

.java文件 ----经历java编译器 javac编译 ,此过程会对我们代码进行自动优化 ------------ 》.class文件 (又叫java字节码文件) ---------java虚拟机解释----->机器码 ------》交给操作系统运行

.class文件又叫字节码文件,它只面向java虚拟机,不面向任何操作系统。这里学习一下.class文件的组成结构


如何查看.class文件信息

.class文件是字节码文件,一字节八位,我们采用16进制查看,每两个数字(0 - F)组成一字节。使用NotePad++或其他支持工具。

查看字节码

  • 写一个java类,编译一下生成class文件

简单的Person类加两个属性

public class Person {
    private String name;
    private int age;
    //getter and setter
}
复制代码

编译生成的class文件没什么大的区别,只不过会给我们自动生成无参构造函数

public class Person {
    private String name;
    private int age;
    public Person() {
    }
}
复制代码
  • 使用NotePad++打开

这是16进制的形式,可确定每2个数字代表一个字节,并且内存连续。

image-20220804161050110.png

javap

javapjava class文件的分离器,可以对class文件进行简单解释,使得程序员不用直接面对字节码。

基本上使用 javap -v classpath\classname.class 来查看

当然如果class文件过大,终端显示不友好,可以将信息输出到文件查看。

使用命令:javap -v classpath\classname.class > filename

会输出如图所示的内容,相对于字节码令人更有食欲一些.

image-20220804162500618.png

jclasslib

使用idea插件jclasslib分析class文件。

安装:

设置 -->Plugins->到Marketplcae搜索下载

使用:

view ->show bytecode with JclassLib

image-20220807175157830.png

jclasslib为我们友好的分了类: image-20220807175316455.png


class文件内容

class文件字节码结构

示意图:

image-20220807010445339.png

魔数

魔数(magic),是JVM用于识别是否是JVM认可的字节码文件。

所有由java编译器生成的class字节码文件的首四个字节码都是CA FE BA BE。

JVM准备加载某个class文件到内存的时候,会首先读取该字节码文件的首四位字节码,判断是否是CA FE BA BE,如果是则JVM认可,如果不是JVM则会拒绝加载该字节码文件。

Class文件不一定都是由.java文件编译而来的,Kotlin以及其他java虚拟机支持的都可以。

比如:

使用Kotlin写一个类:

image-20220804174802920.png 编译过后查看其字节码:

也是cafebabe开头的

image-20220804174837725.png

版本号

版本号包括主版本号(major_version)和副版本号(minor_version)。

我们一般只需要关注主版本号,平常所说的java8其实是java1.8。副版本号主要是对主版本的一个优化和bug修复。目前java版本都来到了18了。

主版本号占用7、8两个字节,副版本号占用5、6两个字节。JDK1.0的主版本号为45,以后版本每升级一个版本就在此基础上加一,那么JDK1.8对应的版本号为52,对应16进制码为0x34。

一个版本的JVM只可以加载一定范围内的Class文件版本号,一般来说高版本的JVM支持加载低版本号的Class文件,反之不行。JVM在首次加载class文件的时候会去读取class文件的版本号,将读取到的版本号和JVM的版本号进行对比,如果JVM版本号低于class文件版本号,将会抛出java.lang.UnsupportedClassVersionError错误。

我们修改一下Person.class关于版本号的数据,提高class文件的版本号为0x39 ,为10进制57,jvm版本为java1.13。

通过java <classpath>.classname运行一下:

image-20220805114144243.png

说我们的jvm只支持运行java版本最高为52的class文件,也就是java1.8

image-20220805114453890.png

同时也可以通过javap命令查看当前class文件支持的最低jvm版本。

image-20220805133137424.png

常量池计数器(constant_pool_count)

紧跟于版本号后面的是常量池计数器占两个字节。记录整个class文件的字面量信息个数,决定常量池大小。

constant_pool_count = 常量池元素个数 + 1。 只有索引在 (0,constant_pool_count)范围内才会有效,索引从1开始。

常量池数据区(constant_pool)

常量池类似于一张二维表,每一个元素代表一条记录,包含class文件结构及其子结构中引用的所有字符串常量、类、接口、字段和其他常量。且常量池中每一个元素都具备相似的结构特征,每一个元素的第一字节用做于识别该项是哪种数据类型的常量,称为tag byte

访问标志(access_flags)

用于表示一个类或接口的访问权限。占用两个字节。

标记 值(0x) 作用
ACC_PUBLIC 0x0001 公共的
ACC_FINAL 0x0010 不允许被继承
ACC_SUPER 0x0020 需要特殊处理父类方法
ACC_INTERFACE 0x0200 标记为接口,而不是类
ACC_ABSTRACT 0x0400 抽象的,不可被实例化
ACC_SYNTHETIC 0x1000 表示由编译器自己生成的
ACC_ANNOCATION 0x2000 表示注解
ACC_ENUM 0x4000 表示枚举
  • ACC_SYNTHETIC

由编译器自己生成的代码,比如一些桥接方法,我们写一个类实现一个范型接口

然后使用javap -v查看字节码信息

public class AboutACCSYNTHETIC implements Comparator<String> {
    @Override
    public int compare(String o1, String o2) {
        return 0;
    }
}
复制代码

会发向编译器会为我们生成一个桥接方法,类型是Object的,且访问标志存在 ACC_SYNTHETIC

  public int compare(java.lang.String, java.lang.String);
    descriptor: (Ljava/lang/String;Ljava/lang/String;)I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=3
         0: iconst_0
         1: ireturn
      LineNumberTable:
        line 16: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       2     0  this   Lcom/roily/jvm/day01/AboutACCSYNTHETIC;
            0       2     1    o1   Ljava/lang/String;
            0       2     2    o2   Ljava/lang/String;

  public int compare(java.lang.Object, java.lang.Object);
    descriptor: (Ljava/lang/Object;Ljava/lang/Object;)I
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=3, locals=3, args_size=3
         0: aload_0
         1: aload_1
         2: checkcast     #2                  // class java/lang/String
         5: aload_2
         6: checkcast     #2                  // class java/lang/String
         9: invokevirtual #3                  // Method compare:(Ljava/lang/String;Ljava/lang/String;)I
        12: ireturn
      LineNumberTable:
        line 12: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   Lcom/roily/jvm/day01/AboutACCSYNTHETIC;
复制代码
  • ACC_ENUM

表示这个类是一个枚举类

其实可以看出枚举在编译的时候会被当做一个普通类处理,只不过会继承Enum

image-20220806212914672.png

image-20220806213718774.png

  • ACC_INTERFACE

表示是一个接口,而不是一个类。如果一个class文件被标识了ACC_INTERFACE那么他一定他也是抽象的,也就是得标志上ACC_ABSTRACT。

并且一个接口拿来就是为了实现的,那么就不能被标志上ACC_FINAL。

也不可以设置为ACC_ENUM和ACC_SUPER

  • ACC_ANNOTATION

表示为一个注解,被ACC_ANNOTATION标识就必须被ACC_INTERFACE标识。

image-20220806214256368.png

  • ACC_SUPER

被ACC_SUPER标识的类,调用父类的方法会特殊处理。所有版本的编译器都应该设置这个标志(除了一些低版本的编译器)。jdk1.0.2及其之前版本的编译器生成的class文件标志位都没有ACC_SUPER标志。

目前来说我们接触到的编译器都会为我们生成ACC_SUPER标识。

特殊处理指的是什么呢?

子类在调用父类的方法的时候会使用一个叫invokespecial指令。

每一个方法都有一个CONSTANT_Methodref_info 结构来描述这个方法,而这个结构是编译期就决定的,如果此刻类上面没有ACC_SUPER标识,那么 invokespecial指令就会按照编译器生成的CONSTANT_Methodref_info结构来进行父类的调用。

举个例子:以下三个类存在如下继承关系,SonSon的super.parentMethod();肯定调用的Parent的方法,那么

SonSonCONSTANT_Methodref_info结构内肯定存着这么一个信息。

public class Parent {
    void parentMethod() {
        System.out.println("parentMethod");
    }
}
class Son extends Parent {

}
class SonSon extends Son {
    void sonSonMethod() {
        super.parentMethod();
    }
}
复制代码

那么如果此刻如果我们对Son进行更新,添加一个parentMethod会怎么样呢?(不对SonSon进行重编译),只对Son重编译。如果没有ACC_SUPER标志那么SonSon调用的还是Parent的方法。如果存在ACC_SUPER标识则会特殊处理,去寻找最近的父类进行调用对应的方法。

class Son extends Parent {
    @Override
    void parentMethod() {
        System.out.println("SonMethod");
    }
}
复制代码

小结:

access_flags占用两个字节也就是16位,每一位可以表示一个ACC_FLAG,一个类存在多个ACC_FLAG会通过按位与的方式进行保存。

那么以上只有8个标志,那么还剩余的是为了以后预留的。

猜你喜欢

转载自juejin.im/post/7128418346185261063
今日推荐