解读JVM06-类文件结构

JVM如何实现平台无关性和语言无关性的?

平台无关性:使用java编译器为java源码为统一存储格式的字节码文件,实现不同平台的JVM去解释执行字节码。

语言无关性:实现不同语言的编译器,编译成统一存储格式的字节码文件,由某平台JVM去解释执行字节码。

 

什么是高位在前?

就是各个字节上的各个bit代表的数据的数位是从高到低。

123,表示一百二十三,就是高位在前的大端数;如果表示三百二十一,就是高位在尾的小端数。

 

对class文件数据结构表的理解。

类似于C语言结构体,仍旧由无符号数组成,用于描述有层次关系的复合结构的数据。

field_info {

    u2             access_flags;

    u2             name_index;

    u2             descriptor_index;

    u2             attributes_count;

    attribute_info attributes[attributes_count];

}

 

利用class文件数据结构如何表示某一类型数据的集合?

通常是一个前置的容器计数器n + n个连续的数据项组成。

如字段集合由1个u2类型的field_count和field_count个field_info数据项组成。

 

查看class文件的版本号。

方法一:用十六进制工具查看class文件,第7、8个字节即为版本号。

Linux工具:

od -t x1 Test.class   内核命令,推荐

hexdump Test.class

Windows工具:

Winhex16   还需要额外装个软件

文本编辑器的hexdump插件   如notepad++,vscode,推荐‘

方法二:使用反编译工具

javap -version Test.class

 

字节码查看器:

jclasslib bytecode viewer  用于查看字节码格式,神器呀

bytecode-viewer  一个java8 jar和android apk反向工程套件

 

由低版本JDK执行高版本class文件引发的异常。

Exception in thread "main" java.lang.UnsupportedClassVersionError: WriteLog : Unsupported major.minor version 52.0  

52.0对应jdk1.8即class文件是由jdk1.8的编译器生成,而JVM的版本低于1.8时,报不支持的Class版本错误

 

理解常量池。

常量池中的常量并不是指java中被static final修饰的变量,而是整个字节码文件的一个共享资源仓库,方便被后面的数据项重用。分成两大类:字面量和符号引用。

字面量:

基本类型字面量:当基本类型被static final修饰时生成,byte、char、boolean被转换成int

private final static int = 2;

字符串字面量:java中在任何地方出现的字符串字面量都被生成1个CONSTANT_String_info和1个CONSTANT_Utf8_info类型。

private String str = "hello";

utf-8字面量:

字节码文件中所有需要用到字符串描述的地方都会生成CONSTANT_Utf8_info类型。

 

符号引用:用来准确描述类、字段和方法的字符串。

描述类和接口:类和接口的全限定名   com/tiro/jvm/Test

描述字段:   类全限定义称.字符名称:字段描述符  com/tiro/jvm/Test.m:I

描述方法:   类全限定名称.方法名:方法描述符  java.lang.Object."<init>":()V 

 

为什么常量池的常量数量比容器计数要少1个?

常量的索引从1开始,如果有21个常量,索引从1~20

第0项常量空出来用于某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。

 

解读一个空类的class文件。

 一个空类的class文件大小为182字节,也是一个合法的java字节码文件的最小存储空间了。

javap -verbose Test.class

常量池解读:

   #1 = Methodref          #3.#10         // java/lang/Object."<init>":()V                 //描述父类默认构造方法的符号引用

   #2 = Class              #11            // com/tiro/jvm/Test      //描述当前类的符号引用 

   #3 = Class              #12            // java/lang/Object     //描述父类的符号引用 

   #4 = Utf8               <init>            //默认构造方法的名称

   #5 = Utf8               ()V               //默认构造方法的描述符

   #6 = Utf8               Code           //方法的Code属性名称,所有方法都有

   #7 = Utf8               LineNumberTable     //方法的LineNumberTable属性名称,所有方法都有

   #8 = Utf8               SourceFile    //当前类的SourceFile属性名称

   #9 = Utf8               Test.java      //当前类的SourceFile属性值

  #10 = NameAndType        #4:#5          // "<init>":()V    //默认构造方法的部分符号引用 

  #11 = Utf8               com/tiro/jvm/Test          //当前类的全限定名称

  #12 = Utf8               java/lang/Object       //父类的全限定名称

方法解读:

当类没有显式定义构造方法时,会自动生成一个不带参数的构造方法。是不是很熟悉呀!

{

  public com.tiro.jvm.Test();

    descriptor: ()V

    flags: ACC_PUBLIC

    Code:

      stack=1, locals=1, args_size=1

         0: aload_0

         1: invokespecial #1                  // Method java/lang/Object."<init>":()V

         4: return

      LineNumberTable:

        line 3: 0

}

Code属性数据项的结构自行查阅。

stack=1,代表方法运行时,操作数栈深度的最大值。

locals=1,代表方法运行时,局部变量表的最大存储空间,单位为Slot。32位虚拟机中,不超过32位的数据类型占用1个Slot,long和double占用两个Slot;64位虚拟机中,所有的数据类型占用1个Slot。

args_size=1,代表当前的参数个数,隐含参数this必有,通常是存储在局部变量表的第0个Slot。

接着就是4条执行指令:

0: aload_0  把第0个Slot的引用(即this)加载到操作数栈;

1: invokespecial #1  调用父类java.lang.Object的默认构造方法,这条指令占3个字节,后两个字节存放操作数的常量索引

4: return  退出方法执行

解读实例变量、类变量、类常量的赋值。(以基础数据类型为例)

类常量:

public class Test {  

    private final static int m = 2; 

    public int add(int n) {  

        return m + n;  

    }  

}

编译阶段:

在常量池中生成1个CONSTANT_Integer_info类型的常量;

与任何其它字面量和常量的运算立即进行;

在字段表中生成1个ConstantValue的属性,属性值指向常量池中的索引。

类加载阶段:

在加载阶段会把class文件常量池的内容放入方法区的运行时常量池时中(字符串常量被移到堆中了)

方法调用:

iconst_2 对于int类型,在编译时被直接写入指令的操作数中,不需要从运行时常量池中获取

ldc2_w #2 对于long、double类型,从运行时常量池中加载常量到操作数

类变量:

public class Test {  

    private static int m = 2; 

    public int add(int n) {  

        return m + n;  

    }  

}

编译阶段:

如果类中有static语句块和类变量赋值行为,就会生成类初始化方法<clinit>,而类变量赋值在<clinit>中进行。

类加载阶段:

在准备阶段在方法区静态域中为类变量分配内存,为类变量赋零值,而不是实际值;

在初始化阶段执行<clinit>方法为类变量赋值。

方法调用:

getstatic #2 调用getstatic指令从方法区静态域中获取值

实例变量:

public class Test {  

    private int m = 2; 

    public int add(int n) {  

        return m + n;  

    }  

}

对象初始化阶段:

创建对象时,会为每个对象在堆中分配该对象的内存空间,并把所有实例变量设置为零值。

调用对象的<init>方法时,为有初始值的实例变量赋值,用到了iconst_2和putfield #2两个指令。

 

 

 

猜你喜欢

转载自tiro-li.iteye.com/blog/2375258