《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》

server/2025/3/1 16:11:30/

作者: 周志明

DeepSeek建议JVM书籍首选。

第一部分 走进Java

第1章 走进Java

世界上并没有完美的程序,但我们并不因此而沮丧,因为写程序本来就是一个不断追求完美的过程。

JAVA的优点,摆脱了平台的束缚,实现了一次编写,到处运行。

1.2 Java技术体系

从广义上讲,Kotlin,Clojure,JRuby,Groovy等运行于JAVA虚拟上的编程语言都属于JAVA体系中的一员。

JavaEE,这条产品线在JDK6以前被称为J2EE,JDK10以后被Oracle放弃,捐献给Eclipse基金会,被称为Jakarta EE。

1.3 Java发展史

1991.4月,James Gosling博士的绿色计划开始。

1995.5.23,Oak改为Java。

1999.4.27,HotSpot虚拟机诞生。

2009.4.20,Oracle宣布Sun公司。

2014.3.18,JDK8发布,增加新功能,Lambda表达式支持,内置Nashorn JavaScript引擎支持,新的时间日期API,彻底移除HotSpot的永久代。

2018.9.25,JDK11发布,XGC革命性的垃圾收集器出现。

1.4 Java虚拟机家族

1.4.1 虚拟机始祖:Sun Classic/Exact VM

世界上第一款商用JAVA虚拟机。

JDK1.2之前是JDK中唯一的虚拟机。

JDK1.4完全退出商用虚拟机的历史舞台。

1.4.2 武林盟主:HotSpot VM

理所当然的成为全世界使用最广泛的JAVA虚拟机,是JAVA虚拟机家族中毫无争议的武林盟主.

1.4.3 小家碧玉:Mobile/Embedded VM

智能手机上的跨平台,国内的老人手机和出口到经济欠发达国家的供智能手机还在广泛使用这种更加简单,资源消耗更小的上一代JavaME虚拟机。

1.4.4 天下第二:BEA JRockit/IBM J9 VM

其他就不写了。

JavaInJava,元循环,证明一门语言可以自举,具有研究价值。

自己实现虚拟机就很合适。

Maxine VM,Jikes RVM也都是元循环虚拟机。

“自举”(Bootstrap)是一个核心概念,指系统通过自身或最小化的初始力量逐步构建更复杂功能的过程。这一术语源自短语“pull oneself up by one’s bootstraps”(拽着鞋带把自己拉起来),比喻系统从简单起点实现自我升级的能力。

利用初始的简单能力,通过递归或迭代构建更复杂的系统

经典比喻

  • 启动过程:像用火柴点燃火种,再用火种生起篝火。
  • 编译器自举:如同先用木头搭建梯子,再踩着木梯建造更坚固的金属梯子。

1.5 展望Java技术的未来

1.5.1 无语言倾向

2018年4月,公开了一项黑科技:Graal VM。

口号:“Run Programs Faster Anywhere”。

Graal VM被官方称为"Universal VM",在HotSpot虚拟机上增强而成的跨语言全栈虚拟机,可以作为任何语言的运行平台使用。

工作原理是将源代码或编译后的中间格式LLVM字节码通过解释器转换为能被Graal VM接受的中间表示IR,例如设计一个解释器专门对LLVM输出的字节码进行转换来支持C和C++,此过程称为程序特化。

1.5.2 新一代即时编译器

HotSpot虚拟机包含两个即时编译器,编译耗时短单输出代码优化程度低的客户端编译器C1,编译时间长但代码优化程度高的客户端编译器C2。

JDK10起,加入全新即时编译器:Graal编译器。

1.5.3 向Native迈进

近几年从大型单体应用架构向小型微服务应用架构发展的技术潮流下,Java表现出来不适应,单体体积更小。

跨进程、面向用户程序的类型信息共享(Application Class Data Sharing,AppCDS),无操作垃圾收集器(Epsilon),提前编译(Ahead of Time Compilation,AOT)提供支持。

SubstrateVM极小型的运行时环境,包括独立的异常处理、同步调度、线程管理、内存管理和JNI访问。

1.5.4 灵活的胖子

HotSpot经过不断地添加新功能,以成长为会十八般武艺的身手灵活敏捷的胖子。

1.5.5 语言语法持续增强

JDK10中提供本地类型变量推断。

上面变量推断可以用在Lamdba中。

JDK13中提供实现switch语句的表达式支持。

JDK13中支持文本块功能,接收HTML,SQL场景中的大量+操作。

1.6 实战:自己编译JDK

要窥探Java虚拟机内部实现原理,最直接一条路径就是编译一套自己的JDK,通过阅读和跟踪调试JDK源码来了解Java技术体系运作。比阅读门槛高。

我们选择OpenJDK来进行编译实战。

1.6.1 获取源码

编译OpenJDK和OracleJDK,基本可以认为性能、功能和执行逻辑上都一样。

OpenJDK源码主页:http://openjdk.java.net/

本次用OpenJDK12.

通过 https://hg.openjdk.java.net/jdk/jdk12获取源码

若无法科学上网,则打包出源码压缩包再下载吧。

点击菜单的"Browse"显示根目录页面,点击左边zip下载打包好的源码。

1.6.2 系统需求

尽量使用Linux或者MacOS构建OpenJDK。

留出2-4G内存,6-8G硬盘。

认真阅读源码中的doc/building.html文档。

尽量不要使用中文路径的文件夹。

1.6.3 构建编译环境

我没有作者说的环境,只有Windows系统。

后续补。。。

第二部分 自动内存管理

第2章 Java内存区域与内存溢出异常

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进来,墙里面的人却想出来。

2.1 概述

对于C,C++程序开发的开发人员来说,在内存管理领域,他们既是最高权力的皇帝,又是从事最基础工作的劳动人民–既拥有每一个对象的所有权,又担负着每一个对象生命从开始到终结的维护责任。

Java程序员,不再需要为每一个new操作去写配对的"delete/free"代码,不容易出现内存泄漏和内存溢出的问题。但控制内存权力给了Java虚拟机,一旦出现问题,如果不了解虚拟机是怎样使用内存的,那排查错误、修正问题将会成为一项异常艰难的工作。

以下章节介绍Java虚拟机内存的各个区域,这是翻越虚拟机内存管理的第一步。

2.2 运行时数据区域

根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包含以下几个运行时数据区域:

画板

2.2.1 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间。

它可以看做是当前线程所执行的字节码行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

JVM多线程通过线程流转切换、分配处理器执行时间方式来实现的,在任何确定的时刻,一个处理器都只会执行一条线程中的指令,线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各条线程之间计数器互相不影响,独立存储,这类内存区域称为"线程私有"的内存。

线程正在执行Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是本地(Native)方法,计数器的值为空(undefined)。

2.2.2 Java虚拟机栈

Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,声明周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态连接,方法出口等信息。每个方法被调用直至执行完毕的过程,对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

Java内存区域笼统地划分为堆内存(Heap)和栈内存(Stack)。

局部变量表:存放编译期可知的基本数据类型、对象引用(reference类型,指向对象起始地址的引用指针)和returnAddress类型(指向一条字节码指令的地址)。

局部变量表中的存储空间以局部变量槽(Slot)来表示,64位long和double占用两个变量槽,其余类型占用1个,局部变量表大小表示的是槽的数量

栈溢出错误,StackOverflowError异常,线程请求的栈深度大于虚拟机所允许的深度。

内存溢出,OutOfMemoryError异常,程序无法申请到足够内存。

2.2.3 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,虚拟机栈作为虚拟机执行Java方法,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

HotSpot直接把本地方法栈和虚拟机栈合二为一。

2.2.4 Java堆

Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。

Java堆是被所有线程共享的一块内存区域。

虚拟机启动时创建,Java堆唯一目的就是存放对象实例。

《Java虚拟机规范》对Java堆的描述:所有的对象实例以及数组都应当在堆上分配。

Java堆是垃圾收集器管理的内存区域,也被称为"GC堆"(Garbage Collected Heap,垃圾堆)。

HotSpot里面出现了不采用分代设计的新垃圾收集器。

Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。

Java堆可以处于物理上不连续的内存空间中,逻辑上它应该被视为连续的。

Java堆可以被实现成固定大小的,-Xmx和-Xms设定。

2.2.5 方法区

方法区(Method Area)与Java堆,是各个线程共享的内存区域。

方法区称为"非堆"(Non-Heap)。

JDK8以前,愿意将方法区称呼为"永久代"(Permanent Generation).

两者并不等价,使用永久代实现方法区。

HotSpot JDK6放弃永久代(逐步改为本地内存Native Memory),JDK7把放在永久代的字符串常量池、静态变量等移除,JDK8完全废弃永久代,实现元空间(Meta-space)。

2.2.6 运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。

Class文件中除了有类的版本、字段、方法、接口等描述信息外,常量池表(Constant Pool Table),存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

运行时常量池在运行期间可以将新的常量放入池中。

2.2.7 直接内存

直接内存(Direct Memory)不是虚拟机运行时数据区一部分,也不是规范中定义的内存区域。JDK1.4加入NIO(New Input/Output)引入通道(Channel)缓冲区(Buffer)的I/O方法,可以使用Native函数直接分配对外内存。存在Java堆中DirectByteBuffer对象引用这块内存。

直接内存分配不受Java堆大小的限制。

2.3 HotSpot虚拟机对象探秘

Java堆中对象分配、布局和访问的全过程。

2.3.1 对象的创建

new关键字

虚拟机遇到字节码new指令时。

  • 检查指令参数是否在常量池中定位到一个类的符号引用。
  • 检查这个符号引用代表的类是否已被加载、解析和初始化。
  • 如果没有则先执行类加载过程。
  • 为新生对象分配内存,对象大小在类加载完后便可完全确定,分配过程就是把一块确定大小的内存块从Java堆中划分出来。假设堆内存绝对规整,使用过的放一边,空闲的内存放另一边,中间放着一个指针作为分界点的指示器,分配过程就是把指针向空闲方向挪动一段与对象相等距离,此方式称为"指针碰撞(Bump The Pointer)"。

若不规整,则已使用的内存和空闲内存交错在一起,必须维护一个列表,分配的时候从列表中找到一块足够大的空间划分给对象,并更新列表上的记录,这种分配方式叫**“空闲列表(Free List)”**。

分配方式由规整决定,规整由垃圾收集器是否带有**空间压缩(Compact)能力决定。使用Serial、ParNew等带压缩整理的收集器时,采用分配算法是指针碰撞。使用CMS这种基于清除(Sweep)**算法的收集器时,理论上只能采用复杂的空闲列表来分配内存。分配过程不是线程安全的,可能给对象A分配内存,指针还没来得及修改,对象B又同时使用原来的指针来分配内存的情况。解决这个问题的两种可选方案:1.对分配内存空间动作进行同步处理-采用CAS配上失败重试的方式保证更新操作的原子性。2.内存分配动作按线程划分在不同的空间之中进行,每个线程预先分配一小块内存称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),只有本地缓冲区用完了,分配新的缓冲区需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UserTLAB参数来设定。

  • 内存分配完成之后,虚拟机将分配到的内存空间(不包含对象头)都初始化为零值
  • Java虚拟机还要对**对象头(Object Header)**进行设置,包括此对象是哪个对象的示例、如何找到类的元数据信息、对象的哈希码(实际会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄、是否启用偏向锁等信息。
  • 一般在new指令之后会直接执行()方法,按照程序员意愿对对象进行初始化,这样一个真正可用的对象才算被构造出来。
HotSpot虚拟机字节码解释器(bytecodeInterpreter.cpp)中的代码片段

很少使用这个解释器,大部分平台用模版解释器,以下为了了解HotSpot运作过程。

2.3.2 对象的内存布局

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

对象头,两类信息

第一类:存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称为Mark Word。

用32/64位Bitmap结构来记录,考虑到空间效率,Mark Word被设计成一个有着动态定义的数据结构,以便极小的空间内存储尽量多的数据,对象未被同步锁的状态下,Mark Word32比特存储空间中25比特用于存储对象哈希码4比特存储对象分代年龄2比特存储锁标志位,1比特固定为0,其他状态(轻量级锁、重量级锁、GC标记、可偏向)下的对象存储锁标志位内容如下表:

第二类:是类型指针,即对象指向它的类型元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例,不是所有虚拟机必须在对象数据上保留类型指针,查找对象元数据信息并不一定要经过对象本身。若对象是一个Java数组,头中还要记录数组长度,可以通过元数据知道对象大小,但是无法从元数据中推断出数组大小。

32位虚拟机Mark Word的存储布局

实例数据

数据部分是对象真正存储的有效信息,即我们在程序中定义的各种类型的字段内容。

存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中的定义顺序影响。

HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shrots/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs 普通对象指针)。

相同的宽度字段总是被分配到一起存放,满足此条件下,父类中定义的变量会出现在子类之前。

HotSpot虚拟机的+XX:CompactFields参数值为true(默认为true),子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。

对齐填充

无特别含义,起着占位符的作用,虚拟机自动内存管理系统要求对象起始地址必须是8字节的整数倍,任何对象的大小必须是8字节的整数倍。对象头精心被设计成8字节的1倍或2倍。若实例对象部分没有对齐的话,需要通过对齐来补全。

2.3.3 对象的访问定位

Java程序会通过栈上的reference数据来操作堆上的具体对象。

reference类型在规范里是一个指向对象的引用,访问方式由虚拟机实现而定的,主流方式两种。

使用句柄

Java堆画出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据域类型数据各自具体的地址信息。

直接指针

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

两种对象访问方式各有优势,使用句柄最大好处就是reference中存储的是稳定句柄地址,对象被移动时,只会改变句柄中的实例数据指针,而reference本身不需要被修改。

直接指针来访问最大的好处就是速度更快,节省了一次指针定位的时间开销,由于对象访问非常频繁,这类开销积少成多也是一项极为可观的执行成本,本书的HotSpot主要使用第二种方式进行访问,整个软件开发来看,句柄来访问情况也十分常见。

2.4 实战:OutOfMemoryError异常

测试内存溢出错误,需要设置虚拟机启动参数,对结果有直接影响。

控制台执行程序,直接在Java命令后书写,用Eclipse,则在Debug/Run页签中设置。

原书是OpenJDK7,我这里是JDK17.

2.4.1 Java堆溢出

Java堆用于存储对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,随着对象数量增加,总量触及最大堆的容量限制后就会产生内存溢出异常。

以上限制堆为20MB,不可拓展,-XX:+HeapDumpOnOutOf-MemeoryError可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便事后分析。

Java堆内存溢出异常测试
java">import java.util.ArrayList;
import java.util.List;public class YoungGenGC {static class OOMObject {}public static void main(String[] args) {List<OOMObject> list = new ArrayList<OOMObject>();while (true) {list.add(new OOMObject());}}}

运行结果:

要解决内存区域的异常,常规处理方法是通过内存映像分析工具(Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析。

2.4.2 虚拟机栈和本地方法栈溢出

HotSpot中不区分虚拟机栈和本地方法栈,栈容量只由-Xss决定。

两种异常:

1.如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

2.若虚拟机的栈内存允许动态拓展,当拓展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。

验证这点做实验:

用-Xss参数减少栈内存容量,定义大量本地变量

JDK17的64位Windows,栈容量要求不低于180K,Linux则是228K。

增大此方法帧中本地变量表的长度。

不断建立线程的方式也会产生内存溢出异常

每个线程分配的栈内存越大,可以建立的线程数量自然越少,建立线程时越容易把内存耗尽。

2.4.3 方法区和运行时常量池溢出

运行时常量池是方法区的一部分,JDK7逐步去永久代,JDK8使用元空间代替永久代。

String::intern()/ˈɪntɜːrn/(实习生)是一个本地方法,作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用,否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

JDK6之前,HotSpot虚拟机中常量池分配在永久代中,通过-XX:PermSize和-XX:MaxPermSize限制永久代大小,可间接限制其中常量池的容量。

看到提示信息中有"PermGen space",说明运行时常量池确实属于方法区的一部分。

JDK7或8不会得到相同结果,7中-XX:maxPermSize或8中-XX:MaxMeta-spaceSize参数把方法区同样限制在6MB,也不会重现JDK6的溢出异常,将一直循环下去,永不停歇。原因是:自JDK7开始,原存放在永久代的字符串常量池被移至Java堆中,所以限制无作用。-Xmx限制最大堆到6MB就能够看出结果。

String.intern()返回引用的测试

JDK6中,intern()会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面的实例引用。StringBuilder创建的字符串对象实例在堆中,必然不可能是同一个引用,结果都将返回false。

JDK7以上中,此方法就不用拷贝字符串实例到永久代了,字符串常量池移到了堆中,只需要记录一下首次出现的实例引用即可。此时intern返回引用和StringBuilder创建的字符串实例是同一个。java这个字符串已经有了,不是首次出现,返回值是false。

元空间的防御措施

2.4.4 本机直接内存溢出

直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定。若不指定,则与堆最大值一样(-Xmx)。

越过DirectByteBuffer直接通过反射获取Unsafe实例进行内存分配。

真正申请分配内存的方法是Unsafe::allocateMemory();

直接内存导致的内存溢出,明显特征是在Heap Dump中看不出明显异常情况,若发现Dump文件很小,或又使用了DirectMemory(典型的就是NIO),有理由怀疑是直接内存方面的原因了。

第3章 垃圾收集器与内存分配策略

第6章 类文件结构

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

6.1 概述

老师说过,计算机只认识0和1,所以我们写的程序需要被编译器翻译成由0和1构成的二进制格式才能被计算机执行。

但十多年过去了,把程序编译成二进制本地机器码(Native Code)不是唯一选择,很多语言选择与操作系统和机器指令集无关的、平台中立的格式作为存储格式。

6.2 无关性的基石

指令集只有x86,系统只有Windows,也许不会有Java语言的出现。

所有平台支持**字节码(Byte Code)**构成平台无关性的基石。

6.3 Class类文件的结构

Class文件结构稳定,很多年来没咋变化,后面的版本兼容前面。

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在文件之中,中间没有添加任何分隔符,整个内容全部是程序运行的必要数据,没有空隙存在。

根据规范,Class文件格式采用类似于C语言结构体的伪结构来存储数据。

包含两种数据类型:“无符号数"和"表”.

  • 无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数(没有负数),可以描述数字、索引引用、数量值或UTF-8编码的字符串值。
  • 表由多个无符号数或其他表作为数据项的复合数据类型,便于区分,表的命名以"_info"结尾,表用于描述有层次关系的复合结构的数据,整个Class文件本质上视为一张表,看如下排列顺序。

无论是无符号数或表,描述同一类型但数量不定的多个数据时,会使用前置的容量计数器加若干连续的数据项的形式,称为"集合"。

数据行顺序和数量都是严格定义,不允许改变。

6.3.1 魔数与Class文件的版本

每个Class文件头4个字节被称为魔数(Magic Number),作用是确定此文件是否是一个能被虚拟机接受的Class文件。GIF,JPEG等文件头中都存有魔数。

魔数值为:0xCAFEBABE(咖啡宝贝,设计类的大佬有趣,暗指Java是一种咖啡)。

5,6字节是次版本号(Minor Version),7,8是主版本号(Major Version)。

简单Java代码编译成Class来分析
java">
public class TestClass {private int m;public int inc() {return m + 1;}}

Ctrl+1,0等转换为十进制或十六进制。

主版本为61,减去44,JDK为17,可支持6-17的代码。

基本上JDK12之后,主要主版本,次版本都是0.

6.3.2 常量池

与其他项目关联最多,空间也最大。

常量池入口放一个常量池计数值(constant_pool_count),从1开始,不是0.

这里是22,则有21项常量。第0项特殊考虑,表示不引用任何一个常量池项目,可以把索引设为0.

常量池中存放两大类常量:字面量(Literal),不用变量保存,是固定值,int a = 10,10是字面量和符号引用(Symbolic References),例如方法名a调b对象,存b的方法printMessage,加载类后内存中解析为直接引用(内存地址或句柄),这就是动态链接。

字面量接近为常量概念,符号引用是编译原理的概念。

包含的常量:

  • 模块导出的或开发的包(Package)
  • 类和接口的全名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符
  • 方法句柄和方法类型(Method Handle、MethodType、Invoke Dynamic)
  • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

字段描述符

  • 定义:字段描述符是一种特定的字符串格式,用于在字节码层面描述字段的数据类型。它遵循特定的规则,不同的数据类型有不同的表示方式。这种描述方式使得 Java 虚拟机(JVM)能够准确识别字段的数据类型,即使在字节码中无法直接看到像 Java 代码中那样直观的类型声明。
  • 规则及示例
    • 基本数据类型
      • booleanZ 表示。
      • byteB 表示。
      • charC 表示。
      • shortS 表示。
      • intI 表示。
      • longJ 表示(由于 long 类型占 64 位,在描述符中需要特殊标识)。
      • floatF 表示。
      • doubleD 表示(同理,double 类型占 64 位,有特定描述符)。
    • 引用数据类型:类和接口的描述符是其全限定名,不过斜杠(/)代替点(.)作为包名分隔符,并使用 L 作为前缀和分号(;)作为后缀。例如,java.lang.String 的描述符是 Ljava/lang/String;
    • 数组类型:对于数组,描述符以左方括号([)开头,后面跟着元素类型的描述符。例如,int[] 的描述符是 [IString[] 的描述符是 [Ljava/lang/String;

常量池中每一项常量都是一个表,共17种,仔细看标志不连续。

这17种没有共性,要逐项讲解。

6.5 公有设计,私有实现

遵循《Java虚拟机规范》是共有设计。

但更希望在约束下对具体实现做出修改和优化的私有实现更鼓励。

虚拟机实现的方式主要有以下两种:

  • 将输入的Java虚拟机代码在加载时或执行时翻译成另一种虚拟机的指令集。
  • 将输入的Java虚拟机代码在加载时或执行时翻译成宿主机处理程序的本地指令集(即时编译器代码生成技术)。

6.6 Class文件结构的发展

《Java虚拟机规范》已经超过二十年,Java技术体系翻天覆地,但Class文件结构一直处于相对稳定的状态,主体结构,字节码指令和数量几乎没有出现过变动,所有的改建,都集中在访问标志、属性表这些可拓展的数据结构。

访问标志新加入ACC_SYNTHETIC,ACC_ANNOTATION,ACC_ENUM,ACC_BRIDGE,ACC_VARARGS。

属性表增加了20项属性,枚举、变长参数、泛型、动态注解等.


http://www.ppmy.cn/server/171579.html

相关文章

spring注解开发(Spring整合MyBatis——Mapper代理开发模式、(Spring、MyBatis、Jdbc)配置类)(6)

目录 一、纯MyBatis独立开发程序。 &#xff08;1&#xff09;数据库与数据表。 &#xff08;2&#xff09;实体类。 &#xff08;3&#xff09;dao层接口。&#xff08;Mapper代理模式、无SQL映射文件——注解配置映射关系&#xff09; &#xff08;4&#xff09;MyBatis核心配…

Fisher信息矩阵(Fisher Information Matrix, FIM)与自然梯度下降:机器学习中的优化利器

Fisher信息矩阵与自然梯度下降&#xff1a;机器学习中的优化利器 在机器学习尤其是深度学习中&#xff0c;优化模型参数是一个核心任务。我们通常依赖梯度下降&#xff08;Gradient Descent&#xff09;来调整参数&#xff0c;但普通的梯度下降有时会显得“笨拙”&#xff0c;…

【Rabbitmq篇】高级特性----TTL,死信队列,延迟队列

目录 一.TTL ???1.设置消息的TTL 2.设置队列的TTL 3.俩者区别? 二.死信队列 定义&#xff1a; 消息成为死信的原因&#xff1a; 1.消息被拒绝&#xff08;basic.reject 或 basic.nack&#xff09; 2.消息过期&#xff08;TTL&#xff09; 3.队列达到最大长度? …

Ubuntu 下 nginx-1.24.0 源码分析 - ngx_init_cycle 函数 - 详解(5)

详解&#xff08;5&#xff09; 初始化打开文件列表&#xff08;open_files&#xff09; if (old_cycle->open_files.part.nelts) {n old_cycle->open_files.part.nelts;for (part old_cycle->open_files.part.next; part; part part->next) {n part->nelts…

BiliBili视频下载-原理与实现Python+FFmpeg

脚本地址: 项目地址: Gazer BiliGrab.py 提要 适用于: 登录状态下, 非大会员视频下载. 自动解析任意 B 站非大会员 / 付费视频的视频 & 音频请求链接并下载, 需要添加 Cookie 保证视频清晰度. 使用 FFmpeg 命令无损合并视频和音频. 使用方法 克隆或下载项目代码.安装…

Ubuntu+deepseek+Dify本地部署

1.deepseek本地部署 在Ollama官网下载 需要魔法下载 curl -fsSL https://ollama.com/install.sh | sh 在官网找到需要下载的deepseek模型版本 复制命令到终端 ollama run deepseek-r1:7b 停止ollama服务 sudo systemctl stop ollama # sudo systemctl stop ollama.servi…

论软件设计模式及其应用-软考

软件设计模式(Software Design Pattern)是一套被反复使用的、多数人知晓的代码设计经验的总结。使用设计模式是为了重用代码以提高编码效率、增加代代码可理解性、保证代码的可靠性。软件设计模式是软件开发中的最佳实践之一,它经常被开发人员在面向对象软件开发过程中所采用…

【大模型】大模型推理能力深度剖析:从通用模型到专业优化

大模型推理能力深度剖析&#xff1a;从通用模型到专业优化 大模型推理能力深度剖析&#xff1a;从通用模型到专业优化一、通用语言模型与推理模型的区别&#xff08;一&#xff09;通用语言模型&#xff1a;多任务的“万金油”&#xff08;二&#xff09;推理模型&#xff1a;复…