导语
  对于JVM来说,Class文件作为虚拟机的一个重要接口。无论使用什么样的语言,进行软件开发,只要能将源码编译为Class文件,并放到正确的路径下,那么这种语言就可以被JVM所执行。可以说Class可以运行在任何平台JVM中,下面来介绍一下Class文件,有助于理解JVM的运行过程。

文章目录

  • 跨平台、跨语言
  • Class文件
    • Class文件的标志-魔数
    • Class文件的版本
    • 常量池
    • Class的访问标记(Access Flag)
    • 当前类、父类和接口
    • Class文件的字段
    • Class文件的方法基本结构

跨平台、跨语言

  JVM提供了Java语言的跨平台特性。可以使用不同平台的Java虚拟机,让同一份Class文件运行在不同的平台上。这个作为Java语言的一个特性。但是从目前的发展来看,这个优势并不是绝对优势了,现在有很多的编程语言都开始有了同样的支持。例如Python、PHP、Perl、Ruby、Lisp等,虽然这些语言的运行原理与Java的运行原理不同,但是依靠它们强大的解释器,可以天生的实现平台无关性。即便是.NET的体系中也有。跨平台似乎已经是编程语言的一般特点了。

  JVM,作为一个平台,JVM不仅提供了跨平台功能,甚至还支持了一些跨语言的操作。从理论上将,无论使用任意语言编程的软件,都可以在任意平台上执行。而实现这一特点的内容就是通过一个统一的方式进行解释,并且这个解释,在底层上支持各种平台。JVM就是通过Class文件来实现这一强大功能的统一。

  目前来看微软的.NET 平台是跨语言的典范,无论是C#还是VB都可以在.NET 平台中执行。随着Java虚拟机的不断发展,在这个方面,也在不断的发展。当下很多的语言都可以在JVM上可以执行。例如Groovy、Scala、Jython等。

Class文件

  分析Class文件,是理解JVM的基础之一,Class文件的结构并不是一成不变的,随着JVM的发展,不可避免的要对Class文件结构进行调整,但是其原理是不变的。

  在了解的总体结构之后,下面就来详细说明一下Class文件的各个组成部分。
  首先,需要聊的就是用于描述Class文件结构的基本数据类型。
  在JVM规范中,Class文件使用一种类似C语言结构体的方式进行描述,并且统一使用无符号的整型作为基本数据类型,由u1、u2、u4、u8分别表示无符号单字节、2字节、4字节、8字节整数。对于字符串。则使用u1数组进行表示。

  Class 文件的结构严格按照该结构的定义:

  • 1、文件以一个四字节的Magic(被称为魔数)开头,紧跟着大、小版本号。
  • 2、在版本号之后是常量池,常量池的个数为constant_pool_count,常量池中的可以存放constant_pool_count-1。
  • 3、常量池之后是类的访问权限修饰符、代表自身类的引用、父类引用及接口数量和实现的接口引用。
  • 4、在接口之后,有字段的数量和字段描述、方法数量以及方法描述。
  • 5、存放文件的属性信息。

Class文件的标志-魔数

  魔数(Magic Number) 作为Class文件的标志,用来告诉Java虚拟机,这是一个Class文件。魔数是一个4字节的无符号整数。它固定位0xCAFEBABE。


注意
  魔数作为Class文件标识,在程序开发的时候,软件设计者喜欢使用一些特殊的数字来标识固定的文件类型或者是特殊的含义。除了Java的Class文件,常用的TAR文件、PE文件等等,甚至网络的DHCP报文内部,都有类似的手法。

public class SimpleUser{public static final int TYPE = 1;private int id;private String name;public int getId(){return id;}public void setId(int id) throws IllegalStateException{try{this.id = id;} cache(IllegalStateException e){System.out.println(e.toString());}}public String getName(){return name;}public void setName(String name){this.name = name;}}

  使用软件WinHex 打开上面代码,就可以在生成的Class文件中找到魔数

Class文件的版本

  在魔数后面,紧跟着是Class的小版本号和大版本号,表示当前Class文件是由那个版本的编译器编译产生的,首先出现的是小版本号,是一个2字节的无符号整数,在此之后是为大版本号,也是用2个字节表示。

  如下图所示,大版本号是0x33,换算为十进制是51,因此可以判断该文件该Class文件是由JDK1.7的编译器或者在JDK1.8下使用“-target 1.7” 参数编译生成的。

注意:0x33 是十六进制表示,一般的在十六进制前加上0x前缀,用来区别于十进制的数字。


  目前,高版本的Java虚拟机可以执行由低版本的编译器编译生成的Class文件,而低版本的Java虚拟机不能执行由高版本的编译器生成的Class文件。下面这个文件应该不陌生。

  在实际开发中,由于开发环境与生产环境的不同,可能会导致上述问题的发生,在开发的时候需要特别注意开发环境与线上运行环境的JDK版本的问题。

常量池

  常量池是Class中的内容比较丰富的区域,随着JVM的发展,常量池中的内容也会越来越丰富。同时,常量池对于Class文件中的字段、方法等都有非常重要的意义。可以说是Class运行的基础。在版本号之后,接着就是常量池的数量,以及若干的常量池内容。

  如下图所示,0x37 表示在Class文件中常量池有54项的内容,为什么是54项,是因为常量池0位置为空,不存放实际内容。在数量之后就是常量池的实际内容勒,每个内容以长度、内容或者是内容、类型等格式排列。

  在详细解析常量池之前,先给出常量池表项的类型以及其TAG值,如下图所示,它标识常量池中可能会出现的内容,其中TAG标识该常量的枚举整型数据。

  作为常量池底层的数据类型,CONSTANT_Utf8、CONSTANT_Integer等内容分别表示UTF8字符串、整数等。

CONSTANT_Utf8{ul tag;u2 length;u1 bytes[length]
}

  UTF8 的TAG值为1,紧接着是该字符串的长度length,最后存储的是字符串的内容。

  如下图所示,0x01 表示一个UTF8的常量,接着0x0019表示该常量一共25个字节,因此从0x0019之后的内容就是字符串的具体内容勒,从图中也可以看到字符串的内容是geym/zbase/ch9/SimpleUser。


  UTF8常量经常被其他类型的常量引用,例如再上面可以看到CONSTANT_Class 常量就会引用该UTF8作为类名出现,CONSTANT_Class的结构如下

CONSTANT_Class_info{ul tag;u2 name_index;
}

  其中tag为7,表示是一个CONSTANT_Class常量,第二个字段为一个2字节整数,表示常量池的索引。在CONSTANT_Class中该索引指向的常量必须是CONSTANT_Utf8。

  如下图所示0x07 表示该常量为CONSTANT_Class,0x02 表示该类的类名由常量池中第二个常量字符串指定,也就是上面提到的类名。也就是geym/zbase/ch9/SimpleUser。由此,可以知道CONSTANT_Class 表示的就是geym/zbase/ch9/SimpleUser。


  CONSTANT_Integer、CONSTANT_Float、CONSTANT_Long、CONSTANT_Double 分别表示数字的字面量。当使用final定义一个数字常量的时候,Class文件中就会生成一个数字常量。结构分别如下

CONSTANT_Integer_info{u1 tag;u4 bytes;
}
CONSTANT_Float_info{u1 tag;u4 bytes;
}
CONSTANT_Long_info{u1 tag;u4 high_bytes;u4 low_bytes;
}
CONSTANT_Double_info{u1 tag;u4 high_bytes;u4 low_bytes;
}

  如下图所示,找到其中的0x03,表示是一个整数常量,接着是00 00 00 01 表示数字1。

  另外一个值得注意的字面量是CONSTANT_String,它标识一个字符串常量,结构如下

CONSTANT_String_info{u1 tag;u2 string_index
}

  其中tag为8,一个2字节长度的无符号指向常量池的索引,表示该字符串的UTF8的内容
  还有一个需要注意的就是CONSTANT_NameAndType 从名字上可以看出,它表示一个名称和类型,格式如下

CONSTANT_NameAndType_info{u1 tag;u2 name_index;u3 descripter_index;
}

  其中tag 为12,第一个2字节name_index 表示名称,意思是常量池的索引,表示常量池底name_index 项的名字,通常可以表示字段名字或者方法名字。第二个2字节descriptor_index 类型描述,比如方法的前面或者字段的类型。

  0x0C 表示一个CONSTANT_NameAndType ,0x0009表示第9个常量的名称,查常量池可以知道,第九项常量为id字符串。0x0006表示第六个常量,查常量池为字符串I,表示int类型,所以CONSTANT_NameAndType 表示一个名称为id,类型为Int的表项。


  CONSTANT_NameAndType的descriptor_index使用了一组特定的字符串来表示类型。

  对于对象类型来说,总是以L开头,紧跟类的全限定名,用分号结尾,例如字符串"Ljava/lang/Object";表示类java.lang.Object。数组则是以左中括号为标记,例如String二维数组,使用[[java/lang/String;字符串表示。

  类的方法和字段分别使用CONSTANT_Methodref 和 CONSTANT_Fieldref 表示。分别表示一个类的方法及字段的引用。CONSTANT_Methodref 和CONSTANT_Fieldref 的结构是非常类似的。如下

CONSTANT_Methodref_info{u1 tag;u2 class_index;u2 name_and_type_index;
}CONSTANT_Fieldref_info{u1 tag;u2 class_index;u2 name_and_type_index;
}

  其中CONSTANT_Methodref 的tag值为10,CONSTANT_Fieldref的tag值为9。它们的class_index 表示方法或者字段所在的类在常量池中的索引会指向一个CONSTANT_Class结构。第二项name_and_type_index 也指向常量池的索引,但是表示一个CONSTANT_NameAndType结构,它定义了方法或者字段的名称、类型或者签名。

  如下图所示,System.out.println() 在常量池中的关系引用。可以看到,Methodref结构的class index指向41项常量池,表示Class,而该项又进一步指向常量池中的UTF8数据,表明该Class的类型。而Methodref 的name_and_type字段则指向了常量池43项,NameAndType类型的数据,它包括名字和类型两个字段,又分别指向常量池中的两个字符串println和(Ljava/lang/String)V,表示方法的名字和方法的签名。(Ljava/lang/String;)V 表示该方法接收一个String类型的参数,并且返回值为void,这样就可以清楚的描述出Methodref的引用结构了。

  最后,来看一组常量类型CONSTANT_MethodHandle、CONSTANT_MethodType 和 CONSTANT_InvokeDynamic。它们是在JDK1.7 中引入的新的常量类型,分别表示函数句柄、函数类型签名和动态调用。结构如下

CONSTANT_MethodType{u1 tag;u2 descripter_index;
}

  其中tag为16,descriptor_index为指向常量池的一个UTF8字符串的索引。使用方法和前面介绍的索引是一样的,该常量项用于描述一个方法签名,比如"()V" 表示一个不接收参数、返回值为Void的方法,当需要传送给引导方法一个MethodType类型参数的时候,类文件中就会出现此项。

  CONSTANT_MethodHandle 为一个方法句柄,它可以用来表示方法、类的字段或者构造函数等,方法句柄指向一个方法、字段,和C语言中的函数指针或者C#中的委托有些类似。

CONSTANT_MethodHandle_info{u1 tag;u1 reference_kind;u2 reference_index;
}

  其中tag值为15,referenc_kind 表示这个方法句柄的类型,reference_index为指向常量池的索引,reference_index具体指向的类型由reference_kind 确定,两者关系如下

  CONSTANT_InvokeDynamic 结构用于描述一个动态调用,动态调用是JVM平台引入,专门为动态语言提供函数动态调用绑定支持功能。结构如下

CONSTANT_InvokeDynamic_info{u1 tag;u2 bootstrap_method_attr_index;u2 name_and_type_index;
}

  其中tag为18,bootstrap_method_attr_index 为指向引导方法表中的索引,即定位到一个引导方法。引导方法用于在动态调用时进行运行时函数查找和绑定。引导方法表属于类文件的属性(Attribute),name_and_type_index为指向常量池的索引,且指向表项必须是CONSTANT_NameAndType,用于表示方法的名字及签名。

Class的访问标记(Access Flag)

  在常量池后面,紧跟着就是访问标识,该标识用2个字节来表示,用于表明该类的访问信息,例如public、final、abstract等等。从下表中可以看出每个类型的表示都是通过访问标记32位中的特定位置来标识的。

  如下图所示,标记为0x0021 可以判断出他是public类,且ACC_SUPER 被标记为1,

  提示:使用ACC_SUPER可以让类更为准确地定位到父类的方法super.method(),现在很多的编译器都是设置并且使用这个标记。

当前类、父类和接口

  在访问标记之后,会指定该类的类别、父类类别以及实现的接口

u2 this_class;
u2 super_class;
u2 interface_count;
u2 interfaces[interfaces_count]

  其中,this_class、super_class都是2字节无符号整数,它们指向常量池中一个CONSTANT_Class,以表示当前的类型及父类。由于在Java中只能使用单继承,所以只需要保存单个父类。

注意:super_class指向父类不能是final

  由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引,表示接口的每一个索引也是一个指向常量池的CONSTANT_Class。如果该类没有任何实现接口,则interfaces_count 为0;

Class文件的字段

  在接口表述之后,会有类的字段信息。由于一个类会有多个字段,所以需要首先指明字段个数

u2    fields_count;
field_info field[fields_count]

  字段数量fields_count是一个2字节无符号整数。字段数量之后的字段的具体信息,每个字段为一个field_info的结构,该结构如下

field_info {u2   access_flags;u2   name_index;u2   descriptor_index;u2   attributes_count;attribute_info attributes[attributes_count]
}
  • 1、字段访问标记,非常类似于类的访问标记。如下图所示
  • 2、一个2字节整数,表示字段的名称,它指向常量池中的CONSTANT_Utf8结构。
  • 3、名称后的descriptor_index 也指向常量池中的CONSTANT_Utf8,该字符用于描述字段类型,具体如下
  • 4、一个字段还可能拥有一切属性,用于存储更多的额外信息,比如初始化值、一些注释信息等。属性个数存放在attributes_count中,属性具体内容存放在attributes数组中。

  以常量属性为例,结构如下

CONSTANTVALUE_attribute{u2 attribute_name_index;u4 attribute_length;u2 constantvlaue_index;
}

  常量属性的attribute_name_index 为2字节整数,指向常量池的CONSTANT_Utf8。并且这个字符串为“ConstantValue”。接着是attribute_length,由4个字节组成,表示这个属性的剩余长度为多少。对常量属性而言,这个值恒为2。最后的constantvalue_index 表示属性值,但是值并不直接出现在属性中,而是存放在常量池中,这里的constantvalue_index也是指向常量池的索引,并且存在如下图所示的对应关系中。

Class文件的方法基本结构

  在字段之后,就是类的方法信息。方法信息和字段类似,由两部分组成:

u2 methods_count;
method_info methods[methods_count]

  其中methods_count 为2字节整数,表示该类中有几个方法。接着就是methods_count 个 method_info 结构,每个method_info结构表示一个方法,如下

method_info{u2  access_flags;u2  name_index;u2 descriptor_index;attribute_info attributes[attributes_count];
}

  (1) access_flag 为方法的访问标记,用于标明方法的权限及相关特性,它的取值如下

  (2) 在访问标记后,name_index 表示方法的名称,它是一个指向常量池的索引。descriptor_index为方法描述,它也是指向常量池的索引,是一个字符串,表示方法的签名(参数、返回值等),它基于商标中的字符串的类型表示方法,同时对方法签名的表示做了一些规定。它将函数的参数类型写在一对小括号中,并在括号右侧给出方法的返回值类型

Object m(int i,double d,Thread t){……}

它的方法描述符为

{IDLjava/lang/Thread;}Ljava/lang/Object;

  可以看到,方法的参数统一列在一对小括号中,“I” 表示int,“D” 表示double,“Ljava/lang/Thread;” 表示Thread对象,小括号右侧的Ljava/lang/Object;表示方法返回值为Object对象。

  (3)和字段类似,方法也可以附带若干属性,用于描述一些额外的信息,例如方法字节码等,attributes_count表示该方法中属性的数量,接着就是attributes_count个属性的描述。

  对于属性来讲,统一格式为

attribute_info{u2 attribute_name_index;u4 attribute_length;u1 info[attribute_length];
}

  其中,attribute_name_index 表示当前attribute的名称,attribute_length为当前attribute的剩余长度,接着就是attribute_length字节的byte数组。

垃圾回收算法与实现系列-Java的Class文件详解相关推荐

  1. 垃圾回收算法与实现系列-Java堆内存溢出原因

    导语   内存一直是所有开发人员探索的一片天地,再JVM中,内存往往会被分为几块,了解不同的内存区域对编写出优质的代码有很大的帮助.堆内存作为JVM中比较重要的区域,有很多值得我们探索的地方.下面就来 ...

  2. 垃圾回收算法与实现系列-学习GC之前的准备工作

    导语   在学习垃圾回收算法之前,首先需要了解什么是Heap.什么是Root.什么是Object.什么是Stack.什么是Pointer,这写概念都是什么,为什么要在垃圾回收算法中使用,使用这些东西有 ...

  3. 垃圾回收算法与实现系列-GC 标记-清除算法

    导语   在GC 中最重要的算法就是GC标记-清除算法(Mark-Sweep GC).在很多的场景下都还是在使用这个算法来进行垃圾回收操作.就如如同它的名字一样先标记,然后清除.下面就来看看标记清除算 ...

  4. 垃圾回收算法与实现系列-String在虚拟机中的实现

    导语   String 字符串一直作为各种编程语言的核心内容存在.作为动态字符的一种是实现方案,应用很广泛.每一种计算机语言对于这种数据结构都进行了特殊的优化和实现.在Java中,String作为引用 ...

  5. 垃圾回收算法与实现系列-锁在Java虚拟机中的实现和优化

    导语   上篇分享中提到了对象头Mark Word 的基本概念之后,接下来就可以深入到虚拟机内部了.在多线程程序中,线程之间的竞争是不可避免的,并且这是一种多线程程序的常态.那么如何高效的处理多线程的 ...

  6. 垃圾回收算法与实现系列-JVM无锁实现

    导语   为了确保多线程场景下数据安全,使用锁机制一直是一种优秀的解决方案,但是再高并发场景下,对锁的竞争可能成为性能瓶颈.为此,有出现了一种新的解决方案,被称为是非阻塞同步的方案.这种实现方式不需要 ...

  7. 垃圾回收算法与实现系列-锁在应用层的优化思路

    导语   之前的分享中主要介绍了虚拟机内部的对锁机制的优化与具体实现,在实际的开发过程中,还可以通过在应用层的合理优化,达到保证性能的目的,那么下面就学习介绍一下在应用层中如何进行锁的优化. 文章目录 ...

  8. 垃圾回收算法与实现系列-线程安全与锁简介

    导语   锁是多线程软件开发的必要工具,它的基本作用是保护临界区资源不被多个线程同时访问进而受到破坏.如果由于多线程访问造成数据不一致,那么系统将会得到一个错误的结果.通过锁可以让多个线程排队一个一个 ...

  9. java解析excel文件详解_java解析excel文件的方法

    建立工程前需要导入POI包.POI相关jar包下载地址:http://poi.apache.org/download.html 1.解析.xlsx后缀名的的EXCEL文件: package com.s ...

最新文章

  1. CentOS 7.8升级gcc-8.2
  2. 微软称不放弃收购雅虎
  3. jquery ajax自动完成,使用AJAX进行JQuery自动完成(JQuery Autocomplete with AJAX)
  4. Nginx--------地址重写
  5. java之spring mvc之拦截器
  6. c++读出像素矩阵_Python传numpy矩阵调c++(求3D图像连通区域)
  7. c语言皮尔森系数程序,按条件选入观测;皮尔森相关系数
  8. django 1.8 官方文档翻译:13-1-3 密码管理
  9. php mysql复杂查询_PHP MySQL如何做更复杂的查询
  10. android的looper,handler消息小结
  11. linux(Centos7系统)中安装JDK、Tomcat、Mysql
  12. WEB安全 asp+access注入
  13. 微信小程序前端微信支付功能 支付流程
  14. 安装与测试Hypopg(适用于pg9.0版本以上)
  15. 甘肃康县乡村“蝶变”:北方山沟引来“南方媳妇”
  16. 微信小程序上传图片后 开发者工具自动刷新问题
  17. bellman - ford算法c++
  18. 值得反复看的经典算法书
  19. 12【不定式  动名词】to-infinitive gerund
  20. HDU 4509 湫湫系列故事——减肥记II

热门文章

  1. java final date_Java 8新特性之Date/Time(八恶人-4)
  2. Play framework logging设置
  3. IOS精品源码,仿探探UIButton封装iOS提示弹框迅速引导页自定义导航栏
  4. 父元素遮住子元素的问题
  5. ZOJ2724_Windows Message Queue(STL/优先队列)
  6. 学习.NET是因为热爱 or 兴趣 or 挣钱?
  7. 大一下学期的自我目标
  8. position:sticky布局
  9. Ubuntu Touch 预览版安装过程解析
  10. SQL Server 2000优化SELECT语句方法