Java虚拟机原理

1.JVM体系结构
2.运行时数据区

程序运行起来就是一个动态的过程,必须合理的划分内存区域,来存放各种数据。运行时数据区的划分, 是和JVM的体系结构相关的。类加载器子系统用于将class文件加载到虚拟机的运行时数据区中(准确的说应该是方法区) 。 可以认为执行引擎是字节码的执行机制, 一个线程可以看做是一个执行引擎的实例。

2.1程序计数器
  • 程序计数器(Program Counter Register)是一块较小的内存空间,存储了下一条需要执行的(JVM汇编)字节码指令的地址。它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • JVM多线程是通过线程轮换并分配执行时间的方式实现的。任何一个确定的时间内,JVM只会执行其中一条指令。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,即线程私有内存。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryErro情况的区域。
  • 如果线程正在执行的是一个Java方法,这个计数器记录的事正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器的值则为空(Undefined)。
2.2Java虚拟机栈
  • 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。在线程创建的时候,都会为其创建一个私有的虚拟机栈,虚拟机栈的生命周期跟线程一样。
  • Java虚拟机栈可能发生的异常情况:
    • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
    • 如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
  • 栈帧(Stack Frame)
    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
    • 局部变量表
    • 局部变量表是一组变量值存储空间,用于存放方法参数和方法内部的局部变量。在Java编译为Class文件是,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
    • 局部变量表存放了编译期可知的各种基本数据类型(byte、short、char、int、float、double、long、boolean)、对象引用(reference类型,不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向一条字节码指令的地址)
    • 局部变量表的容量以变量槽(Slot)为最小单位,为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,在某些情况下,Slot的复用会直接影响到系统的垃圾收集行为。
    • 操作数栈
    • 操作数栈的最大深度在编译的时候写入到Code属性的max_stacks数据项中。
    • 操作数栈所属的栈帧在刚刚被创建的时候,操作数栈是空的。在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。
    • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,即操作数栈的操作必须与操作数栈栈顶的数据类型相匹配。
    • 动态链接
    • 在Class文件的常量池中存有大量的符号引用,这些符号引用一部分在类加载阶段或者第一次使用的时候就转化为直接引用,称为静态解析。另一部分将在每一次运行期间转化为直接引用,称为动态链接。
    • 方法返回地址
    • 方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入到调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。
    • 附加信息
    • 方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入到调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。
2.3本地方法栈

本地方法栈与虚拟机栈所发挥的作用非常相似,区别在于虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、方式与数据结构没有强制规定,因此具体的虚拟机可以自由实现。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryErro异常。

2.4堆
  • 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。所有的对象实例以及数组都要在堆上分配。
  • 堆是垃圾收集器管理的主要区域,存储了被自动内存管理系统所管理各种对象,这些受管理的对象无需,也无法显式地被销毁。
  • 类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行。
  • Java 堆可能发生如下异常情况:
    • 如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那 Java 虚拟机将会抛出一个OutOfMemoryError 异常。
  • 垃圾收集器

    • 早期垃圾收集器
      堆被分解为较小的三个部分。具体分为:新生代、老年代、持久代。
      1118277-20170305105444876-14823016.png
    • 绝大部分新生成的对象都放在Eden区,当Eden区将满,JVM会因申请不到内存,而触发Young GC ,进行Eden区+有对象的Survivor区(设为S0区)垃圾回收,把存活的对象用复制算法拷贝到一个空的Survivor(S1)中,此时Eden区被清空,另外一个Survivor S0也为空。下次触发Young GC回收Eden+S0,将存活对象拷贝到S1中。新生代垃圾回收简单、粗暴、高效。
    • 若发现Survivor区满了,则将这些对象拷贝到old区或者Survivor没满但某些对象足够Old,也拷贝到Old区(每次Young GC都会使Survivor区存活对象值+1,直到阈值)。
    • Old区也会进行垃圾收集(Young GC),发生一次 Major GC 至少伴随一次Young GC,一般比Young GC慢十倍以上。
    • JVM在Old区申请不到内存,会进行Full GC。Old区使用一般采用Concurrent-Mark–Sweep策略回收内存。
    • G1垃圾收集器
    • G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
    • G1收集器中,Java堆得内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),每一块区域对应虚拟机的一片连续内存。这些区域(Region)被映射为逻辑上的Eden, Survivor和Old generation,它们都是一部分Region(不需要连续)的集合。
    • G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。有计划的避免在整个Java堆中进行全区域的垃圾收集,保证了G1收集器在有限的时间可以获取尽可能搞的手机效率。
    • Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered来避免全堆扫描。
    • G1堆空间分配(Heap Allocation)

    • 新生代收集
    • 堆被分为大约2000(2048)个区. 最小size为1 Mb, 最大size为 32Mb. 蓝色的区保存老年代对象,绿色区域保存年轻代对象.G1中各代的heap区不像老一代垃圾收集器一样要求各部分是连续的.
    • 年轻代的垃圾收集,此时会有一次 stop the world(STW)暂停.在操作时所有的应用程序线程都会被暂停(stopped).
    • 年轻代 GC 通过多线程并行进行.存活对象被转移到存活区(survivor regions)或老年代(old generation regions).

    • 老年代收集
      • 存活对象的初始标记被固定在年轻代垃圾收集里面. 在日志中被记为 GC pause (young)(inital-mark)。
      • 如果找到空的区域(如用红叉“X”标示的区域), 则会在 Remark 阶段立即移除. 当然,”清单(accounting)”信息决定了活跃度(liveness)的计算.
      • 空的区域被移除并回收。现在计算所有区域的活跃度(Region liveness).
      • G1选择“活跃度(liveness)”最低的区域, 这些区域可以最快的完成回收. 然后这些区域和年轻代GC在同时被垃圾收集 . 在日志被标识为 [GC pause (mixed)]. 所以年轻代和老年代都在同一时间被垃圾收集
      • 所选择的区域被收集和压缩到下图所示的深蓝色区域和深绿色区域.

2.5方法区
  • 方法区在虚拟机启动的时候被创建,被视为堆内存的一个逻辑部分,但是方法区另有别名Non-Heap(非堆)。由于具体的JVM实现而不同,甚至在方法区不实现垃圾回收处理也是可以的。
  • 方法区(和堆内存一样)是可供各线程共享的运行时内存区域。用于存储(每一个类的结构信息)已被虚拟机加载的类信息、常亮、静态变量、即使编译器编译后的代码等数据。
  • 方法区可能发生如下异常情况:
    -如果方法区的内存空间不能满足内存分配请求,那 Java 虚拟机将抛出一个OutOfMemoryError 异常。
2.6运行时常量池
  • 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面了和符号引用,这部分内容将在类加载后进入到方法区的运行时常量池中存放。
  • 运行时常亮池相对于Class文件常量池一个重要的特征是具备动态性,运行期间也可能将新的常量放入到池中(例:String类的intern()方法)。
  • 在创建类和接口的运行时常量池时,可能会发生如下异常情况:
    • 当创建类或接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

3.Class文件结构
  • Class文件是一组以8位字节为基础单元的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,之间没有任何分隔符。当遇到需要占用8位字节以上空间的数据项,则会按照高位在前的方式分割成若干个8位字节进行存储。
  • 任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(例如类或接口也可以通过类加载器直接生成)。
  • Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,伪结构中只有两种数据类型:无符号数

    • 无符号数属于基本的数据类型(u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数),无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
    • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的以”_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。

3.1 魔数
  • 魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的Class文件。魔数值固定为 0xCAFEBABE,不会改变。
3.2 版本号
  • 主版本号(major_version)和次版本号(minor_version)在class文件中各占两个字节,次版本号占5、6字节,而主版本号占7、8字节。
  • JVM在加载class文件的时候,会读取出主版本号,然后比较class文件的版本号和JVM本身的版本号。JVM拒绝执行超过其版本号的Class文件。
3.3 常量池
  • 常量池计数器(constant_pool_count)
    常量池计数器的值等于constant_pool表中的成员数加1,计数是从1而不是0开始的。第0项常量空出来是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达”不引用任何一个常量池项目”的含义。Class文件结构中只有常量池的容量计数是从1开始的。
  • 常量池数据区(constant_pool[])

    • 常量池是一种表结构,它包含Class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。
    • 常量池中的每一项都具备相同的格式特征:第一个字节作为类型标记用于识别该项是哪种类型的常量,即:tagbyte。
    • 常量池的索引范围是1至constant_pool_count−1。常量池项的索引值只有在大于0且小于constant_pool_count时才会被认为是有效的。设计者将第0项常量空出来是为了满足某些指向常量池的索引值的数据在特定的情况下需要表达”不引用任何一个常量池项目”。

    • 常量池项(cp_info)结构
    • 每个常量池项(cp_info)都会对应记录着class文件中的某种类型的字面量,JVM虚拟机根据tag的值来确定是某个常量池项(cp_info)表示什么类型的字面量。

    • 常量池数据范围
    • ** int和float数据类型的常量在常量池中的表示和存储**
      将int和Float类型的常量分别使用CONSTANT_Integer_info和Constant_float_info表示,结构如下:

      场景:构建一个Java类,声明了五个变量,但是取值就两种:int类型的10和Float类型的11f,编译成class字节码文件后,通过javap -v IntAndFloatTest指令常量池信息,在常量池中只有一个常量10和一个常量11f。
      使用javap -v 指令能看到易于我们阅读的信息,查看真正的字节码文件可以使用HEXWin、NOTEPAD++、UtraEdit 等工具。

      代码中所有用到 int 类型 10 的地方,会使用指向常量池的指针值#8 定位到第#8 个常量池项(cp_info),即值为 10的结构体 CONSTANT_Integer_info,而用到float类型的11f时,也会指向常量池的指针值#23来定位到第#23个常量池项(cp_info) 即值为11f的结构体CONSTANT_Float_info

    • long和double数据类型的常量在常量池中的表示和存储
      long类型和double类型的数据类型占用8个字节的空间。在常量池中,将long和double类型的常量分别使用CONSTANT_Long_info和Constant_Double_info表示,结构如下:

      代码中所有用到long类型-6076574518398440533L的地方,会使用指向常量池的指针值#18定位到第#18个常量池项(cp_info),即值为-6076574518398440533L的结构体CONSTANT_Long_info,而用到double类型的10.1234567890D时,也会指向常量池的指针值#26来定位到第#26个常量池项(cp_info)即值为10.1234567890D的结构体CONSTANT_Double_info。

    • String类型的字符串常量在常量池中的表示和存储
      JVM会将字符串类型的字面量以UTF-8 编码格式存储到在class字节码文件中,CONSTANT_String_info结构体中的string_index的值指向了CONSTANT_Utf8_info结构体,而字符串的utf-8编码数据就在这个结构体之中。

      CONSTANT_String_info结构体位于常量池的第#15个索引位置。而存放”Java虚拟机原理” 字符串的 UTF-8编码格式的字节数组被放到CONSTANT_Utf8_info结构体中,该结构体位于常量池的第#16个索引位置。
    • 类文件中定义的类名和类中使用到的类在常量池中的组织和存储(CONSTANT_Class_info)
      Java类中所有使用到了的类的完全限定名二进制形式的完全限定名封装成CONSTANT_Class_info结构体中,然后将其放置到常量池里。CONSTANT_Class_info 的tag值为 7 。

      • 类的完全限定名: 定义了一个Java类ClassTest,并把它放到com.louis.jvm包下,则ClassTest类的完全限定名为com.louis.jvm.ClassTest。
      • 二进制形式完全限定名: JVM编译器将类编译成class文件后,是以二进制形式的完全限定名存储的,即它会把完全限定符的”.”换成”/” ,即在class文件中存储的 ClassTest类的完全限定名称是”com/louis/jvm/ClassTest”。因为这种形式的完全限定名是放在了class二进制形式的字节码文件中,所以就称之为 二进制形式的完全限定名。


      对于某个类而言,其class文件中至少要有两个CONSTANT_Class_info常量池项,用来表示自己的类信息和其父类信息。(除了java.lang.Object类除外,其他的任何类都会默认继承自java.lang.Object)如果类声明实现了某些接口,那么接口的信息也会生成对应的CONSTANT_Class_info常量池项。
      如果在类中使用到了其他的类,只有真正使用到了相应的类,JDK编译器才会将类的信息组成CONSTANT_Class_info常量池项放置到常量池中。将类信息放置到常量池中的目的,是为了在后续的代码中有可能会反复用到它。
      对于某个类或接口而言,其自身、父类和继承或实现的接口的信息会被直接组装成CONSTANT_Class_info常量池项放置到常量池中;
      类中或接口中使用到了其他的类,只有在类中实际使用到了该类时,该类的信息才会在常量池中有对应的CONSTANT_Class_info常量池项;
      类中或接口中仅仅定义某种类型的变量,JDK只会将变量的类型描述信息以UTF-8字符串组成CONSTANT_Utf8_info常量池项放置到常量池中,上面在类中的private Date date;JDK编译器只会将表示date的数据类型的“Ljava/util/Date”字符串放置到常量池中。

    • 类中引用到的field字段在常量池(CONSTANT_Fieldref_info, CONSTANT_Name_Type_info)
      在定义类的过程中会定义一些 field 字段,然后会在这个类的其他地方(如方法中)使用到它。将类中的Field封装成 CONSTANT_Fieldref_info常量池项,放到常量池中,在类中引用到它的地方,直接放置一个指向field字段所在常量池的索引。


      定义一个Person类,声明name和age两个field字段。使用javap -v Person指令,查看class文件信息,在Person类中引用到age和name field 字段的地方,都指向了常量池中age和name field字段对应的常量池项中。

      CONSTANT_Fieldref_info常量池项和其他项的关联关系

      扩展:字段描述符表示

    • 类中引用到的method方法在常量池(CONSTANT_Methodref_info, CONSTANT_Name_Type_info)
      CONSTANT_Methodref_info结构:

      定义一个方法getInfo(),调用了getName()和getAge()方法。这时候JVM编译器会将getName()和getAge()方法的引用信息包装成CONSTANT_Methodref_info结构体放入到常量池之中。
    • 类中引用接口中定义的method方法在常量池(CONSTANT_InterfaceMethodref_info)
      当在某个类中使用到了某个接口中的方法,JVM会将用到的接口中的方法信息放置到这个类的常量池中。JVM会使用CONSTANT_InterfaceMethodref_info结构体来描述。

      定义了一个Worker接口,和一个Boss类,在Boss类中调用了Worker接口中的方法,这时候在Boss类的常量池中会有Worker接口的方法的引用表示。对Boss.class执行javap -v Boss

      在Boss类的makeMoney()方法中调用了Worker接口的work()方法,机器指令是通过invokeinterface指令完成的,invokeinterface指令后面的操作数,是指向了Boss常量池中Worker接口的work()方法描述,表示的意思就是:“我要调用Worker接口的work()方法”
3.4 访问标志

JVM在编译某个类或者接口的源代码时,JVM会解析出这个类或者接口的访问标志信息,然后,将这些标志设置到 访问标志(access_flags)这16个位上。访问标志(access_flags)紧接着常量池后,占有两个字节,总共16位。

  • 每个定义的类或者接口都会生成class文件(这里也包括内部类,在某个类中定义的静态内部类也会单独生成一个class文件)。
  • 对于定义的类,JVM在将其编译成class文件时,会将class文件的访问标志的第11位设置为 1 。第11位叫做ACC_SUPER标志位;
  • 对于定义的接口,JVM在将其编译成class文件时,会将class文件的访问标志的第8位 设置为 1 。第8位叫做ACC_INTERFACE标志位;
  • JVM在编译接口的时候也会对class文件的访问标志上的ACC_ABSTRACT标志位设置为 1

    定义一个简单的类Simple.java,使用编译器编译成class文件,然后观察(UltraEdit)class文件中的访问标志的值,以及使用javap -v Simple 查看访问标志。

3.5 类索引

类索引的作用,就是为了指出class文件所描述的这个类叫什么名字。
一个Java类源文件经过JVM编译会生成一个class文件,也有可能一个Java类源文件中定义了其他类或者内部类,这样编译出来的class文件就不止一个,但每一个class文件表示某一个类,至于这个class表示哪一个类,便可以通过 类索引 这个数据项来确定。JVM通过类的完全限定名确定是某一个类。

以上面定义的Simple.class 为例,如下图所示,查看他的类索引在什么位置和取什么值。

3.6 父类索引

Java支持单继承模式,除了java.lang.Object 类除外,每一个类都会有且只有一个父类。class文件中紧接着类索引(this_class)之后的两个字节区域表示父类索引,跟类索引一样,父类索引这两个字节中的值指向了常量池中的某个常量池项CONSTANT_Class_info,表示该class表示的类是继承自哪一个类。

3.7 接口索引集合

一个类可以不实现任何接口,也可以实现很多个接口,为了表示当前类实现的接口信息,class文件使用了如下结构体描述某个类的接口实现信息:

由于类实现的接口数目不确定,所以接口索引集合的描述的前部分叫做接口计数器(interfaces_count),接口计数器占用两个字节,其中的值表示着这个类实现了多少个接口,紧跟着接口计数器的部分就是接口索引部分了,每一个接口索引占有两个字节,接口计数器的值代表着后面跟着的接口索引的个数。接口索引和类索引和父类索引一样,其内的值存储的是指向了常量池中的常量池项的索引,表示着这个接口的完全限定名。

3.8 字段表集合

字段表集合是指由若干个字段表(field_info)组成的集合。对于在类中定义的若干个字段,经过JVM编译成class文件后,会将相应的字段信息组织到一个叫做字段表集合的结构中,字段表集合是一个类数组结构
注意:这里所讲的字段是指在类中定义的静态或者非静态的变量,而不是在类中的方法内定义的变量

  • 字段表field_info结构体的定义
    字段表集合的结构是一个类似数组的结构,由于一个类中会定义若干个字段,JVM在编译类的时候,会将类中定义的字段的个数设到字段计数器中,然后将每一个字段信息组成每一个field_info的结构,依次存放在字段计数器之后。

  • field字段的访问标志
    field_info结构体,field字段的访问标志(access_flags)占有两个字节

  • 字段的数据类型表示和字段名称表示
    class文件对数据类型的表示

    注: class文件将字段名称和field字段的数据类型表示作为字符串存储在常量池中。在field_info结构体中,紧接着访问标志的,就是字段名称索引和字段描述符索引,它们分别占有两个字节,其内部存储的是指向了常量池中的某个常量池项的索引,对应的常量池项中存储的字符串,分别表示该字段的名称和字段描述符。

    • 属性表集合-静态field字段的初始化
      在定义field字段的过程中,会很自然地对field字段直接赋值
       
           
      1. public static final int MAX=100;
      2. public int count=0;
      对于虚拟机而言,上述的两个field字段赋值的时机是不同的:
    • 非静态(即无static修饰)的field字段的赋值将会出现在实例构造方法<init>()中
    • 静态的field字段,有两个选择:1、在静态构造方法<cinit>()中进行;2 、使用ConstantValue属性进行赋值

    Sun javac编译器对于静态field字段的初始化赋值策略
    如果使用final和static同时修饰一个field字段,并且这个字段是基本类型或者String类型的,那么编译器在编译这个字段的时候,会在对应的field_info结构体中增加一个ConstantValue类型的结构体,在赋值的时候使用这个ConstantValue进行赋值;如果该field字段并没有被final修饰,或者不是基本类型或者String类型,那么将在类构造方法()中赋值。

    即public static final init MAX=100; javac编译器在编译此field字段构建field_info结构体时,除了访问标志、名称索引、描述符索引外,会增加一个ConstantValue类型的属性表。

    定义一个简单的Simple类,然后通过查看Simple.class文件内容并结合javap -v Simple 生成的常量池内容,分析str field字段的结构:

 
 
  1. public class Simple {
  2. private transient static final String str ="This is a test";
  3. }

  1. 字段计数器中的值为0x0001,表示这个类就定义了一个field字段
  2. 字段的访问标志是0x009A,二进制是00000000 10011010,即第9、12、13、15位标志位为1,这个字段的标志符有:ACC_TRANSIENT、ACC_FINAL、ACC_STATIC、ACC_PRIVATE;
  3. 名称索引中的值为0x0005,指向了常量池中的第5项,为“str”,表明这个field字段的名称是str;
  4. 描述索引中的值为0x0006,指向了常量池中的第6项,为”Ljava/lang/String;”,表明这个field字段的数据类型是 java.lang.String类型;
  5. 属性表计数器中的值为0x0001,表明field_info还有一个属性表;
  6. 属性表名称索引中的值为0x0007,指向常量池中的第7项,为“ConstantValue”,表明这个属性表的名称是ConstantValue,即属性表的类型是ConstantValue类型的;
  7. 属性长度中的值为0x0002,因为此属性表是ConstantValue类型,它的值固定为2;
  8. 常量值索引 中的值为0x0008,指向了常量池中的第8项,为CONSTANT_String_info类型的项,表示“This is a test” 的常量。在对此field赋值时,会使用此常量对field赋值。
3.9 方法表集合

方法表集合是指由若干个方法表(method_info)组成的集合。对于在类中定义的若干个方法,经过JVM编译成class文件后,会将相应的method方法信息组织到一个叫做方法表集合的结构中,方法表集合是一个类数组结构。方法表集合紧跟在字段表集合的后面。

一个类中的method方法的表示,即method_info结构体的定义

  • 访问标志(access_flags)
    method_info结构体最前面的两个字节表示的访问标志(access_flags),记录这这个方法的作用域、静态or非静态、可变性、是否可同步、是否本地方法、是否抽象等信息,后面会详细介绍访问标志这两个字节的每一位具体表示什么意思。
  • 名称索引(name_index)
    紧跟在访问标志后面的两个字节称为名称索引(name_index),这两个字节中的值指向了常量池中的某一个常量池项,这个方法的名称以UTF-8格式的字符串存储在这个常量池项中。如public void methodName(),很然,“methodName”则表示着这个方法的名称,那么在常量池中会有一个CONSTANT_Utf8_info格式的常量池项,里面存着“methodName”字符串,而mehodName()方法的方法表中的名称索引则指向了这个常量池项。

  • 描述索引(descriptor_index)
    描述索引(descriptor_index)表示的是这个方法的特征或者说是签名,一个方法会有若干个参数和返回值,而若干个参数的数据类型和返回值的数据类型构成了这个方法的描述,其基本格式为:(参数数据类型描述列表)返回值数据类型。
    方法描述符索引(descrptor_index)是紧跟在名称索引后面的两个字节,这两个字节中的值跟名称索引中的值性质一样,都是指向了常量池中的某个常量池项。这两个字节中的指向的常量池项,是表示了方法描述符的字符串。
    所谓的方法描述符,实质上就是指用一个什么样的字符串来描述一个方法

    method_info结构体的名称索引中存储了一个索引值x,指向了常量池中的第x项,第 x项表示的是字符串”greeting”,即表示该方法名称是”greeting”;描述符索引中的y 值指向了常量池的第y项,该项表示字符串”()V”,即表示该方法没有参数,返回值是void类型。

  • 属性表(attribute_info)集合
    属性表(attribute_info)集合非常重要,方法的实现被JVM编译成JVM的机器码指令,机器码指令就存放在一个Code类型的属性表中;如果方法声明要抛出异常,那么异常信息会在一个Exceptions类型的属性表中予以展现。Code类型的属性表可以说是非常复杂的内容。
    属性表集合记录了某个方法的一些属性信息:

    • 这个方法的代码实现,即方法的可执行的机器指令
    • 这个方法声明的要抛出的异常信息
    • 这个方法是否被@deprecated注解表示
    • 这个方法是否是编译器自动生成

      属性表(attribute_info)结构体的一般结构

      • Code类型的属性表–method方法中的机器指令的信息
        Code类型的属性表(attribute_info)可以说是class文件中最为重要的部分,因为它包含的是JVM可以运行的机器码指令,JVM能够运行这个类,就是从这个属性中取出机器码的。除了要执行的机器码,它还包含了一些其他信息,如下所示

        • 机器指令-code
          JVM使用一个字节表示机器操作码,即对JVM底层而言,它能表示的机器操作码不多于2的 8 次方,即 256个。
        • 异常处理跳转信息-exception_table
          如果代码中出现了try{}catch{}块,那么try{}块内的机器指令的地址范围记录下来,并且记录对应的catch{}块中的起始机器指令地址,当运行时在try块中有异常抛出的话,JVM会将catch{}块对应懂得其实机器指令地址传递给PC寄存器,从而实现指令跳转;
        • Java源码行号和机器指令的对应关系-LineNumberTable属性表
          编译器在将java源码编译成class文件时,会将源码中的语句行号跟编译好的机器指令关联起来,这样的class文件加载到内存中并运行时,如果抛出异常,JVM可以根据这个对应关系,抛出异常信息,告诉我们我们的源码的多少行有问题,方便我们定位问题。
          这个信息不是运行时必不可少的信息,默认情况下,编译器会生成这一项信息。可以使用-g:none 或-g:lines来取消或者要求设置这一项信息。如果使用了-g:none来生成class文件,class文件中将不会有LineNumberTable属性表,造成的影响就是 将来如果代码报错,将无法定位错误信息报错的行,并且如果项调试代码,将不能在此类中打断点(因为没有指定行号。)
        • 局部变量表描述信息-LocalVariableTable属性表
          局部变量表信息会记录栈帧局部变量表中的变量和java源码中定义的变量之间的关系。这个信息不是运行时必须的属性,默认情况下不会生成到class文件中。可以根据javac指令的-g:none或者-g:vars选项来取消或者设置这一项信息。
          当我们使用IDE进行开发时,如果在项目中引用到了第三方的jar包,而第三方的包中的class文件中有无LocalVariableTable属性表的区别如下所示:
          • attribute_name_index,属性名称索引,占有2个字节,其内的值指向了常量池中的某一项,该项表示字符串Code。
          • attribute_length,属性长度,占有4个字节,其内的值表示后面有多少个字节是属于此Code属性表的;
          • max_stack,操作数栈深度的最大值,占有2个字节,在方法执行的任意时刻,操作数栈都不应该超过这个值,虚拟机的运行的时候,会根据这个值来设置该方法对应的栈帧(Stack Frame)中的操作数栈的深度;
          • max_locals,最大局部变量数目,占有2个字节,其内的值表示局部变量表所需要的存储空间大小
          • code_length,机器指令长度,占有4个字节,表示跟在其后的多少个字节表示的是机器指令
          • code,机器指令区域,该区域占有的字节数目由code_length中的值决定。JVM最底层的要执行的机器指令就存储在这里
          • exception_table_length,显式异常表长度,占有2个字节,如果在方法代码中出现了try{} catch()形式的结构,该值不会为空,紧跟其后会跟着若干个exception_table结构体,以表示异常捕获情况
          • exception_table,显式异常表,占有8个字节,start_pc,end_pc,handler_pc中的值都表示的是PC计数器中的指令地址。exception_table表示的意思是:如果字节码从第start_pc行到第end_pc行之间出现了catch_type所描述的异常类型,那么将跳转到handler_pc行继续处理。
          • attribute_count,属性计数器,占有2个字节,表示Code属性表的其他属性的数目
          • attribute_info,表示Code属性表具有的属性表,它主要分为两个类型的属性表:“LineNumberTable”类型和“LocalVariableTable”类型。
          • LineNumberTable类型的属性表记录着Java源码和机器指令之间的对应关系
          • LocalVariableTable类型的属性表记录着局部变量描述

        定义Simple.java类

         
               
        1. public class Simple {
        2. public static synchronized final void greeting(){
        3. int a = 10;
        4. }
        5. }
    **使用javac -g:none Simple.java 编译出Simple.class 文件** 如果在类中没有定义实例化构造方法,JVM编译器在将源码编译成class文件时,会自动地为这个类添加一个不带参数的实例化构造方法,这种添加是字节码级别的,JVM对所有的类实例化构造方法名采用了相同的名称:“<init>”。如果显式地如下定义Simple()构造函数,这个类编译出来的class文件和上面的不带Simple构造方法的Simple类生成的class文件是完全相同的。 除了实例化构造方法,JVM还会在特殊的情况下生成一个叫类构造方法”<cinit>()”。如果在类中使用到了static修饰的代码块,那么,JVM会在class文件中生成一个“<cinit>()”构造方法。
    • Simple.class中的<init>()方法
    1. 方法访问标志(access_flags): 占有 2个字节,值为0x0001,即标志位的第 16 位为 1,所以该<init>()方法的修饰符是:ACC_PUBLIC;
    2. 名称索引(name_index): 占有 2 个字节,值为 0x0004,指向常量池的第 4项,该项表示字符串“<init>”,即该方法的名称是“<init>”;
    3. 描述符索引(descriptor_index): 占有 2 个字节,值为0x0005,指向常量池的第 5 项,该项表示字符串“()V”,即表示该方法不带参数,并且无返回值(构造函数确实也没有返回值);
    4. 属性计数器(attribute_count): 占有 2 个字节,值为0x0001,表示该方法表中含有一个属性表,后面会紧跟着一个属性表;
    5. 属性表的名称索引(attribute_name_index):占有 2 个字节,值为0x0006,指向常量池中的第6 项,该项表示字符串“Code”,表示这个属性表是Code类型的属性表;
    6. 属性长度(attribute_length):占有4个字节,值为0x0000 0011,即十进制的 17,表明后续的 17 个字节可以表示这个Code属性表的属性信息;
    7. 操作数栈的最大深度(max_stack):占有2个字节,值为0x0001,表示栈帧中操作数栈的最大深度是1;
    8. 局部变量表的最大容量(max_variable):占有2个字节,值为0x0001, JVM在调用该方法时,根据这个值设置栈帧中的局部变量表的大小;
    9. 机器指令数目(code_length):占有4个字节,值为0x0000 0005,表示后续的5 个字节 0x2A 、0xB7、 0x00、0x01、0xB1表示机器指令;
    10. 机器指令集(code[code_length]):这里共有 5个字节,值为0x2A 、0xB7、 0x00、0x01、0xB1;
    11. 显式异常表集合(exception_table_count): 占有2 个字节,值为0x0000,表示方法中没有需要处理的异常信息;
    12. Code属性表的属性表集合(attribute_count): 占有2 个字节,值为0x0000,表示它没有其他的属性表集合,因为我们使用了-g:none 禁止编译器生成Code属性表的 LineNumberTable 和LocalVariableTable;
    • Simple.class中的greeting()方法
    1. 方法访问标志(access_flags):占有2个字节,值为 0x0039 ,即二进制的00000000 00111001,即标志位的第11、12、13、16位为1,根据上面讲的方法标志位的表示,可以得到该greeting()方法的修饰符有:ACC_SYNCHRONIZED、ACC_FINAL、ACC_STATIC、ACC_PUBLIC;
    2. 名称索引(name_index):占有2个字节,值为 0x0007,指向常量池的第7项,该项表示字符串“greeting”,即该方法的名称是“greeting”;
    3. 描述符索引(descriptor_index): 占有 2 个字节,值为0x0005,指向常量池的第 5 项,该项表示字符串“()V”,即表示该方法不带参数,并且无返回值;
    4. 属性计数器(attribute_count): 占有 2 个字节,值为0x0001,表示该方法表中含有一个属性表,后面会紧跟着一个属性表;
    5. 属性表的名称索引(attribute_name_index):占有2个字节,值为0x0006,指向常量池中的第6项,该项表示字符串“Code”,表示这个属性表是Code类型的属性表;
    6. 属性长度(attribute_length):占有4个字节,值为0x0000 0010,即十进制的16,表明后续的16个字节可以表示这个Code属性表的属性信息;
    7. 操作数栈的最大深度(max_stack):占有2个字节,值为0x0001,表示栈帧中操作数栈的最大深度是1;
    8. 局部变量表的最大容量(max_variable):占有2个字节,值为0x0001, JVM在调用该方法时,根据这个值设置栈帧中的局部变量表的大小;
    9. 机器指令数目(code_length):占有4 个字节,值为0x0000 0004,表示后续的4个字节0x10、 0x0A、 0x3B、0xB1的是表示机器指令;
      10.机器指令集(code[code_length]):这里共有4 个字节,值为0x10、 0x0A、 0x3B、0xB1 ;
    10. 显式异常表集合(exception_table_count): 占有2 个字节,值为0x0000,表示方法中没有需要处理的异常信息;
    11. Code属性表的属性表集合(attribute_count): 占有2 个字节,值为0x0000,表示它没有其他的属性表集合,因为我们使用了-g:none 禁止编译器生成Code属性表的 LineNumberTable 和LocalVariableTable;

      • Exceptions类型的属性表–method方法声明的要抛出的异常信息
        定义Interface接口,抛出Exception异常
         
               
        1. public interface Interface {
        2. public void sayHello() throws Exception;
        3. }

    Exceptions类型的属性表(attribute_info)结构体

    如果某个方法定义中,没有声明抛出异常,那么,表示该方法的方法表(method_info)结构体中的属性表集合中不会有Exceptions类型的属性表;即如果方法声明了要抛出的异常,方法表(method_info)结构体中的属性表集合中必然会有Exceptions类型的属性表,并且该属性表中的异常数量不小于1。
    假设异常数量中的值为N,那么后面的异常名称索引的数量就为N,它们总共占有的字节数为N*2,而异常数量占有2个字节,那么将有下面的这个关系式:

    • 属性长度(attribute_length)中的值= 2 + 2*异常数量(number_of_exceptions)中的值
    • Exceptions类型的属性表(attribute_info)的长度=2+4+属性长度(attribute_length)中的值

      Interface接口类编译成class文件,并辅助javap -v Inerface 查看常量池信息

    1. 方法计数器(methods_count)中的值为0x0001,表明其后的方法表(method_info)就一个,即我们就定义了一个方法,其后会紧跟着一个方法表(method_info)结构体;
    2. 方法的访问标志(access_flags)的值是0x0401,二进制是00000100 00000001,第6位和第16位是1,对应上面的标志位信息,可以得出它的访问标志符有:ACC_ABSTRACT、ACC_PUBLIC。细心的读者可能会发现,在上面声明的sayHello()方法中并没有声明为abstract类型啊。确实如此,这是因为编译器对于接口内声明的方法自动加上ACC_ABSTRACT标志。
    3. 名称索引(name_index)中的值为0x0005,0x0005指向了常量池的第5项,第五项表示的字符串为“sayHello”,即表示的方法名称是sayHello
    4. 描述符索引(descriptor_index)中的值为0x0006,0x0006指向了常量池中的第6项,第6项表示的字符串为“()V” 表示这个方法的无入参,返回值为void类型
    5. 属性表计数器(attribute_count)中的值为0x0001,表示后面的属性表的个数就1个,后面紧跟着一个attribute_info结构体;
    6. 属性表(attribute_info)中的属性名称索引(attribute_name_index)中的值为0x0007,0x0007指向了常量池中的第7 项,第 7项指向字符串“Exceptions”,即表示该属性表表示的异常信息;
    7. 属性长度(attribute_length)中的值为:0x00000004,即后续的4个字节将会被解析成属性值;
    8. 异常数量(number_of_exceptions)中的值为0x0001,表示这个方法声明抛出的异常个数是1个;
    9. 异常名称索引(exception_index_table)中的值为0x0008,指向了常量池中的第8项,第8项表示的是CONSTANT_Class_info类型的常量池项,表示“java/lang/Exception”,即表示此方法抛出了java.lang.Exception异常。

转载于:https://www.cnblogs.com/icyfumanix/p/6504581.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值