JVM高级特性与实践(六):Class类文件的结构(访问标志,索引、字段表、方法表、属性表集合)

来源:互联网 发布:js中syntax error 编辑:程序博客网 时间:2024/05/29 10:53

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编译发展的一大步。

上篇博文中学习讲解了Class类文件结构的有关知识点,关于数据项方面介绍了常量池,此篇文章将介绍完余下的数据项部分,大致知识点如下:

  • 访问标志
  • 类索引、父类索引与接口索引集合
  • 字段表集合
  • 属性表集合
  • 各数据项的定义、作用、结构组成及实例讲解

JVM高级特性与实践(一):Java内存区域 与 内存溢出异常
JVM高级特性与实践(二):对象存活判定算法(引用) 与 回收
JVM高级特性与实践(三):垃圾收集算法 与 垃圾收集器实现
JVM高级特性与实践(四):内存分配 与 回收策略
JVM高级特性与实践(五):实例探究Class类文件 及 常量池
JVM高级特性与实践(七):九大类字节码指令集(实例探究 )
JVM高级特性与实践(八):虚拟机的类加载机制
JVM高级特性与实践(九):类加载器 与 双亲委派模式(自定义类加载器源码探究ClassLoader)
JVM高级特性与实践(十):虚拟机字节码执行引擎(栈帧结构)
JVM高级特性与实践(十一):方法调用 与 字节码解释执行引擎(实例解析)
JVM高级特性与实践(十二):Java内存模型 与 高效并发时的内外存交互(volatile变量规则)
JVM高级特性与实践(十三):线程实现 与 Java线程调度
JVM高级特性与实践(十四):线程安全 与 锁优化


一. Class类文件中的部分结构

0. 测试的实例

(1)示例源码

以下简单代码作为后续讲解的事例,代码如下:

package org.fenixsoft.clazz;public class TestClass {    private int m;    public int inc() {        return m + 1;    }}

(2)Class文件

下图显示的是使用十六进制编辑器WinHex打开Class文件结果:

这里写图片描述

(3)TestClass.class文件字节码

以下是TestClass.class文件字节码内容(省略了常量池以外的信息):

d:\>javap -verbose Test  Compiled from "Test.java"  public class com.test.Test extends java.lang.Object    SourceFile: "Test.java"    minor version: 0    major version: 49    Constant pool:  const #1 = class        #2;     //  com/test/Test   const #2 = Asciz        com/test/Test;  const #3 = class        #4;     //  java/lang/Object   const #4 = Asciz        java/lang/Object;  const #5 = Asciz        m;  const #6 = Asciz        I;  const #7 = Asciz        <init>;  const #8 = Asciz        ()V;  const #9 = Asciz        Code;  const #10 = Method      #3.#11; //  java/lang/Object."<init>":()V  const #11 = NameAndType #7:#8;//  "<init>":()V  const #12 = Asciz       LineNumberTable;  const #13 = Asciz       LocalVariableTable;  const #14 = Asciz       this;  const #15 = Asciz       Lcom/test/Test;;  const #16 = Asciz       getM;  const #17 = Asciz       ()I;  const #18 = Field       #1.#19; //  com/test/Test.m:I  const #19 = NameAndType #5:#6;//  m:I  const #20 = Asciz       SourceFile;  const #21 = Asciz       Test.java;  


1. 访问标志

(1)作用

常量池之后的数据结构是访问标志(access_flags),这个标志主要用于识别一些类或者接口层次的访问信息,主要包括:

  • 这个Class是类还是接口;
  • 是否定义为public类型;
  • 是否定义abstract类型;
  • 如果是类的话是否被声明为final;

以上只是举的几个例子,具体的标志位以及标志的含义如下:

(2)组成

这里写图片描述

如上所示,访问标志中一共有16个标志位可以使用,当前只制定了8个。

(3)例子实践

以开头的简单代码为例,TestClass是一个普通Java类,不是接口、枚举或注解,被public关键字修饰但没有被声明为final 和 abstract,因此它的 ACC_PUBLIC、ACC_SUPER标志应为真,其余的标志位为假,它的 access_flags 值应为:0x0001|0x0020 = 0x0021,从下图可看验证推理正确:

这里写图片描述



2. 类索引、父类索引与接口索引集合

(1)定义与作用

类索引(this_class)父类索引(super_class)都是一个 u2 类型的数据,而接口索引集合(interfaces)一组 u2类型的数据集合,Class文件中由这三项数据来确定类的继承关系。

  • 类索引:用来确定这个类的全限定名;
  • 父类索引:用来确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的Java类都有父类。因此除了java.lang.Object 外,所有Java类的父类索引都不为0。
  • 接口索引:用来描述这个类实现了哪些接口。这些被实现的接口将按 implement 语句(若此类本身是一个接口,则应当是extend语句)后的接口顺序从左到右排列在接口索引集合中。

(2)类索引查找

类索引、父类索引、接口索引都按照顺序排列在访问标志之后,类索引和父类索引用两个 u2 类型的索引值表示,它们各自指向一个类型为 CONSTANT_Class_info 的类描述符常量,而在上一篇博文中讲过,通过CONSTANT_Class_info 类型常量中的索引值可以找到定义在 CONSTANT_Utf8_info类型常量中的全限定名字符串。下图所示为类索引查找过程:

这里写图片描述

(3)接口计数器

对于接口索引集合,入口的第一项——u2 类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器为0,后面接口的索引表不再占用任何字节。示例代码中的类索引、父类索引、接口索引的内容如下图:

这里写图片描述

查看上图可知,从偏移地址 0x000000F1 开始的3个 u2 类型的值分别为 0x0001、0x0003、0x0000,代表着类索引为1、父类索引为3、接口索引集合为0。


3. 字段表集合

(1)定义与作用

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量和实例级变量,但是不包括方法内部声明的局部变量。

(2)字段表结构

可以想一想在Java中描述一个字段可以包含什么信息?可以包括的信息有:

  • 字段的作用域(public、private、protected修饰符)
  • 是实例变量还是类变量(static修饰符)
  • 可变性(final)
  • 并发可见性(volatile修饰符,是否强制从主内存读写)
  • 是否被序列化(transient修饰符)
  • 字段数据类型(基本类型、对象、数组)
  • 字段名称

在上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、被定义成什么数据类型都是无法固定的,只能引用常量池中的常量来描述。下表列出了字段表的最终格式:

这里写图片描述

  • access_flags:是一个 u2的数据类型。
  • name_index 索引值: 对常量池的引用,代表着字段的简单名称。
  • descriptor_index 索引值: 对常量池的引用,代表字段和方法的描述符。

(3)字段访问标志 access_flags

字段修饰符放在 access_flags 项目中,它与类中的 access_flags 项目是非常类似的,都是一个 u2的数据类型,其中可以设置的标志位和含义如下:

这里写图片描述

由于Java本身的语言规则决定,在实际情况中有诸如此类的规则,以下举例部分现象:

  • ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志中只能选其一;
  • ACC_FINAL、ACC_VOLATILE不能同时选择;
  • 接口之中的字段必须有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志。

(4)“简单名称”、“描述符”、“全限定名”解释

全限定名:以示例中的代码为例,“org/fenixsoft/clazz/TestClass”是这个类的全限定名,仅仅是把类全名的“.”替换成了“/”,为了使连续的多个全限定名之间不产生混淆,最后一班加个“;”代表全限定名结束。

简单名称:指没有类型和参数修饰的方法或字段名称,这个类中的inc() 方法和 m字段的简单名称是“inc”和“m”。

描述符

作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述规则,基本数据类型(byte、char、float、double、short、int、long、boolean)以及代表无返回值的void类型都用一个大写字符表示,而对象类型则用字符L加对象的全限定名来表示,如下图:

这里写图片描述

对于数组类型,每一维度将使用一个前置的“[”字符来描述,例如“int[]”将被记录为“[I”。

用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”内。举个例子:方法void inc() 的描述符为“()V”。

(5)实例讲解

这里写图片描述

结合上图与代码TestClass.class 文件而言,字段表集合从地址 0x000000F8开始:

  • 第一个u2类型的数据为容量计数器fields_count,其值为0x0001,说明这个类只有一个字段表数据。
  • 接下来紧跟的是 access_flags标志,其值为0x0002,代表private修饰符的ACC_PRIVATE标志位为真(ACC_PRIVATE标志的值为0x0002) 。
  • 代表字段名称name_index值为0x0005,从上篇博文中的常量表可查出第5项常量是一个 CONSTANT_Utf8_info类型的字符串,其值为“m”。
  • 代表字段描述符descriptor_index 的值为0x0006,指向常量池的字符串“I”。

根据以上信息,可以推断出原代码定义为“private int m;”。



4. 方法表集合

(1)结构

Class文件存储格式中对方法表的描述与字段表是一致的,包括了:

  • 访问标志(access_flags)
  • 名称索引(name_index)
  • 描述符索引(descriptor_index)
  • 属性表集合(attributes)

这些数据项目的含义也非常类似,仅在访问标志和属性表集合的可选项中有区别:

这里写图片描述

(2)方法表与字段表的区别

区别在于访问标志的不同:在方法中不能了用volatiletransient关键字修饰,所以方法表中无ACC_VOLATILE、ACC_TRANSIENT。与之相对的 synchronizednativestrictfpabstract关键字可修饰方法,所以在方法表中就增加了相应的访问标志。

(3)标志位

对于方法表,所有的标志位及取值如下表:

这里写图片描述

行文自此,你会发现方法的定义可以通过访问标志、名称索引、描述符索引表达清楚,但是方法里的代码去哪里了?

方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合一个名为“Code”属性里,属性表作为Class文件格式中最具扩展性的一种数据项目,在后续会详细讲解。

(4)实例讲解

这里写图片描述

还是以示例代码为例,方法表集合的入口地址为 0x00000101:

  • 第一个u2 类型的数据(即计数器容量)的值为0x0002,代表集合中有两个方法(这两个方法是编译器添加的实例构造器和源码中的方法inc())。
  • 第一个方法的访问标识值是0x001,也就是只有ACC_PUBLIC 标志为真。
  • 名称索引为0x0007,查找常量池表可知对应的方法名是“”。
  • 描述符索引值为 0x0008,对应常量为“()V”。
  • 属性表计数器 attributes_count的值为0x0001,表示此方法的属性表集合有一项属性。
  • 属性名称索引为 0x0009,对应常量为“Code”,说明此属性是方法的字节码描述。



二. 属性表集合

由于这一部分内容较多,所以另起一点进行讲解。

0. 属性表

属性表(attribute_info)在Class文件、字段表、方法表都可以携带自己的属性集合,用于描述某些场景专有的信息。

与Class文件中的其它数据项木要求严格的顺序、长度和内存不同,属性表集合限制稍宽松,不再要求各个属性表具有严格顺序,只要不与已有属性名重复,任何人实现的二便一起都可以想属性表中写入自己定义的属性信息,而Java虚拟机会忽略掉它不认识的属性。

下表对其中的一些属性中关键常用部分进行讲解:

这里写图片描述

对于每个属性,它的名称需要从常量池中引入一个 CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4 长度属性去说明属性值所占用的位置即可。一个符合规则的属性表应满足如下结构:

这里写图片描述


1. Code属性

Java程序方法体中的代码经过javac 编译后,最终变为字节码指令存储在Code属性内。

(1)结构

Code属性出现在方法表的属性集合中,但并非所有的方法表都必须存在这个属性,例如接口或抽象类中的方法就不存在。如果方法表有Code属性存在,它的结构将如下:

这里写图片描述

(2)结构分析

  • attribute_name_index:是一项指向CONSTANT_Utf8_info 型常量的索引,常量固定值为 “Code”,它代表了该属性的属性名称;

  • attribute_length:代表属性长度;

  • max_stack:代表操作数栈(Operand Stacks)深度的最大值;

  • max_locals:代表了局部变量表所需要的存储空间;Slot是虚拟机为局部变量分配内容所使用的最小单位,对于byte, char, flaot, int, short, boolean and returnAddress 等长度不超过32位的数据类型,每个局部变量占用1个Slot;而double 和 long 这两种64位的数据类型则需要 两个Slot来存放;

  • code_length 和 code:用来存储java 源代码编译后生成的字节码指令。code_length代表字节码长度,code用于存储 字节码指令的一系列字节流;关于code_length,需要注意的是,虽然它是一个u4类型的长度值,但虚拟机中明确限制了一个方法不允许超过65535条字节码指令,即它实际上只使用了u2的长度,如果超过这个限制, javac编译器会拒绝编译;

  • Code属性:是Class文件中最重要的一个属性,如果把一个java程序中的信息分为代码(Code,方法体里面的java代码)和元数据(metadata,包括类,字段,方法定义以及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据;

(3)实例解析

这里写图片描述

还是以原代码为实例,可见上图,这是上一节分析过的实例构造器“”方法的Code属性。它的操作数栈的最大深度和本地变量表的容量都为0x0001,字节码区域所占空间的长度为0x0005。虚拟机读取到字节码区域的长度后,按照顺序依次读入紧随的5个字节,并根据字节码指令表翻译出所对应的字节码指令。翻译“2A B7 00 0A 01”的过程:

  • 1). 读入2A,查表得0x2A对应的指令为 aload_0,这个指令的含义是将第0个Slot中为 reference类型的本地变量推送到操作数栈顶。
  • 2). 读入B7,查表得0xB7对应的指令为 invokespecial,这条指令的作用是以栈顶的 reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。
  • 3). 读入000A,这是invokespecial 的参数,查常量池得0x000A 对应的常量为实例构造器“”方法的符号引用。
  • 4). 读入B1,查得0xB1对应的指令为return,含义是返回此方法,并且返回值为 void。这条指令执行后,当前方法结束。

2. Exceptions 属性

(1)作用
Exception属性是在方法表中与Code属性平级的一项属性,切勿与Code属性中的异常混淆。Exception属性的作用是列举出方法中可能抛出的受查异常(Checked Exception),也就是方法描述时在 throws 关键字后面列举的异常。

(2)结构

Exceptions 属性的结构如下表所示:

这里写图片描述

(3)结构解析

number_of_exceptions:表示方法可能抛出 number_of_exceptions 种异常,每一种异常使用一个 exceptoin_index_table 项表示, exceptoin_index_table是一个指向常量池中 CONSTANT_Class_info 型常量的索引,代表了该异常类型。


3. LineNumberTable 属性

(1)作用

LineNumberTable 属性用于描述java 源码行号与字节码行号的对应关系。它并不是运行时必须的属性,但默认生成到Class文件中,可以在javac中通过 -g:none 或 -g:lines 来取消或要求生成这个信息。如果选择不生成,那么当抛出异常时,堆栈中将不会显示错误的行号,且在调试程序的时候,也无法按照源码行来设置断点;

(2)结构

LineNumberTable 属性的结构如下表所示:

这里写图片描述

(3)结构解析

line_number_table 是一个数量为 line_number_table_length,类型为 line_number_info 的集合,line_number_info 表包括了 start_pc 和 line_number 两个u2 类型的数据项,前者是字节码行号,后者是 java 源码行号。


4. LocalVariableTable(局部参数表) 属性

(1)作用

LocalVariableTable 属性用于描述栈帧中局部变量表中的变量与 java 源码中定义的变量之间的关系。它并不是运行时必须的属性,但默认生成到Class文件中,可以在javac中通过使用 -g:none 或 -g:vars 来取消或要求生成这项信息;如果没有生成这项消息,最大的影响是当其他人引用这个方法时,所有的参数名称都将会丢失。

(2)结构

LocalVariableTable 属性的结构如下表所示:

这里写图片描述

其中local_variable_info项目代表了一个栈帧与源码中的局部变量的关联,结构见下表:

这里写图片描述

(3)结构解析

  • start_pc 和 length 属性分别代表了 这个局部变量的生命周期开始的字节码偏移量及其作用范围长度,两者结合起来就是这个局部变量在字节码中的作用域范围;

  • name_index 和 desc_index: 都是指向常量池中 CONSTANT_Utf8_info 型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符;

  • index:是这个局部变量在栈帧局部变量表中Slot的位置。当这个变量是 64 位类型时,它占用的Slot 为 index and index + 1;

  • LocalVariableTable属性:它增加了一个姐妹属性——LocalVariableTypeTable,这个新增的属性结构与LocalVariableTable 非常相似,仅仅是把记录的字段描述符的desc_index 替换为字段的特征签名,对于非泛型类型来说, 描述符和特征签名能描述的信息是基本一致的,但是泛型引入后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能正确地描述泛型类型了,所以就引入了LocalVariableTypeTable了;


5. SourceFile(记录源文件名称) 属性

(1)作用

SourceFile 属性用于记录生成这个Class文件的源文件名称。这个属性是可选的,默认生成,使用 javac的-g:none 或 -g:source 来关闭或生成它;如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名;

(2)结构

SourceFile 属性的结构如下表所示:

这里写图片描述

(3)结构解析

sourcefile_index:是指向常量池中CONSTANT_Utf8_info 型常量的索引,常量值是 源码文件的文件名。


6. ConstantValue 属性

(1)作用

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。

  • 只有被static关键字修饰的变量才可以使用这项属性;
  • 对于非static类型的变量(实例变量),它的赋值是在实例构造器方法中进行的;
    • 在类构造器方法中;
    • 使用 ConstantValue属性;

(2) Sun javac 编译器的选择

目前 Sun javac 编译器的选择是:如果同时使用final 和 static 关键字来修饰一个变量,且其数据类型是基本类型或 String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非是基本数据类型或String类型,则将会选择在 方法中进行初始化;

(3)ConstantValue属性的定义

jvm 规范中并没有强制要求字段必须为final,只是要求有ConstantVluae属性的字段必须设置为static而已,对final关键字的要求是 javac编译器自己加入的限制;而且对ConstantValue属性值只能限于基本类型和String,因为Class文件格式的常量类型中只有与基本属性和字符串相对应的字面量,所以ConstantValue属性不可能支持别的类型。

(4)结构

ConstantValue 属性的结构如下表所示:

这里写图片描述

(5)结构解析

  • ConstantValue属性:是一个定长属性,他的attribute_length数据项 必须为2;
  • constantvalue_index数据项:代表了常量池中一个字面量常量的引用,根据字段类型的不同 ,字面量可以是 CONSTANT_Long_info, CONSTANT_Float_info,CONSTANT_Double_info,CONSTANT_Integer_info, CONSTANT_String_info,常量中的一种;

7. InnerClass属性

(1)作用

InnerClass属性用于记录内部类和宿主类间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClass属性。

(2)结构

InnerClass属性的结构如下表所示:

这里写图片描述

这里写图片描述

(3)结构分析

数据项 number_of_classes: 代表需要记录多少个内部类信息,每个内部类的信息都由一个 inner_classes_info 表进行描述。 其结构如下:

这里写图片描述

  • inner_class_info_index 和 outer_class_info_index :都是指向常量池中 CONSTANT_Class_info 型常量的索引,分别代表了内部类和宿主类的符号引用;
  • inner_name_index:是指向常量池中 CONSTNAT_Utf8_info 型常量的索引,代表这个内部类的名称,如果是匿名内部类,那么这项值为0;
  • inner_class_access_flags:是内部类的访问标志,它的取值范围如下所示:

这里写图片描述


8. Deprecated和Synthetic属性(属于标志类型的布尔属性)

(1)定义

Deprecated和Synthetic属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。

  • Deprecated属性:用于表示某个类,字段或方法,已经被程序作用定为不在使用;
  • Synthetic属性:代表这个字段或方法并不是由java 源码直接产生的,而是由编译器自行添加的;

在jdk1.5后, 标识一个类,字段或方法是编译器自动产生的,也可以设置它们访问标志中的ACC_SYNTHETIC 标志位。所有由非用户代码产生的类,方法以及字段都应该至少设置Synthetic属性和 ACC_SYNTHETIC标志位中的一项,唯一的例外就是 实例构造器 方法和类构造器 方法;

(2)结构

Deprecated和Synthetic属性的结构非常简单,如下表:

这里写图片描述

(3)结构分析

其中 attribute_length 数据项的值必须为0x00000000,因为没有任何属性值需要设置。


9. StackMapTable 属性

(1)作用

StackMapTable 属性位于Code属性的属性表中, 该属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用, 目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

(2)栈映射帧

StackMapTable属性包含零个至多个栈映射帧(Stack Map Frames),每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。

(3)结构

Deprecated和Synthetic属性的结构非常简单,如下表:

这里写图片描述


10. Signature 属性

(1)作用

Signature 属性可以出现在类,属性表和方法表结构的属性表中。在jdk1.5 大幅增强了 java 的语法后, 任何类,接口,初始化方法或成员的泛型 签名如果包含了类型变量或参数化类型,则 Signature 属性会为它记录泛型签名信息。

(2)引入原因

因为java 的泛型采用的是擦除方法实现的伪泛型,在字节码(Code属性)中, 泛型信息编译之后都通通被擦除掉。擦除后的坏处就是: 运行期无法将泛型类型与用于定义的普通类型同等对待,例如运行期无法做反射时无法获得泛型信息。

Signature属性就是为了弥补这个缺陷而增设的,现在 java的反射API能够获得泛型类型,最终的数据来源就是这个属性。

(3)结构

Signature 的结构如下表:

这里写图片描述

(4)结构分析

signature_index 项的值必须是一个对常量池的有效索引常量池在该索引处的项必须是 CONSTANT_Utf8_info结构,表示类签名、方法类型签名或字段类型签名。


11. BootstrapMethods 属性

(1)作用

BootstrapMethods 属性用于保存 invokedynamic 指令引用的引导方法限定符。

(2)结构

BootstrapMethods 的结构如下表:

这里写图片描述

(3)结构分析

上图结构中引用到的 bootstrap_method结构见下表:

这里写图片描述

BootstrapMethods 属性中:

  • num_bootstrap_methods项的值给出了 bootstrap_methods[] 数组中的引导方法限定符的数量。
  • bootstrap_methods[]数组中的每个成员包含了一个指向常量池 CONSTANT_MethodHandle 结构的索引值,它代表了一个引导方法,还包含了这个引导方法静态参数的序列,数组中每个成员必须包含以下3项内容:
    • bootstrap_method_ref :它的值必须是一个对常量池的有效索引。
    • num_bootstrap_arguments:它的值给出了bootstrap_methods[]数组的成员数量。
    • bootstrap_arguments:数组的每个成员必须是一个对常量池的有效索引。



三. 总结

通过此篇博文与上一篇博文完成了有关Class文件存储格式的具体细节及描述的详细信息,各个不同数据项的区别于使用。解析Class文件的数据结构是这两篇文章的重点,对数据结构方面的学习难免会有些枯燥,而且底层知识学习的难度也有所增加,可能学习起来不如最初几篇博文轻易,但是还是要重复声明,这部分是了解虚拟机的重要基础之一,不可避免。


若有错误,欢迎指教 ~

阅读全文
0 0
原创粉丝点击