当前位置: 首页 > news >正文

JVM Class类文件结构

国庆节快乐            2024年10月2日17:49:22 



目录

前言

magic 数

文件版本

使用JClassLib观察class文件

一般信息

接口        

常量池

字段

方法 

常量池计数器

常量池

类型

 CONSTANT_Methodref_info

CONSTANT_Class_info

类型结构总表

访问标志 

类索引, 父类索引与接口索引

字段表

方法表

属性表 


作者@NICEFF_KING, 文中描述有错误的地方请在评论区不吝教诲

前言

        对于Java的各种版本来说, 一般都遵从这个特点, 高版本的JDK是可以编译低版本的java源代码的, 因此在CLass文件各项细节, 几乎没有随着版本有太大的改变, 甚至没有做出任何修改, 后续的jdk版本来说, 更新只是在原有的结构上进行扩充, 熙增新增.

        前面我们讲解, 一个java源代码文件, 可以被编译器编译为JVM可以识别的字节码文件, 这个字节码文件是一个二进制文件(二进制流), 各个数据项是严格按照某种特定的顺序排列在文件之后, 中间没有加任何间隔符.

 Class文件是以8个字节为基础的二进制文件, 其文件格式采用类似于C语言结构体的类似的数据结构来存储数据, Class文件中存在两种数据类型

  • 无符号数

        无符号数对应的是基本数据类型, 无符号数通常使用u1, u2, u4, u8(u后面的数字代表的是n个字节, 例如u1就是一个字节的无符号数, u既是unsigned) 来表示无符号数. 

        表则由多个无符号数, 或者其他表作为其构成的复合型数据结构, 为了便于区分表和无符号数, 所有的表的命名都习惯性的加上 _info后缀.  整个Class文件就可以看做是一个表.

        下图是Class文件的格式: 

        下面我们来看看各个数据项的特殊含义, 在开始之前, 你应该写上一个类, 然后将其编译, 然后使用16进制编辑器查看class文件, 如下是一段代码, 以及它编译后的字节码文件: 

public class Test {private int x;public static void main(String[] args) {}public int inc() {return x + 1;}
}

(使用IDEA插件查看class文件)

下载插件: 

对编译好的class文件右击, 然后选择打开于:  

选择刚才下载的插件:

        字节码:

  • 二进制格式 

  • 16进制格式:

magic 数

        首先对于上图的16进制的文件, 不难看出开头的四个字节(2^4可以描述一位16进制的数, 因此两位16进制数需要两个2^4的二进制, 一个字节是8个bit位, 2^4为四个bit位因此2个16进制的数需要1个字节)被标记为天蓝色. 

使用HXD软件查看的字节码文件

        其内容为:"CAFEBABE", 这个也不难理解, 其实你可能已经猜出来了这个是干嘛用的, 它的唯一作用就是检查这个文件是否是一个可以被JVM识别接受的Class文件, 参考其他的文件格式, 有很多也是使用模式来判断身份标识, 例如GIF等文件格式.  不使用扩展名的原因是扩展名可以随意修改, 为了安全起见, 没有采用扩展名的方式. 

        你能找出开头不为:cafebabe的class文件吗.



文件版本

         紧接着magic的四个字节是次版本号(minor_version)和主版本号(major_version), 先来看看这四个字节的16进制数是多少: 

        其中地址偏移量(offset) 为0x04 ~ 0x05的是次版本号, 0x06~0x07的是主版本号, 可以看到次版本号为0, 主版本号为0x0034, 对应的10进制就是52, 这个52到底是什么意思, 为什么次版本号为0呢? 

        首先需要知道的是java的版本号是从45开始的, JDK1.1之后每一个大版本发布都将主版本号+1, 高版本的JDK可以兼容低版本的Class文件, 但是不能兼容高于当前JDK版本的JDK编译生成的Class文件, 即使文件格式没有发生任何改变, 如果版本号高于当前的JDK版本, 虚拟机会需要拒绝此文件(来自java虚拟机规范)

        例如JDK1.1支持的版本号是45.0~45.65535, 但是无法执行版本号为4.6以及以上的Class文件. (上图中我自己的版本号为52, 刚好对应的是JDK1.8版本). 

        下图是对应版本号对应的JDK版本: 

        关于次版本号为0, 存在一些历史因素, 其中在JDK1.1支持的版本是45.0~45.65535, 但是从1.2之后次版本号均没有被使用, 都是为0, 到了JDK12以及后期, JDK本身集成了非常多的功能, 其中可能不乏一些不稳定的新特性, 这些新特性在还没有全面的测试使用和分析数据的情况下, 是不能直接进入商业应用阶段的, 因此为了区分, 如果CLass文件中使用了该JDK版本未正式列入特性清单中的功能, 那么就会将次版本号的值改为: 65535(二进制全1), 因此你可能会在偏移量为0x04和0x05这两个偏移(相对) 看见 FFFF(HEX)的值.



        为了更好的理解常量池是怎么设计的, 我们先使用一些工具来查看字节码的结构

使用JClassLib观察class文件

        首先我们需要下载JclassLib: 

GitHub - ingokegel/jclasslibjclasslib bytecode editor is a tool that visualizes all aspects of compiled Java class files and the contained bytecode. - ingokegel/jclasslibicon-default.png?t=O83Ahttps://github.com/ingokegel/jclasslib        jclasslib(现在简称jcl) 字节码编辑器是一个可以分析java的Class文件结构的工具, 它将Class文件解析成我们可以看懂的结构特征. 目前已经支持到了JDK21版本. 

        下载之后, 进行安装, 然后我们编写如下代码: 

public class Test implements Runnable{private int x;private static final String a = "abc";public int inc() {return x + 1;}@Overridepublic void run() {}
}

        我们使用javac来编译当前的代码: 

javac Test.java

         当前编译的JDK: 

         然后将编译成的Test.class使用jclassLib打开: 

         可以看到jclassLib解析出了6个模块分别是: 

  • 一般信息
  • 常量池
  • 接口
  • 字段
  • 方法
  • 属性

一般信息

         一般信息是类的统计信息, 例如当前javaclass字节码文件中的主次版本号, 常量池中常量项的个数, 访问标志(是否是public), 父类的全限定类名, Test类的名称的常量, 还有当前这个类的实现的接口的数量, 字段数量, 自身的属性等. 

        首先主次版本号中, 次版本号为0 , 说明当前的JDK版本中没有集成哪些不稳定的需要公测的特性. 主版本号为52, 正好对应着JDK1.8版本, 然后常量池中常量计数器的数值(常量项的个数)

        访问标志是两个字节存储的, 也就是u2, 例如0x000021, 它的二进制表示为: 

00000000 00000000 00000000 00100001, 其中第0位表示公共性,其他位可以指示是否是抽象等等. 后面细说.

        然后就是文本索引, 其它的值为cp info #3, 不难猜出其实cp就是类似于linux中的cp指令, 意思就是它引用了#3的常量项(常量池中的), 我们在jclasslib'中点击它, 可以看见它跳转到了如下页面: 

         可以看到它来自这个名为[03]CONSTANT_Class_info的常量项, 并且这个常量项引用了另外一个#24的常量项, 我们继续点击这个#24, 跳转如下: 

        我们注意到这个页面有两个常量池项的类型: 

  • CONSTANT_String_info : 用于表示一个字符串常量
  • CONSTANT_Utf8_info 

        CONSTANT_String_info更接近于Java语法中数据类型的东西, 它包含一个指向CONSTANT_Utf8_info 常量类型的引用, 可以用作字符串的符号引用,在编译时用于表示字符串常量, 而CONSTANT_Utf8_info 专门用于存储UTF-8编码的字符串数据, 也就是真是存储数值的常量项, 这是一个通用的条目类型,可以被其他常量类型引用,例如CONSTANT_Utf8_info 和CONSTANT_String_info, CONSTANT_Utf8_info本身不提供上下文,只是存储字符串的原始内容

        总结来说, CONSTANT_String_info是一个引用, 用于在常量池中指定一个具体的常量, CONSTANT_Utf8_info则是世界存储该字符串的内容的条目

        这里提一嘴, CONSTANT_Class_info直接引用CONSTANT_Utf8_info, 而没有通过CONSTANT_String_info简介去引用CONSTANT_Utf8_info常量的原因是, 类名信息室不需要区分数据类型的, 它只需要表示可读的文本即可, 但是例如你在内存中定义了一个常量String a = "abc", 此时你引用这个a, 就需要引用一个CONSTANT_String_info来表示它是一个String类型的常量, 然后再引用其真实的值. 对于类信息, 例如类名, 它使用Class_info表名其类型, 然后再引用这个CONSTANT_Utf8_info类型的常量. 

        父类引用为#04号常量, 从图中可以简略看出来它是继承自Object.

        我们查看这个引用, 信息如下 : 

         可以看到这个#04的引用是一个Class_info类型的, 说明它描述的是类的信息(毕竟是继承, 继承是表达类与类之间关系的一种方式).  这个Class_info引用了另外一个类型(#27), 通过上面的讲述, 其实你也能猜到引用的是一个CONSTANT_Utf8_info类型的常量如下: 

         它的字面量是一个全限定类名: java/lang/Object. 

接口        

还有一个接口数, 可以看见接口数量为1 : 

        但是貌似没有任何引用. 虽然在一般信息里面不能直接看到, 但是可以从接口栏目查看, 如下: 

        可以看到它引用了 #5 这个常量, 如下: 

         可见, 接口信息也被归属到 类信息中, 并且引用了另外一个CONSTANT_Utf8_info的字面量. 

常量池

        从上面的分析不难看出, 常量池中主要存储的是CONSTANT_Utf8_info的类型的字面量, 还存放着CONSTANT_Class_info的类信息, 除此之外还有CONSTANT_String_info的类型的信息. 具体还有哪些就不一一讲解, 大致总结如下(随着jdk版本的变更或多或少为了支持新特性会增加一些其他类型的信息) : 

        从上图不难看出每种常量池的类型都有不同的标志位, 从1开始往后. 字节码指令中, 往往就是存在着一些对常量池中数据的引用, 如下: 

        这种通过编号引用到常量池中数据的过程成为符号引用. 

字段

如下: 

        我们可以看出来字段拥有着这几类的描述信息: 

  • 字段名 : 名称
  • 描述符 : 描述符号, 可以理解为类型
  • 访问标志: 权限, 是public 还是Private等

        我们首先看名字, 它(字段a)引用了第八项常量, 如下(字段的名称直接引用的是字面量) , 以此来说明这个字段的命名为"a": 

         其次它还引用了第九项常量, 如下: 

         可以看出来描述也是一个CONSTANT_Utf8_info的字面量, 它描述的是类型的信息.  除此之外它的访问标志为

         除此之外, 除了这些描述性的信息(字段名, 类型, 访问标志), 他还有自己的值, 我们双击这个常量a, 就可以返现它有一个CONSTANT的常量值: 

        具体信息如下: 

        其中特有信息引用的是第11项常量值, 如下: 

         它是一个String类型的CONSTANT引用, 因此表面此数据类型是String类型的字面量, 然后引用了第二十九项常量(Class_info类型), 其值为"abc".

         其次, 一般信息中它还引用了一个10号常量, 如下: 

        可以发现 , 里面的字面量为ConstantValue, 这个 ConstantValue有什么用呢? 我不是已经通过一个访问标志来查看它的类型了吗, 而且还有访问标志符号来判定它是否是一个静态常量, 为什么还需要一个一般信息的constantValue来描述它? 

        ConstantValue属性是专门用于存储静态字段(特别是static final字段)的常量值的, ConstantValue属性是专门用于存储静态字段(特别是static final字段)的常量值的. 这个属性会指向常量池中的一个CONSTANT_Utf8_info(对于字符串)或者CONSTANT_Integer_info(对于整数)等,从而指定静态字段的值. ConstantValue属性的存在是为了在类加载阶段能够直接获取到这些静态常量的值,而不需要在运行时通过方法调用等方式去计算或获取.

        在进行类加载的时候, 仅仅只是知道这些常量在常量池中的位置是不够的, 还需要知道哪些字段是静态常量, 并且在类加载的时候, 就加载上正确的值, 这个时候, 就需要看ConstantValue的脸色了, ConstantValue是类文件中字段表的一个可选的属性, 通过解析类文件中的字段表,查找具有ConstantValue属性的字段,并读取该属性所指向的常量值来识别和加载静态常量.

方法 

        需要解析的东西还不少 ... ... 

         回顾一下这个class文件的源代码: 

public class Test implements Runnable{private int x;private static final String a = "abc";public int inc() {return x + 1;}@Overridepublic void run() {}
}

         通过对比源码可知, 0号方法为init, 我们暂时猜测它是构造方法, inc为我们自己定义的方法, run是Runnable的实现方法, 我们首先来看自己定义的inc方法, 如下: 

        同样的拥有两样信息: 

  • 方法名称, 描述方法的名称, 引用了第19个常量, 该常量是一个utf8_info的数据, 值为"inc", 正式我们的方法名
  • 描述符 : 描述该方法的参数和返回值, 该例子中它引用了第20个常量, [20] 常量是一个utf8_info类型的数据, 是一个字面量. 

        双击这个inc就可以发现, 它里面的方法体如下: 

        我们首先看这个Code, 如下: 

         同样拥有一般信息和特有信息(包括方法体的字节码, 异常表,和杂项), 首先是一般信息中, 引用了[14] 号常量, 它是一个utf8_info类型的字面量, 值为Code, 然后字节码为: 

0 aload_0
1 getfield #2 <Test.x : I>
4 iconst_1
5 iadd
6 ireturn

对此字节码指令的说明: 

  • aload_0(操作码:26,局部变量索引:0)

        这条指令从局部变量表的第0个位置加载一个引用类型的值到操作数栈顶。在Java方法中,局部变量表的前几个位置通常用于存储方法的参数和this引用(对于非静态方法). 因此,aload_0通常用于加载当前对象的引用(即this)

  • getfield #2 <Test.x : I>(操作码:179,字段引用索引:2)

        这条指令从操作数栈顶取出一个对象引用(由aload_0加载的),并根据常量池中的索引2找到对应的字段描述符(这里是<Test.x : I>),然后从该对象中获取名为x的字段的值,并将该值(类型为int,由I表示)压入操作数栈顶。这里假设Test是一个类,x是Test类的一个int类型的实例字段。

  • iconst_1(操作码:8)

        这条指令将整型常量1压入操作数栈顶 

  • iadd(操作码:96)

        这条指令从操作数栈顶弹出两个整型值(第二个弹出的是操作数,第一个弹出的是被加数),将它们相加,然后将结果压回操作数栈顶. 在这里,它将getfield指令获取的x的值与iconst_1指令压入的1相加 

  • ireturn(操作码:172)

        这条指令从操作数栈顶弹出一个整型值,并将其作为方法的返回值。这意味着方法的执行将结束,并且该整型值将被返回给方法的调用者 

杂项: 

 至于说下面的这两项: 

        在Java虚拟机(JVM)中,LineNumberTable和LocalVariableTable都是类文件中方法属性表(attribute_info)的一部分,它们提供了关于方法的附加信息,这些信息对于调试和代码分析非常有用 .

    LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它不是运行时必需的属性,但默认会生成到Class文件中,可以使用javac编译器的选项来取消或要求生成这项信息。

  • 作用:它帮助开发者在调试时,将字节码指令映射回Java源码中的具体行号,从而更容易地定位和理解代码的执行流程。
  • 结构LineNumberTable属性包含一个表,表中每一项都包含一个源码行号和对应的字节码偏移量。

LocalVariableTable

        localVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。它同样不是运行时必需的属性,默认也不会生成到Class文件之中,但可以使用javac编译器的选项来要求生成这项信息。

  • 作用:它帮助开发者在调试时,了解局部变量在方法执行过程中的值和作用域,从而更容易地跟踪和理解代码的执行状态。
  • 结构LocalVariableTable属性包含一个表,表中每一项都包含变量的名称、类型、作用域起始和结束位置(以字节码偏移量表示)等信息。

其他的一些都是class文件的一些属性, 待读者自行研究.  

常量池计数器

        紧接着版本号的就是常量池(红框), 它是Class文件结构中与其他项目关联最多的数据, 通常也是一个CLass文件占用最多的部分, 但是由于常量池的长度不固定, 因此需要有一个标识来记录其常量池的容量的计数值: 

        名为constant_pool_count, 大小为2个字节, 是一个无符号数. 能表示的范围为0~65535.  我之前的截图文件中的constant_pool_count 的数值如下: 

        1A(HEX)即 26(DEC), 这就代表常量池中存在着25项常量. 但是与其他的结构不同的是, 这个值的计数是从1开始, 就例如你数数量, 从来都是从1开始数起. 

        这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示, 你暂时只需要记住这两个字节是表示常量池常量项个数的标识即可. 

常量池

类型

        通过我们上面的jclassLib观察的结果, 发现常量池中的很多种类型, 我们已经熟悉了: 

  • CONSTANT_Class_info , 类信息
  • CONSTANT_Utf8_info , 字面量
  • CONSTANT_String_info , 数据类型

        说完常量池计数器, 紧接着的就是常量池.  常量池中主要存放两大类: 

  • 字面量 : 比较接近java语言中的常量的概念(CONSTANT_Utf8_info)
  • 符号引用 , 主要包括下面几类常量: 
    • 被模块导出或者开放的包
    • 类和接口的全限定名 (CONSTANT_Class_info)
    • 字段的名称和描述符
    • 方法的名称和描述符
    • 方法句柄和方法类型
    • 动态调用点和动态常量

        具体符号引用是什么, 我们需要先理解一下, java的编译原理. 

        在java的Class文件中, 是不会存储文件中各个字段, 方法等最终在内存中的布局信息, 这些字段方法的符号引用不经过java虚拟机在运行期间的转换, 是无法真正得到内存中的入口地址的. 也就无法被虚拟机使用, 

        常量池中的每一项都是一个表, 每个表最初具有11种各不相同的表结构数据, 后来为了更好的支持动态语言调用, 额外增加了4中动态语言相关的常量 ... 

        后续增加到17种, 如下: 

        每一种类型都有自己的标志位来表示自己是哪种类型的, 为了表示它, 每一个类型的最前面都会有一个u1类型的标志, 来表示这个标志位. 

        为了演示, 我们编写如下代码: 

public class Test{private int m;public int inc() {return m + 1;}
}

        然后使用HxD查看其字节码文件如下: 

Test.class

 我们回顾一下之前的讲解: 

  • 00 ~ 03 是 java字节码文件的标识(魔数)
  • 04~05 是次版本号
  • 06 ~ 07 是主版本号 34(hex) 对应的10进制是52, 正好对应着JDK1.8
  • 08 ~ 09 对应着常量池中常量项的个数.  在上图的Test.class中对应的16进制是16, 转换成10进制就是22, 也就是说常量池中存在着22个常量项, 通过jclasslib查看如下: 
使用jclasslib观察Test.class

        虽然是22个, 但是这里只显示了21个, 也就是说索引的范围为1~21, 那还少了一个, 这是因为常量池的容器计数是从1开始的, 第0项有着特殊的含义, 也就是将其他常量项的值设置为索引0来表示它不索引任何一个值.

        我们回过头来看看这个常量池中的第一个常量的标志位:

 CONSTANT_Methodref_info

        可以看到第一个常量是(地址偏移为0x0000000A) 0x0A, 也就是标志位值为10, 对应的类型是CONSTANT_Methodref_info, 其实我们也可以在jclassLib中查看: 

         CONSTANT_Methodref_info 说明它是类中方法的引用, 它引用了类名的描述和方法名以及其描述符, 类名引用的是CONSTANT_Class_info类型的类名(此类名引用了CONSTANT_Utf8_info类型的字面量), 名字和描述符引用了CONSTANT_NameAndType_info类型的符号引用, 这个符号引用分别引用了名字的符号引用和符号的引用: 

        描述符描述的就是返回类型和参数, 是一个utf8字面量.  名字也是如此. 

        它的结构如下:

        除了tag(标志位), 下面两个字节是u2类型, 来表示这个方法的类描述符(CONSTANT_Class_info), 紧接着这个u2的也是一个u2类型, 指向名称以及类型描述符(CONSTANT_NameAndType_info)

         可以看到本例子中的第一个常量就是一个CONSTANT_Methodref_info 类型, 第一个index表示了[04]位的常量, 第二个index位0x12, 标识了[18]位常量, 如下: 

        在字节码层面上, 我们对上图的字节码文件的结构进行解析, 一直解析到第一个常量项:  如下

         可以看到, 第一个常量的类型的地址偏移从0x0000000A~0x0000000E, 也就是: 

  • 0A:   为一个u1类型, 表示常量类型的标志位, 这里是0A表示CONSTANT_Methodref_info
  • 0B ~ 0C: 为一个u2类型, 表示 指向声明此方法的类描述符的索引
  • 0D ~ 0E: 为一个u2类型, 表示方法名称和类型描述符

        然后下一个常量开始的地址偏移就是0x0000000F, 其值为9, 表示字段的引用符号: 

         然后以此类推, 就可以查出所有的常量池常量所在的位置

CONSTANT_Class_info

        这个其实就是类的信息, 也使用常量来存储.  它的存储结构如下: 

        第一个u1表示标志类型, 其值与上述的表对应(0x07), name_index 是常量池的索引值, 它使用2个字节来指向常量池中的CONTANT_Class_info类型的常量

        还有很多类型, 这里就不一一列举例子. 

类型结构总表



        避免遗忘回顾一下这张图: 

访问标志 

        可以看出来, 在常量池之后的内容就是一个u2字节的访问标志(access_flags), 用于标识一些类或者接口层次的访问信息, 包括这个Class是类还是接口 , 是否是public类型, 是否是abstract等, 如果是类的话, 又是否是被声明为final等等之类的, 具体的含义如下:

        注意是16进制 ... 

共有16个可以使用, 但只标识了9个

         访问标志位占用2个字节, 也就是16个bit位, 也就是可以使用16位来表示其中的各种含义, 现在不同的标志位对应的含义已经标明, 我们只需要找出访问标志位, 然后解析其值, 并与其上图进行分析即可.

        我们当前使用的代码: 

public class Test{private int m;public int inc() {return m + 1;}
}

        对应的class字节码文件(16进制) 

        由于我们再常量池那一章节中使用的是一个普通的java类, 不是接口, 注解, 也不是枚举, 或者模块, 被public修饰但是没有被final修饰, 并且使用JDK1.8标识, java虚拟机规范要求, 如果没有被使用到的一律需要置为0 

        从上述的代码中可以看出Test类是一个普通的java类, 因此 它应该将public标志位置为真: 

         但是我们发现0x0020也被激活, 这是为什么? 从上图中可以看出, 0x0020(ACC_SUPER)是否允许使用invokespecial字节码指令的新语义,invokespecial指令的语义在JDK1.0.2发生过改变,为了区别这条指令使用哪种语义,JDK1.0.2之后编译出来的类的这个标志都必须为真

        其余项目都应该置为假, 因此就形成了0x0021的值. 

类索引, 父类索引与接口索引

         access_flags下面的四个字节就是this索引和super索引了, 他们分别占用两个字节, 也就是分别为u2类型的数据, 紧接着access_flags的是this_class, 也就是类索引. 除了this_class和super_class, 接下来还有两个两个字节的u2表示接口相关的信息

        他们四个来确定Class文件中类的集成关系, 接下来分别讲述其真正的意义: 

  • this_class: 用于确定类的全限定类名, 指向一个CONSTANT_Class_info的常量类型
  • super_class: 用于确定父类的全限定类名, 指向一个CONSTANT_Class_info的常量类型

        我们还是拿这个代码为案例: 

public class Test{private int m;public int inc() {return m + 1;}
}

         其字节码文件中的类继承相关的字节码偏移量如下: 

         java语言不支持多继承, 但是支持多接口实现, 因此父类索引只能存在一个, 除了Object之外, 所有的Java类都有父类, 因此除了Object其他所有的类的父索引都不为0, 接口索引数据描述实现了哪些接口, 并且这些接口将按照implements的顺序从左到右以此排列在字节码文件中. 

        我们分析其值, 可知: 

  • this_class : 0x0003

         可以看到它引用了一个Utf8_info的字面量, 值为Test

  • super_class: 0x0004

        可以看到 其引用了一个Utf8_info的字面量, 值为java/lang/Object, 也就是Object类的全限定类名. 

  • interfaces_count: 这个是接口的入口, 是一个计数器, 用来表示接口的数量, 你可以将其类比常量池中常量的排列前面总是要加一个constant_pool_count. 

        其值为0, 说明没有接口实现,  这里有个细节, 因为接口计数器值为0 那么它下面的表示接口细节的u2类型的接口索引表也就不会占用任何字节. 




字段表

        首先字段表用于描述接口或者中声明的变量, Java语言中的字段包括了类变量和实例变量, 不包括方法内部局部变量. 

        既然是java的字段, 就应该有以下的描述

  • 访问限定修饰符(public, private 等)
  • 是否是类变量(static)
  • 可变性(final)
  • 可见性(volatile, 强制主内存读写)
  • 字段类型
  • 字段名

        从上图中可以看出, 字段表是紧接着interfaces接口索引集合后面的 (请注意interfaces_count为0的时候, interfaces是不占用任何字节的).  使用了n个字节来表示, 首先需要一个u2类型来描述字段的个数,  然后这两个字节后面就是真正的字段的相关信息

        我们使用如下代码: 

public class Test{private int m;public int inc() {return m + 1;}
}

        字节码还是如下, 并且我已经标出来了字段相关信息的部分: 

        从图中可以看出来fields_count的值为1表示有一个字段, 并且这个字段刚好就应该是我们上面定义的m字段. 

        这个字段后面就是具体的字段表(这个依然可以跟常量池比较, 他们前面都有一个记录数量的标识), 然后每个字段里面的符号引用或者字面量都是大小都是固定的(除了info), 如下: 

字段表

         第一个access_flags是不是很眼熟, 这个跟类的访问标识符很像, 但是他们的值有不同的含义, 如下: 

字段表的access_flags

        显然我们再编写代码的时候, ACC_PUBLIC, PRIVATE, PROTECTED这三个是不能同时选择的, 只能选择一个,  同时VOLATILE和FINAL之间也只能选择一个, 我们根据上述的字段表, 可以看到, access_flags后面的两个字节是name_index, 见名知意, 其实就是名称的索引, descriptor_index就是一种类似于描述符的

        他们都是对常量池项目的引用, 我们可以查看jclassLib中的演示: 

 该字段的值如下: 

  • name_index: 0x0005, 指向索引值为5的常量
  • descriptor_index: 0x0006, 指向常量值为6的常量

         索引值为5的常量为CONSTANT_Utf8_info, 内容为m: 

        索引值为6的同样为 CONSTANT_Utf8_info, 值为I (大写的i, 至于为什么为i, 后面会讲解), 表示该字段的类型为int类型. 

        我们在使用的时候, 导入包的时候, 都是使用"import com.mybatis.cn.*"之类的, 可以看出这里面都是使用的全限定的包名, 但是对于JVM来说, 类的全限定名仅仅只是把里面的"."号替换成为了"/" 而已, 例如我们的Object类 : 

        对于类名和方法名, 字段名这些不需要参数类型, 就只是人能看懂的符号即可, 也就是常见的CONSTANT_Utf8_info类型, 例如上述代码中的inc()方法, 它的方法名(inc)表示如下: 

         但是描述符可能就要复杂一点, 因为描述符需要描述方法和字段的返回值, 数据类型等等之类的, 就基本数据的类型而言(byte、char、double、float、int、long、short、boolean), 以及void类型, 都是用其数据类型命名的第一个字母的大写表示,

如下: 

描述符表示字符的含义

        这里的void只是为了统一让读者了解而列出来的, 在java虚拟机中为VoidDescriptor.  数组类型需要在前面加上"[", 例如int[] 就被描述为"[I". 

        因此下图中(类字段m) : 

        它的描述符为一个引用了Utf8_info类型的, 值为大写i(I)的引用, 此处的大写的i就是表示的基本数据类型int : 

public class Test{private int m;public int inc() {return m + 1;}
}

         对于方法inc的描述, 可以看到其描述符的值为: 

        描述方法的时候, 按照如下顺序来描述: 

  • 参数列表 , 严格按照编码的顺序放在(). 例如方法int func(int[] arrInt, char ch)的描述符就为([IC)I
  • 返回值, 紧跟在()后面, 例如方法char func(), 描述符为()C
字段表

         字段表的固定描述到了descriptor_index就算结束了, 后面的attributes_count和attributes用于存储一些额外的属性信息, 本例中, attributes_count为0, 也就是没有额外的信息存储. 

        那什么时候需要使用到这个, 如果你将m字段声明为static final, 那么可能就需要额外存储一段ConstantValue的属性


方法表

        紧接着字段表的就是方法表, 第一个跟表相关的仍然是count数值, 也就是methods_count, 表示方法的个数. 

        方法表和字段表几乎采用了一摸一样的描述方法. 方法表的结构如下: 

方法表结构

        在access_flags中, 因为方法的访问标志中, 不存在volatile等关键词, 因此在访问标志的描述上,, 存在些许差异: 

方法访问标志表

         唯一需要谈谈的就是, 方法里面的代码去哪了? 

java方法里面的code经过Javac编译器, 编译成字节码指令, 然后存放在方法属性表集合中一个名为code的属性之中去了 : 

         以如下代码为例子: 

public class Test{private int m;public int inc() {return m + 1;}
}

        字节码的方法表如下: 

        methods_count的值为2, 代表有两个方法, java基础好的肯定知道, 虽然只写了一个inc方法, 但是还有一个方法肯定是跟构造方法之类的有关, 没错他就是<init>方法(编译器添加的实例构造器), 第一个方法的访问标志位为0x0001,  对比{方法访问标志表}得知, 表示name_index为7: 

        可知它为init的方法名称. 

         描述符(0x0008) 不再赘述, 自行研究: 对应常量为 -- ()V.

        init方法的属性计数器为1, 因此表示此方法的属性表集合有1项属性,属性名称的索引值为0x0009: 

         查看常量池, 值为code, 说明此属性是描述的方法的字节码. 



属性表 

        属性表紧跟着方法表之后, 开头仍然是一个计数器, 其次才是表

        属性表用来存放什么信息? 

Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息, 我们之前所说的方法的code就是存储在此

         例如我们之前写的代码: 

public class Test{private int m;public int inc() {return m + 1;}
}

         使用JClassLib来观察其方法inc的方法表如下: 

         JDK12规范中的属性KV如下: 

 

         属性表的结构如下 : 

  • attribute_name_index自然是引用的一个CONSTANT_Utf8_info类型的字面量, 
  • attribute_length来说该属性所占用的位数
  • info来表示即为值 

        接下来我们来聊聊方法表中属性中存储的code属性. 

        经过编译器编译之后, 方法体被编译成字节码指令序列, 被存储在code属性中, 它的结构如下: 

         我们逐项解析: 

  • attribute_name_index自然就是引用的Utf8_Info(CONSTANT_Utf8_info), 表示属性名称的字面量. 此处为"Code"
  • attribute_length指示了属性值的长度
  • max_stack 表示操作数栈的最大深度(可控制的值)
  • max_locals 表示局部变量表所需的空间单位是变量槽. 变量槽是虚拟机分配内存的基本单位, 对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64位的数据类型则需要两个变量槽来存放
  • code_length代表字节码长度的长度,
  • code用于存放字节码指令, 每一个字节码指令的大小为u1, 个数为code_length. 

         继续使用如下代码作为案例分析: 

public class Test{private int m;public int inc() {return m + 1;}
}

        字节码: 

        通过上几节的分析, 我们可以找到属性code所在位置: 

         我们使用javap反编译这个文件: 

C:\Program Files\Java\jdk-17\bin>javap -verbose "you\path\Test.class"
Classfile /C:/TestData/ThreadTest/out/production/ThreadTest/Test.classLast modified 2024年9月30日; size 338 bytesSHA-256 checksum 03b1362989e1f14e5cc4311d6fbca80c2caf91724b4d5ee8eb27a94bfffaa76dCompiled from "Test.java"
public class Testminor version: 0major version: 52flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #3                          // Testsuper_class: #4                         // java/lang/Objectinterfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:#1 = Methodref          #4.#18         // java/lang/Object."<init>":()V#2 = Fieldref           #3.#19         // Test.m:I#3 = Class              #20            // Test#4 = Class              #21            // java/lang/Object#5 = Utf8               m#6 = Utf8               I#7 = Utf8               <init>#8 = Utf8               ()V#9 = Utf8               Code#10 = Utf8               LineNumberTable#11 = Utf8               LocalVariableTable#12 = Utf8               this#13 = Utf8               LTest;#14 = Utf8               inc#15 = Utf8               ()I#16 = Utf8               SourceFile#17 = Utf8               Test.java#18 = NameAndType        #7:#8          // "<init>":()V#19 = NameAndType        #5:#6          // m:I#20 = Utf8               Test#21 = Utf8               java/lang/Object
{public Test();descriptor: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       5     0  this   LTest;public int inc();descriptor: ()Iflags: (0x0001) ACC_PUBLICCode:stack=2, locals=1, args_size=10: aload_01: getfield      #2                  // Field m:I4: iconst_15: iadd6: ireturnLineNumberTable:line 4: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       7     0  this   LTest;
}
SourceFile: "Test.java"

        下面是<init>方法的字节码指令: 

  public Test();descriptor: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       5     0  this   LTest;

        可以看到args_size为1, 也就是参数为1, 但是构造方法明明没有任何参数, 同样的, 里面的locals为什么为1? 这是因为在类中的实例方法 无论有无参数, 都会有一个this参数指向自身. 

        仅仅是在编译的时候, 把this关键字作为转变为一个普通的方法参数. 因此在该方法的局部变量表中, 至少会存在一个指向当前实例的局部变量(占用局部变量表中第一个变量槽, 只对实例方法有效)




参考资料

  • 深入理解JAVA虚拟机 3版
  • oracle 官网 JVM规范: 

Java SE Specificationsicon-default.png?t=O83Ahttps://docs.oracle.com/javase/specs/index.html

  • Java虚拟机规范  Java SE 8版

相关文章:

JVM Class类文件结构

国庆节快乐 2024年10月2日17:49:22 目录 前言 magic 数 文件版本 使用JClassLib观察class文件 一般信息 接口 常量池 字段 方法 常量池计数器 常量池 类型 CONSTANT_Methodref_info CONSTANT_Class_info 类型结构总表 访问标志 类索引, …...

解决 GitHub 文件大小限制的问题

要解决 GitHub 文件大小限制的问题&#xff0c;可以使用 Git Large File Storage (Git LFS)。以下是设置步骤&#xff1a; 安装 Git LFS&#xff1a; 对于 macOS&#xff1a;brew install git-lfs对于 Windows&#xff1a;从 Git LFS官网 下载并安装。 初始化 Git LFS&#xff…...

wordpress源码资源站整站打包32GB数据,含6.7W条资源数据

源码太大了&#xff0c;足足32gb&#xff0c;先分享给大家。新手建立资源站&#xff0c;直接用这个代码部署一下&#xff0c;数据就够用了。辅助简单做下seo&#xff0c;一个新站就OK了。 温馨提示&#xff1a;必须按照顺序安装 代码下载...

金融领域的人工智能——Palmyra-Fin 如何重新定义市场分析

引言 DigiOps与人工智能 正在改变全球各行各业&#xff0c;并带来新的创新和效率水平。人工智能已成为金融领域的强大工具&#xff0c;为市场分析、风险管理和决策带来了新方法。金融市场以复杂性和快速变化而闻名&#xff0c;人工智能处理大量数据并提供清晰、可操作的见解的…...

STL--string类

我们从这篇文章之后就正式开始学习STL的string&#xff0c;字面看起来是不是像C语言里面的字符串之类的处理方法&#xff0c;是的&#xff0c;C里面也是对字符串的一些处理函数&#xff0c;但是C有很多这样的函数&#xff0c;给大家推荐一个网站 &#xff0c;这个网站是C的官网…...

iptables 的NDAT报错bash: 9000: command not forward

外网主机设置&#xff1a; iptables -t nat -A PREROUTING -d 192.168.3.51 -p tcp --dport 9000 -j DNAT --to-destination 192.168.3.61:22 本地shell连接&#xff1a; PS C:> ssh root192.168.3.51 9000 显示如下操作&#xff1a; PS C:> ssh root192.168.3.51 9000…...

快速了解:MySQL InnoDB和MyISAM的区别

目录 一、序言二、InnoDB和MyISAM对比1、InnoDB特性支持如下2、MyISAM特性支持如下 三、两者核心区别1、事务支持2、锁机制3、索引结构4、缓存机制5、故障恢复6、使用场景 一、序言 在MySQL 8.0中&#xff0c;InnoDB是默认的存储引擎。除了InnoDB&#xff0c;MySQL还支持其它的…...

TI DSP TMS320F280025 Note14:模数转换器ADC原理分析与应用

TMS320F280025 模数转换器ADC原理分析与应用 ` 文章目录 TMS320F280025 模数转换器ADC原理分析与应用逐次比较型ADC和双积分型ADC工作原理逐次比较型 ADC双积分型 ADC280025ADCADC原理分析ADC时钟SOCSOC内部原理ADC触发方式ADC采集(采样和保持)窗口通道寄生电容基准电压发生器模…...

【C++前缀和】2845. 统计趣味子数组的数目|2073

本文涉及的基础知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 LeetCode 2845. 统计趣味子数组的数目 难度分&#xff1a;2073 给你一个下标从 0 开始的整数数组 nums &#xff0c;以及整数 modulo 和整数 k 。 请你找出并统计数组…...

C++入门基础 (超详解)

文章目录 前言1. C关键字2. C的第一个程序3. 命名空间3.1 namespace的定义3.2 命名空间的嵌套3.3 命名空间使用3.4 查找优先级总结 4. C输入和输出4.1 标准输入输出 (iostream库)4.2 文件输入输出 (fstream库)4.3 字符串流 (sstream库)4.4 C格式化输出4.5 std::endl和\n的区别 …...

docker零基础入门教程

注意 本系列文章已升级、转移至我的自建站点中&#xff0c;本章原文为&#xff1a;Docker入门 目录 注意1.前言2.docker安装3.docker基本使用4.打包docker镜像5.docker进阶 1.前言 如果你长期写C/C代码&#xff0c;那你应该很容易发现C/C开源项目存在的一个严重问题&#xff…...

【Java SE 题库】移除元素(暴力解法)--力扣

&#x1f525;博客主页&#x1f525;&#xff1a;【 坊钰_CSDN博客 】 欢迎各位点赞&#x1f44d;评论✍收藏⭐ 目录 1. 题目 2. 解法(快慢“指针”) 3. 源码 4. 小结 1. 题目 给你一个数组 nums 和一个值 val&#xff0c;你需要原地移除所有数值等于 val 的元素。元素的顺…...

linux文件编程_进程

1. 进程相关概念 面试中关于进程&#xff0c;应该会问的的几个问题&#xff1a; 1.1. 什么是程序&#xff0c;什么是进程&#xff0c;有什么区别&#xff1f; 程序是静态的概念&#xff0c;比如&#xff1a; 磁盘中生成的a.out文件&#xff0c;就叫做&#xff1a;程序进程是…...

java NIO实现UDP通讯

NIO Udp通讯工具类 import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.util.Iterator;impo…...

ffmpeg如何实现视频推流?

FFmpeg是一个强大的多媒体框架&#xff0c;用于处理视频和音频数据。它包括了libavcodec(用于解码和编码)、libavformat(用于格式转换)、libavutil(提供一些辅助工具和函数)、libavfilter(用于音视频过滤)等多个库。 以下这些都是FFmpeg的特性 FFmpeg支持大量的音视频编解码器&…...

【HTML5】html5开篇基础(3)

1.❤️❤️前言~&#x1f973;&#x1f389;&#x1f389;&#x1f389; Hello, Hello~ 亲爱的朋友们&#x1f44b;&#x1f44b;&#xff0c;这里是E绵绵呀✍️✍️。 如果你喜欢这篇文章&#xff0c;请别吝啬你的点赞❤️❤️和收藏&#x1f4d6;&#x1f4d6;。如果你对我的…...

echarts实现3D柱状图(视觉层面)根据博主改编

https://blog.csdn.net/weixin_57798646/article/details/131067725 这是原贴 在这个基础上我需要实现 一根柱子 代码如下 <!DOCTYPE html> <html lang"en" style"height: 100%"><head><meta charset"utf8"> </hea…...

【一篇文章理解Java中多级缓存的设计与实现】

文章目录 一.什么是多级缓存&#xff1f;1.本地缓存2.远程缓存3.缓存层级4.加载策略 二.适合/不适合的业务场景1.适合的业务场景2.不适合的业务场景 三.Redis与Caffine的对比1. 序列化2. 进程关系 四.各本地缓存性能测试对比报告(官方)五.本地缓存Caffine如何使用1. 引入maven依…...

OpenSource - 开源WAF_SamWaf

文章目录 PreSafeLine VS SamWaf开发初衷软件介绍架构界面主要功能 使用说明下载最新版本快速启动WindowsLinuxDocker 启动访问升级指南自动升级手动升级 在线文档 代码相关代码托管介绍和编译已测试支持的平台测试效果 安全策略问题反馈许可证书贡献代码 Pre Nginx - 集成Mod…...

旅游避坑指南

1.火车站旁白的小摊贩&#xff0c;还有周边的小饭店百分之百是黑店&#xff0c;不仅难吃要死而且巨黑&#xff01;&#xff01;&#xff01; 可以地图上搜索附近的大型商超&#xff0c;例如泰安市的银座商超&#xff0c;里面的东西不仅好吃而且价格透明&#xff0c;还有很多当…...

矩阵系统源码搭建的具体步骤,支持oem,源码搭建

一、前期准备 明确需求 确定矩阵系统的具体用途&#xff0c;例如是用于社交媒体管理、电商营销还是其他领域。梳理所需的功能模块&#xff0c;如多账号管理、内容发布、数据分析等。 技术选型 选择适合的编程语言&#xff0c;如 Python、Java、Node.js 等。确定数据库类型&…...

正则表达式调试工具实战

正则表达式调试工具实战 1、新建工程QWidget工程工程名RegexTool 如果QT不会配置,请参考我的博客,QT配置 Widget.cpp 默认内容如下 2、主界面设计 三行两列,每行采用HBoxLayout作为行布局控件,内部一个Lable控件和一个TextEdit控件,采用VBoxLayout 控件包裹三个HBoxLa…...

SQL:函数以及约束

目录 介绍 函数 字符串函数 数值函数 日期函数 流程函数 约束 总结 介绍 说到函数我们都不陌生,在C,C,java等语言中都有库函数,我们在平时也是经常使用,函数就是一段代码,我们既可以自定义实现,又可以使用库里内置的函数;从来更加简洁方便的完成业务;同样的在SQL中也有…...

在Linux中将设备驱动的地址映射到用户空间

本期主题&#xff1a; MMU的简单介绍&#xff0c;以及如何实现设备地址映射到用户空间 往期链接&#xff1a; Linux内核链表零长度数组的使用inline的作用嵌入式C基础——ARRAY_SIZE使用以及踩坑分析Linux下如何操作寄存器&#xff08;用户空间、内核空间方法讲解&#xff09;…...

电脑自带dll修复在哪里,dll丢失的6种解决方法总结

在现代科技日新月异的时代&#xff0c;电脑已经成为我们生活中不可或缺的一部分。然而&#xff0c;在使用电脑的过程中&#xff0c;我们常常会遇到一些常见的问题&#xff0c;其中之一就是dll文件丢失或损坏。当这些dll文件丢失或损坏时&#xff0c;可能会导致某些应用程序无法…...

k8s基于nfs创建storageClass

首先安装nfs #服务端安装 yum install -y nfs-utils rpcbind #客户端安装 yum install -y nfs-utils #启动服务 并设置开启启动 systemctl start rpcbind && systemctl enable rpcbind systemctl start nfs && systemctl enable nfs #创建共享目录 mkdir -p /…...

Chrome无法拖入加载.crx扩展文件(以IDM为例)

问题原因&#xff1a;新版本的Chrome浏览器已不支持加载.crx文件 解决办法&#xff1a;将.crx文件压缩为.zip文件&#xff0c;解压缩后再加载到Chrome中 以IDM的.crx文件作为示例&#xff1b; IDM的.crx文件位于C:\Program Files (x86)\Internet Download Manager; 将IDMGCE…...

数字教学时代:构建高效在线帮助中心的重要性

在数字化教学日益普及的今天&#xff0c;教育领域正经历着前所未有的变革。随着在线课程、虚拟教室、智能学习平台等数字化工具的广泛应用&#xff0c;教育资源的获取方式和学习模式发生了深刻变化。然而&#xff0c;这种变革也带来了新的挑战&#xff0c;其中之一便是如何确保…...

828华为云征文|华为云弹性云服务器FlexusX实例下的Nginx性能测试

本文写的是华为云弹性云服务器FlexusX实例下的Nginx性能测试 目录 一、华为云弹性云服务器FlexusX实例简介二、测试环境三、测试工具四、测试方法五、测试结果 下面是华为云弹性云服务器FlexusX实例下的Nginx性能测试。 一、华为云弹性云服务器FlexusX实例简介 华为云弹性云服…...

知识图谱入门——2:技术体系基本概念:知识表示与建模、知识抽取与挖掘、知识存储与融合、知识推理与检索

知识图谱是通过构建“实体”和“关系”来描述世界的信息网络&#xff0c;它不仅是数据的存储方式&#xff0c;还可以支持推理与查询&#xff0c;帮助系统更好地理解、整合和利用数据。 文章目录 1. 知识表示与建模2. 知识抽取与挖掘3. 知识存储与融合4. 知识推理与检索总结 1.…...

合肥新站开发区管委会网站/武汉seo论坛

使用技术 LR.APP是基于uni-app开发的多端APP/小程序系统&#xff0c;设计理念是解决多端开发问题&#xff0c;使用时&#xff0c;开发者仅需一套代码&#xff0c;即可编译到iOS、Android、H5、小程序等多个平台。 LR.APP封装了跨端兼容的组件和api&#xff0c;如果你不熟悉un…...

家具定制东莞网站建设/seo博客教程

1.介绍Kaldi语音识别工具将HTK比较零碎的各种各样的指令和功能进行整理集合&#xff0c;使用perl脚本调用。同时也加入了深度神经网络的分类器(DNN)&#xff0c;本身由原来做HTK开发的人员制作而成&#xff0c;可以说是HTK的升级加强版。kaldi官方网站请见&#xff1a;http://k…...

兴仁企业建站公司/谷歌手机版下载安装

01. ip address show/ip a 检查网卡地址配置02. ping 测试网络连通性03. nmtui 图形界面修改网卡地址信息04. exit 注销05. shutdown 关机命令shutdown -h 5 指定关机时间 &#xff08;推荐&#xff09;shutdown -r 5 重启主机时间 &#xff08;推荐&#xff09;shutdown -…...

模板网站建设公司/免费奖励自己的网站

8.4.6 用编程方式添加DataTable行 在为DataTable定义了架构之后&#xff0c;也就是设置好了需要的列名以后&#xff0c;就可以可通过将DataRow对象添加到表的Rows集合中来将数据行添加到表中。与添加DataColumn类似&#xff0c;同样可以通过使用DataRow构造函数&#xff0c;或…...

微网站免费建站系统/互联网广告投放代理公司

使用RD Client来远程桌面 可能你会觉得奇怪&#xff0c;team viewer和向日葵之类的难道不香吗&#xff1f;看起来他们两个都是实现了远程桌面的功能&#xff0c;好像没必要特地用Windows自带的RD Client进行内网穿透之后远程桌面。 实际上team viewer之类的在我的使用范围内不…...

做网站制作较好的公司/成都疫情最新消息

一&#xff0e;简介 0. 页面的生命周期。 1. WebForm后台页面类继承于Page类&#xff0c;Page类实现了IHttpHandler接口。 2. 前台页面类继承于后台页面类。 3. 先调用PageLoad方法&#xff0c;再调用Render方法生成html代码。 二. 加密安全 互联网没有绝对的安全&#xff0c;登…...