深入理解JVM虚拟机-周志明【第三版】

          概述

一、走进虚拟机

二、自动内存管理

三、垃圾收集器与内存回收策略

四、虚拟机性能监控、故障处理工具

五、调优案例分析与实战

六、类文件结构

七、虚拟机类加载机制

概述

Java 技术系: Kotlin 、Clojure 、JRuby、Groovy 均是运行在 Java 虚拟机上的程序语言

我们通常把Java 程序设计语言、Java虚拟机、Java 类库 三部分统称为 JDK

Java 前生叫做Oak , 1995 年改名Java,并发布第一个正式环境 JDK1.0

2004  jdk5 发布,将版本的命名分格改变,语法有较大的改变

2014 jdk8 发布 支持 Lambda 表达式、移除 HotSpot 的永久代

一、走进虚拟机

最原始的虚拟机 sun  Classic /Exact VM

Classic 虚拟机特点:无法执行即使编译,通过外挂的方式可以即时编译就会完全托管编译

Exact Vm : 精准的内存管理

HotSpot VM : 继承前两款虚拟机的优点,用于独特的热点代码探测技术

Mobile/Embedded Vm :用于嵌入式

BEA  JRockit: 号称速度最快,后被收购

IBM J9 VM

软硬合璧 BEA Liquid VM / Azul VM :与特定的硬件平台绑定

等等

二、自动内存管理

 参考:https://www.jianshu.com/p/76959115d486

1.程序计数器

Java 多线程之间切换,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各个线程之间的计数器互不影响

2.虚拟机栈 / 栈

栈的生命周期与线程相同,为线程私有,Java 虚拟机执行方法就会同步创建栈帧 Stack Frane ,用于存储局部变量表、操作数栈、动态连接、方法出口

局部变量表存储了编译期可知的虚拟机基本数据类型、对象引用(可能是对象地址、也可能是代表对象的句柄)、返回地址,这些局部变量在局部变量表中以局部变量槽Slot 来表示。

64 位的 long 与 double 会占用 2 个局部变量槽

3.本地方法栈

虚拟机栈是为虚拟机执行Java 方法服务,而本地方法栈是为虚拟机执行本地方法服务

4.堆

按照 HotSpot 虚拟机来说,会分为 新生代、老年代、永久代、Eden 空间、From Survivor 、To Survivor 等空间,其原因是因为GC 回收都基本在堆上进行的,按垃圾收集行为来区分的。而现在垃

圾收集器与过去有较大的不同,有点虚拟机gc 都不是采用分代收集  (举个栗子)多线程下为了更好的分配对象,线程共享的堆可以划分出多个线程私有的分配缓冲区 (Thread Local Allocation

Buffer),以提升对象分配时的效率

5.方法区 Method Area

属于线程共享,用于存储被虚拟机加载的类型信息、常类、静态变量、即使编译器编译后的代码缓存数据(别名:非堆、永久代),在这个区也存在垃圾回收,主要针对常量池的回收与类型的卸载

6.运行时常量池 Runtime Constant Pool

运行时常量池是方法区的一部分,Class 文件中出了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,

这部分内容在类加载后存放到方法区的运行时常量池中

7.直接内存 Direct Memory

直接内存不是虚拟机运行时数据区的一部分,但也被频繁的使用,而且也可能导致 OutOfMemoryError 异常。在 JDK 1.4 加入 NIO( New Input/OutPut)类,引入一种基于通道 Channel 与 缓冲区

Buffer 的IO 方式,它可以使用 Native 函数库直接直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。在一些场景中提高了性能,比避免

了在Java 堆与 Native堆中来回复制数据。虽然该内存不受Java 堆内存限制,但是收到机器内存的限制,也可能导致内存溢出

对象的创建

  当Java 虚拟机遇到一条字节码 new 指令时,首先检查这个指令的参数是否能够在常量池中定位到一个类的符号引用,并且检查这个符号引用所代表的类是否已经被加载、解析、初始化过。

如果没有,则必须执行相应的类加载过程。

  在类加载检查通过后,接下来虚拟机将为新对象分配内存,对象所需内存大小在类加载完成后便可完全确定,为对象分配空间的任务实际上等同于把一块确定大小的内存从Java 堆中划分出出来。

假设Java 堆是绝对规整的,所有使用过的对象放在一边,未使用的对象放在另一边,中间方着一个指针作为分界线的指示器,分配对象就是把指针往空闲方向挪动一块与对象大小相等的距离,这种

分配方式称为指针碰撞 (Bump the Pointer).但如果Java堆的内存并不是规整的,已近使用的内存与未使用的内存交错放在一起,虚拟机必须维护一个列表,记录那些内存是空闲的,在分配的时候

从列表中找到一块足够大的空间分配给实例,并更新列表上的记录,这种分配方式称为空间列表(Free List)。选择哪种分配方式是由Java 堆是否规整决定的,Java 堆是否规整则由垃圾收集器是否

带有空间压缩整理(Compact)的能力决定的。而使用 Serial 、ParNew 等带有压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,简单高效。而使用 CMS 这种基于清除算法的收集器时,

理论上只能采用较为复杂的空闲列表来分配内存。

  还需要考虑另一个问题是对象在虚拟机中创建是非常频繁的行为,即使是修改一个指针指向的位置,在并发的情况下可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同事使用了原

来的指针分配内存。解决这个问题有两种方案: 一 堆分配内存空间进行同步处理--实际上虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作划分在不同的空间

中进行,即为每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer TLAB),只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用

TLAB 可以通过 -XX: +/-UseTLAB 设定

  内存分配完,虚拟机必须将分配的内存空间(不包括对象头)都初始化为零值,如果使用TLAB ,则这一项提前到 TLAB 分配时进行。

  Java 虚拟机还要对对象头进行必要的设计,如对象是哪个类的实例、如何才能找到元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用 Object::hashCode() 方法时才计算),对象

的GC 分代年龄等信息,是否启动偏向锁等。这些信息存在对象头中,Object Header.

  上面的工作完成后,从虚拟机的角度,一个新的对象已经产生了。单从Java 的角度,对象的创建才刚开始--构造函数。 Class 文件 <init>方法还没执行,所有的字段为默认零值,对象需要按照意图

构造好。

 对象的内存布局

  在HotSpot 虚拟机里,对象在堆内存中存储的布局可以划分为三部分:对象头(Header)、实例数据(Instance Data)、和对齐填充(Padding)

  HotSpot 虚拟机对象的对象头部分存储两类信息,第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄锁状态标志、线程持有锁、偏向锁ID、偏向时间戳等。这部分数据

在长度32位和64位的虚拟机(未开启压缩指针)中分别是 32 个比特 、64个比特。官方统称位 Mark Word 。对象需要存储的运行时数据很多,其实已经超过32、64位所能记录的最大长度,但对象头里的

信息是与对象自身定义的数据无关的额外成本,考虑到虚拟机的空间效率,MarkWord 的32位比特存储空间中: 25 个用于存储对象哈希码、4个用于存储对象分代年龄、2个用与存储锁标志位、1个比特固

定为 0 ,如下图

        

   对象头的另一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。并不一定所有的虚拟机实现都必须在对象上保留类型指针,换句话说是

查找对象元数据信息并不一定要经过对象本身。如果对象是Java 数组,那么对象头中还必须有一块用于记录数组长度的数据。

  接下来实例数据部分才是对象真正存储的有效信息,即在代码里定义的各种类型的字段内容、无论从父类继承下来还在字类定义的字段都必须记录下来,这部分的存储顺序会收到虚拟机分配策略参数

和字段在Java 代码定义顺序影响。HotSpot 虚拟机默认的分配顺序: long/double 、int、shorts/chars、byte/boolean

  对象的第三部分是对齐填充,这并不是必然存在的,它仅仅起着占位符的作用。由于 HotSpot 虚拟机的自动内存管理系统要去对象的起始地址必须是8字节的整数倍,对象头的部分已经是整数倍,如果

没有实例数据,将不需要对齐填充

对象的访问定位

  访问对象通过栈上的reference 数据来操作具体的对象,访问的方式由虚拟机实现而定的,主流的访问方式主要有使用句柄直接指针

  如果使用句柄访问,Java堆将可能会划分出一块内存作为句柄池,reference 中存储的地址就是句柄地址,而句柄中包含了对象实例数据和类型数据具体的地址

       

   如果使用直接指针访问,Java 堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference 中存储的地址是对象地址,如果是访问对象本身的话,就不需要多一次间接的访问

      

 使用直接指针的方式的好处是速度快,节省了一次指针定位的时间开销,由于访问对象多,中间会消耗可观的时间成本

三、垃圾收集器与内存分配策略

3.1 对象是否已经死亡

引用计数:在对象中添加一个引用计数器,每当有一个地方引用就将计数器加一,引用失效就减一,当计数器为零表示对象不再被使用。该算法原理简单 ,效率高,但是无法解决循环引用的,

其中微软的 COM 技术Python语言就是使用了引用技术作为内存关联

可达性分析Java 、C# 、Lisp 使用的都是可达性分析算法,通过一系列的GC roots 的根对象作为起点,根据引用向下搜索,搜索走过的路径称为引用链,如果某个对象与 GC roots没人任何引用

链,则认为是不可达的也就是不能被使用的。可以作为GC root 的节点:

  1. 栈帧中本地变量表引用的对象
  2. 方法区静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈引用的对象
  5. 虚拟机内部引用的对象,如常用的异常对象
  6. 同步锁持有的对象

3.2 再谈引用

jdk1.2 之后,Java 对引用进行扩充,分为强引用、软引用、弱引用、虚引用

强引用 例如new Object ,任何情况下,只要强引用关系存在,就不会有垃圾回收及收集

软引用 软引用是早内存溢出之前,将软引用的对象列入二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常

弱引用 弱引用的对象只能生存到下一次垃圾收集器发生为止

虚引用 也叫作幽灵引用。是最弱的引用,为一个对象设置虚引用唯一的目的是该对象回收会收到一个系统通知

  在可达性分析算法中判断对象不可达,也并非”非死不可“,需要经过2次的标记: 如果在可达性分析后发现对象没有引用链,它将会被第一次标记,随后进行一次筛选,筛选的条件是对象是否有必要执行

finalize 方法。没必要执行:对象没有覆盖 finalize() 方法,finalize() 方法已经被虚拟机调用过。

  如果对象有必要执行finalize() 方法,将会被放置在一个 F-Queue的队列中,由一条低优先级的线程执行他们的 finalize 方法。如果对象在被回收期重新被引用,那么第二次回收将被移除队列。如果这时对象

还没有逃脱,那么将被回收。

3.3 回收方法区

  方法区的回收主要是两部分内容:废弃的常量不能再使用的类型

如果常量池中某常量没有任何引用,那么发送回收就会被清理。判断一个类是否被回收,需要满足一下条件

  1. 所有该类的实例都被回收了
  2. 加载该类的类加载器被回收
  3. 该类对应的Java.lang.Class 对象没有任何地方被引用,无法通过反射获取该类的方法

3.4 垃圾收集算法

当前的虚拟机的垃圾收集器,大多都遵循分代收集的理论,它是建立在2个假说之上

  1. 弱分代假说 绝大多数对象都是朝生夕灭
  2. 强分代假说  熬过多次垃圾回收的对象越难以被回收

收集器将Java 堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域中。因此垃圾器才可以每次只回收其中的某些部分的区域,因此才有了 Minor GC 、Major GC、Full GC 回收分类的划分,

才能够安排某些不同区域与对象死亡特征相匹配的收集算法。因而发展出收集算法有:

  • 标记-复制
  • 标记-清除
  • 标记-整理

在分代收集中,夜班会将堆分为:新生代、老年代。 新生代每次垃圾回收都会有大量对象死去,而每次存活的对象将逐步晋升到老年代(缺点:对象之间存在跨代引用

  1. 增加第三条假说:跨代引用相对同代引用来说仅占极少数

例如:老年代中引用了新生代对象,导致新生代对象无法被回收,最后也进入老年代,增加系统的消耗

标记-清除算法

标记清除算法最早1960 Lisp 之父提出,分为标记阶段、清除阶段。首先标记需要回收的对象,标记完统一回收。主要缺点:执行效率不稳定(内存中如果有大量需要被回收的对象,导致标记与回收效率逐渐降低),

第二个缺点:标记清除后会产生大量不连续的内存碎片(将导致有大对象需要创建,没有足够大的空间而触发GC)

标记-复制算法

简称复制算法,1969 年提出,将可用的内存划分为大小相等的2部分,每次只用其中的一部分,当用完就将存活的对象复制到另一半,删除清理之前的一半。缺点:将产生大量的内存间的复制开销,可用内存减半

(现在改算法适应于新生代,因为新生代的对象98% 不能存活过第一轮收集),hotspot 虚拟机将将分为较大的Eden 空间、2块较小的survivor ,比例为 8:1:1 ,因次只有10%的空间是被浪费的。虽然新生代98%

的对象被回收,但是也不一定是绝对的,垃圾回收将Eden + 1块survivor 空间存活的对象复制到另一块 survivor ,当不足容纳时,需要进行分配担保,无法保留的对象将直接进入老年代

标记-整理算法

针对老年代的特征,1974年提出标记-整理算法,其中标记过程与标记-清除算法一样,后续步骤不是直接删除对象,而是将存活的对象整理到一边,然后清理掉剩余的对象。

相对于标记清除,标记整理的特点是移动的,优缺并行。

  不一定对象停顿时间短,甚至不停顿,但从整体程序的吞吐量,移动的标记整理更划算。HotSpot 虚拟机里关注吞吐量的 Parallel Scavenge 收集器是基于标记-整理算法的。而关注低延时的CMS 收集器则

基于标记-清除算法。

3.5 HotSpot 算法细节实现

 根节点枚举: 可达性分析算法中一系列 GC Roots 集合找引用链的过程,都必须暂停用户线程。Java程序越来越大,gc root 数据也是越来越大,使用可达性分析逐个扫描会消耗很大的时间。HotSpot 虚拟机

实现直接定位对象存储位置使用 OopMap 的数据结构。

安全点:在 OopMap 的协助下,HotSpot 可以快速的完成根节点枚举,但是其并没有为每一条指令都生成 OopMap ,在特定的位置记录信息,这些位置称为安全点。

安全区域:安全点保证程序执行时,安全区域确保在代码中,引用关系不会发生变化,在该区域中任意地方开始垃圾回收都是安全的

3.6 经典的垃圾收集器

 

 插入:jdk1.8 查看使用默认的垃圾收集器

Java  -XX:+PrintGCDetails  -XX:+PrintCommandLineFlags
Heap
 PSYoungGen      total 37888K, used 2621K [0x00000000d6100000, 0x00000000d8b00000, 0x0000000100000000)
  eden space 32768K, 8% used [0x00000000d6100000,0x00000000d638f7d0,0x00000000d8100000)
  from space 5120K, 0% used [0x00000000d8600000,0x00000000d8600000,0x00000000d8b00000)
  to   space 5120K, 0% used [0x00000000d8100000,0x00000000d8100000,0x00000000d8600000)
 ParOldGen       total 86016K, used 0K [0x0000000082200000, 0x0000000087600000, 0x00000000d6100000)
  object space 86016K, 0% used [0x0000000082200000,0x0000000082200000,0x0000000087600000)
 Metaspace       used 3262K, capacity 4554K, committed 4864K, reserved 1056768K
  class space    used 367K, capacity 386K, committed 512K, reserved 1048576K

可以看到上述的结果,PSYoungGen 表示由 Parallel Scavenge 垃圾收集器管理新生代 , ParOldGen 表示由 Parallel Old 管理老年代

 比较广泛使用的收集器是 CMS 与 G1

经典的收集器

Serial 收集器

最基础的收集器,在 jdk 1.3.1 之前HotSpot虚拟机新生代收集器。为单线程收集器,单线程的含义是其进行垃圾收集时必须暂停其他所有的线程

 从Serial 收集器到 Parallel 收集器,再到 Concurrent Mark Sweep(CMS)、Grabage First(G1) 收集器,最终至 ShenandoahZGC 

ParNew 收集器

ParNew收集器本质上是 Serial 收集器的多线程并行版本

其最为新生代的收集器,可与老年代收集器 CMS 搭配使用。但随着发展,G1 收集器可以收集全栈

Parallel Scavenge 收集器

parallel scavenge 收集器是一款新生代的收集器,基于标记-复制算法的收集器,也能够支持并行。CMS 收集器的目标是减少垃圾收集导致用户的停顿时间,而 Parallel Scavenge 目的是达到一个可控的吞吐量

虚拟机参数:-Xmn 新生代大小 

Serial Old 收集器

单线程的老年代收集器,使用标记-整理算法

Parallel Old 收集器

是parallel scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。是jdk 1.6 才开始提供

CMS 收集器

Concurrent Mark Sweep 收集器是以获取最短回收停顿时间为目标的收集器,运行过程包括

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

过程只有初始标记与重新标记需要停顿。耗时较长的并发标记与并发清除可以与用户线程一起工作,所以整体停顿时间较短。、

CMS 是基于标记-清除算法实现的收集器,(容易产生碎片)

Garbage First 收集器

简称G1 收集器,作为垃圾收集器发展史的里程碑,是第一款面向全局的收集器。在jdk9 之中,由G1 收集器代替Parallel Scavenge 与 Parallel Old 收集器。称为默认的收集器。

G1 收集器不再固定的收集那块区域,而是将堆内存划分为若干相对的区域,基于Region 的堆内存布局,回收性价比最高的区域。该模式也称为 Mixed GC 模式

G1 收集器运行示意图

低延迟垃圾收集器

Shenandoah

shenandoah 是 RedHat 公司发展的新型收集器,后贡献 openjdk ,目标是任何情况下都把垃圾收集时间控制在10毫秒以内,其不仅要进行并发的垃圾标记,还要并发的进行对象清理后的整理。

也是使用 Region 的堆内存布局,回收策略也是优先回收价值最大的(该回收期并没有分代收集,不会区分新生代、老年代)

ZGC 收集器

 z Garbage collection, 是在 jdk11 中加入的特性,是Oracle 公司研发的。目标也是尽可能的在不影响吞吐量的情况下,降低堆内存的垃圾回收停顿时间

垃圾收集器参数总结

在大多数情况下,对象优先在 Eden 区分配,当Eden 没有足够空间分配时,虚拟机将发起一次 minor  gc. 

大对象直接进入老年代,长期存活的对象进入老年代

在发生 minor gc 前,虚拟机必须检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果条件成立,那么minor gc 可以确保是安全的,否则,虚拟机查询是否允许担保失败,

如果允许,则继续检查老年代最大可用的连续空间是否大于历次晋升到老年代的平均大小,如果大于,则进行一个 minor gc ,尽管是有风险的。如果小于,或者不允许冒险,则进行一次Full  GC

四、虚拟机性能监控、故障处理工具

五、调优案例分析与实战

六、类文件结构

 6.1 Class 类文件结构

Class 文件是一组以8字节为基础的二进制流,Class 文件以无符号数组成。

 当用于描述若干个数量不定的数据时,经常会用一个前置的容量计数器与若干个连续的数据项形式,Class 文件没有任何分隔符

6.2 魔术与版本号

      每个 Class 文件的头4个字节被称为魔术(Magic Number),唯一的作用是确认这个文件能否被虚拟机接收的Class 文件。这里采用魔数而非扩展名来识别,主要是基于安全的考虑。

OxCAFEBABE ,固定的魔数,十六进制标识,长度为 4B 。

  紧接着魔数的4个字节存储的是Class 文件的版本号,前2个字节存储次版本号(Minor Version),后2个字节存储主版本号(Major Version)。Java 的版本号是从45 开始。虚拟机规范

,高版本的 jdk 能够向下兼容以前版本的 Class 文件,但是不能允许以后版本的 Class 文件,Class 文件校验部分明确要求了即使文件格式未发送任何变化,虚拟机也必须拒绝执行超过其

版本号的Class 文件。

jdk    版本号

1.x       45 

1.2x    45~46

1.3x    45~47

1.4x    45~48

1.5x    45~ 49

1.6x    45~50

1.7x    45~51

1.8x    45~52

6.3 常量池

主次版本号之后是常量池入口,常量池的数量不少不定的,所以在常量池入口需要一项 2u 类型数据,代表常量池计数器。与Java语言习惯不同,常量池计数器从 1 开始。

如下述,0x0016 ,即十进制 22 ,标识常量池有21 项常量,所以范围为 1~21 .

 常量池主要存储:字面量 与 符号引用。字面量如文本字符串,被声明为 final 的常量值。符号引用属于编译原理方面的概念,主要包括下几类常量:

  1. 被模块导出或者开放的包
  2. 类和接口的全限定名
  3. 字段的名称和描述符
  4. 方法的名称和描述符
  5. 方法句柄和方法类型
  6. 动态调用点和动态常量

当虚拟机在做类加载时,常会从常量池获取对应的符号引用,再在类创建时会运行时解析,翻译到具体的内存地址中。

常量池的每一项常量都是一个表,截至jdk 13 ,常量表中分别有17 种不同类型的常量

6.4 访问标志位

在常量池结束后,紧接着2个字节代表访问标志位(access_flag),这个标志用于识别一些类或者接口层次的访问信息,包括:这个class 是类还是接口,是否定义为 public 类型,是否定义为 abstract 类型。

如果是类,是否被申明为final

 

 access_flags 中共有16个标志位可以使用。

6.5 类索引、父类索引、与所有集合

类所以 this_class  父类所有 super_class 都是一个 u2 类型的数据,接口索引集合是一组 u2 类型的数据的集合。

类索引确定类的全限定名,父类索引确定这个类的父类的全限定名。类所以与父类索引是两个 u2 类型的索引值,以下是查询类全限定名的过程

 接口索引,入口项是一个 u2 类型的接口计数器,标识索引的容量,如果类没有实现任何接口,则计数器值为 0 

6.6 字段表集合

  字段表(field_info)用于描述接口或者类中声明的变量。 Java 中的字段包括类变量和实例变量,单不包括在方法内部申明的变量。字段表可以修饰的包括

  1. 作用域 public private protected
  2. 类/实例 static
  3. 可变性 final
  4. 并发现 volatile
  5. 能否序列化 transient
  6. 字段数据类型

字段表结构

 字段访问标志

访问权限 acc_public 、acc_private、acc_protected 三个标志最多只能选其一

acc_final 、acc_volatile 不能同时选择

接口必须包含 acc_public 、acc_static、acc_final

在acc_flag 之后是 name_index ,descriptor_index ,他们都是对常量池的引用,分别代表字段的简单名称以及字段和方法的描述符。

现在简单的区别一下 描述符 、简单名称、全限定名

全限定名和简单名称,例如 org/apache/TestClass,这是这个类id全限定名,只是把 . 换成了 / ,为了使多个全限定名区别,最后一般会加入一个 ; 

简单名称就是指没有类型和参数修改的方法或者字段名称,例如inc()方法和 m 字段的简单名称 inc , m

描述符相对复杂一点,描述符的作用是用来描述字段的数据类型、方法的参数列表(数量、类型、顺序)和返回值。描述符的规则对于基本数据类型使用大写字母表示,而对象使用字符L j加对象的全限定名表示

 对于数组类型,将维度前加 [ ,二维 [[ 

描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表严格按照顺序。

例如 void inc() 描述为   ()V

6.7 方法表集合

Class 文件存储格式中对方法的描述与对字段的描述几乎是完全一致的方式,方法表的结构依次是访问标志位(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)

 因为volatile 关键字和 transient 关键字不能修饰方法,所以方法表的标志中没有了 ACC_VOLATILE 和 ACC_TRANSIENNT 。方法表可取参数值如下

 方法的顶义可以通过访问标志、名称所以、描述符索引访问,但是方法里的代码存储在哪里呢?方法里的Java 代码经过 Javac 编译器编译成字节码指令之后,存放在方法属性表集合中名为 Code 的属性里。

 在Java 中要重载一个方法,除了要与原方法具有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名。

注:特征签名分为字节码层面的方法特征签名以及Java 层面的方法特征签名,Java 代码的方法特征签名只包含方法名称、参数顺序、参数类型,而字节码层面的特征签名还包括方法返回值以及受查异常表

所以在Java 语言里无法通过返回值来区别方法重载,但是在 Class 文件格式中,仅仅返回值不同也可以使得方法共存在一个 class 文件中

6.8 属性表

Class 文件,字段表,方法表都可以携带自己的属性表集合,以描述某些场景专有的信息,属性表集合的限制稍微宽松,不严格要求顺序

 对于每一个属性,它的名称都要从常量池中引用一个 constant_utf8_info 类型的常量来表示,属性表结构如下

 attribute_name_index 是一项指向 constant_utf8_info 类型常量的索引,由此常量值固定为 Code, 它代表了该属性的属性名称。

max_stack 代表了操作数栈 (Operand Stack) 深度的最大值。操作数栈不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。

 七、虚拟机类加载机制

原文地址:https://www.cnblogs.com/bytecodebuffer/p/13821856.html