毋庸置疑,Java 已经成为最受欢迎的编程语言之一。然而,并非每位 Java 开发者都会满怀好奇地探索 JVM 的底层工作机制。这篇短文编写了一个玩具 JVM,旨在抛砖引玉希望能借此激发大家的探索欲望。
下面是一段极其简单的代码:
public class Add {
public static int add(int a, int b) {
return a + b;
}
}
执行 javac Add.java,编译后会生成 Add.class。后者是一个二进制文件,交由 JVM 执行。剩下来只需要实现一个 JVM 运行 class 文件即可。让我们用 hexdump 对 Add.class 一探究竟。打开文件,输出的内容也许让您感觉云里雾里:
00000000 ca fe ba be 00 00 00 34 00 0f 0a 00 03 00 0c 07 |.......4........|
00000010 00 0d 07 00 0e 01 00 06 3c 69 6e 69 74 3e 01 00 |........<init>..|
00000020 03 28 29 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 |.()V...Code...Li|
00000030 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 01 00 03 |neNumberTable...|
00000040 61 64 64 01 00 05 28 49 49 29 49 01 00 0a 53 6f |add...(II)I...So|
00000050 75 72 63 65 46 69 6c 65 01 00 08 41 64 64 2e 6a |urceFile...Add.j|
00000060 61 76 61 0c 00 04 00 05 01 00 03 41 64 64 01 00 |ava........Add..|
00000070 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 |.java/lang/Objec|
00000080 74 00 21 00 02 00 03 00 00 00 00 00 02 00 01 00 |t.!.............|
00000090 04 00 05 00 01 00 06 00 00 00 1d 00 01 00 01 00 |................|
000000a0 00 00 05 2a b7 00 01 b1 00 00 00 01 00 07 00 00 |...*............|
000000b0 00 06 00 01 00 00 00 01 00 09 00 08 00 09 00 01 |................|
000000c0 00 06 00 00 00 1c 00 02 00 02 00 00 00 04 1a 1b |................|
000000d0 60 ac 00 00 00 01 00 07 00 00 00 06 00 01 00 00 |`...............|
000000e0 00 03 00 01 00 0a 00 00 00 02 00 0b |............|
虽然看起来结构不清晰,但仍然可以找到线索:例如,上面的内容中 ()V 和 (II)I 是什么?
$ javap -c Add
Compiled from "Add.java"
public class Add {
public Add();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
}
上面可以看到 class、构造函数和 add 方法。方法中包含了一些指令。透过指令似乎可以猜到 add() 方法的作用:加载两个参数(iload_0 和 iload_1),相加后存储结果。JVM 是一种堆栈机器(Stack Machine),没有寄存器。所有参数都存储在内部的堆栈上,所有结果也存储在堆栈上。
接下来,如何像 javap 那样解析 class 文件?
class 文件结构可以在 JVM 规范文档中找到,听起来很简单。通常以4字节签名(CAFEBABE)开头,接着是2+2字节的版本信息。加载器的第一个实现看起来像下面这样,从二进制文件中读取 byte、short、int 和字节流:
type loader struct {
r io.Reader
err error
}
func (l *loader) bytes(n int) []byte {
b := make([]byte, n, n)
// 每步执行中没有处理错误,
// 只存储解析过程中发现的第一个错误,
// 没有进行出错处理
if l.err == nil {
_, l.err = io.ReadFull(l.r, b)
}
return b
}
func (l *loader) u1() uint8 { return l.bytes(1)[0] }
func (l *loader) u2() uint16 { return binary.BigEndian.Uint16(l.bytes(2)) }
func (l *loader) u4() uint32 { return binary.BigEndian.Uint32(l.bytes(4)) }
func (l *loader) u8() uint64 { return binary.BigEndian.Uint64(l.bytes(8)) }
// 调用
f, _ := os.Open("Add.class")
loader := &loader{r: f}
cafebabe := loader.u4()
major := loader.u2()
minor := loader.u2()
按照 JVM 规范,接下来要解析常量池(Constant Pool):它是 class 文件中的一个特殊区域,所有字符串、数字常量和引用都存储在这里。每个存储都具有一个唯一的 uint16 类型的索引号(因此 class 文件最多有 64K个常量)。常量池中支持下面几种类型,每种类型包含一组不同的值:
可以看到,池中的常量经常互相引用。由于本文使用 Go 来实现 JVM,因此没有 union 类型。下面是一个 Const 类型,包含了许多字段:
type Const struct {
Tag byte
NameIndex uint16
ClassIndex uint16
NameAndTypeIndex uint16
StringIndex uint16
DescIndex uint16
String string
}
type ConstPool []Const
接下来按照 JVM 规范开始解析常量池数据:
func (l *loader) cpinfo() (constPool ConstPool) {
constPoolCount := l.u2()
// 从1开始验证常量池索引
for i := uint16(1); i < constPoolCount; i++ {
c := Const{Tag: l.u1()}
switch c.Tag {
case 0x01: // UTF8 字符串, 长度 2 个字节 + data
c.String = string(l.bytes(int(l.u2())))
case 0x07: // Class 索引
c.NameIndex = l.u2()
case 0x08: // String 引用索引
c.StringIndex = l.u2()
case 0x09, 0x0a: // 字段和方法:class 索引 + NaT 索引
c.ClassIndex = l.u2()
c.NameAndTypeIndex = l.u2()
case 0x0c: // 名字与类型
c.NameIndex, c.DescIndex = l.u2(), l.u2()
default:
l.err = fmt.Errorf("unsupported tag: %d", c.Tag)
}
constPool = append(constPool, c)
}
return constPool
}
上面的例子很简单,实际 JVM 处理 long 和 double 常量时会插入未使用的常量以进行区别处理。按照 JVM 的规定,常量会按照32bit 处理。 下面的 Resolve(index uint16) string 方法让按索引号处理字符串变得更简单:
func (cp ConstPool) Resolve(index uint16) string {
if cp[index-1].Tag == 0x01 {
return cp[index-1].String
}
return ""
}
现在开始解析类、接口、字段、方法和它们的属性,通过下面的 helper 方法实现:
func (l *loader) interfaces(cp ConstPool) (interfaces []string) {
interfaceCount := l.u2()
for i := uint16(0); i < interfaceCount; i++ {
interfaces = append(interfaces, cp.Resolve(l.u2()))
}
return interfaces
}
// Field 类型适用于字段和方法
type Field struct {
Flags uint16
Name string
Descriptor string
Attributes []Attribute
}
// Attribute 提供了字段和类的额外信息
// 最有用 "Code" 属性,包含了实际的字节码信息
type Attribute struct {
Name string
Data []byte
}
func (l *loader) fields(cp ConstPool) (fields []Field) {
fieldsCount := l.u2()
for i := uint16(0); i < fieldsCount; i++ {
fields = append(fields, Field{
Flags: l.u2(),
Name: cp.Resolve(l.u2()),
Descriptor: cp.Resolve(l.u2()),
Attributes: l.attrs(cp),
})
}
return fields
}
func (l *loader) attrs(cp ConstPool) (attrs []Attribute) {
attributesCount := l.u2()
for i := uint16(0); i < attributesCount; i++ {
attrs = append(attrs, Attribute{
Name: cp.Resolve(l.u2()),
Data: l.bytes(int(l.u4())),
})
}
return attrs
}
用 Field 代表字段和方法,可以帮助我们节省很多时间。最后把上面的代码合起来解析一个完整的 class 文件。
type Class struct {
ConstPool ConstPool
Name string
Super string
Flags uint16
Interfaces []string
Fields []Field
Methods []Field
Attributes []Attribute
}
func Load(r io.Reader) (Class, error) {
loader := &loader{r: r}
c := Class{}
loader.u8() // magic (u32), minor (u16), major (u16)
cp := loader.cpinfo() // 常量池信息
c.ConstPool = cp
c.Flags = loader.u2() // 访问标记
c.Name = cp.Resolve(loader.u2()) // 当前类
c.Super = cp.Resolve(loader.u2()) // 父类
c.Interfaces = loader.interfaces(cp)
c.Fields = loader.fields(cp) // 字段
c.Methods = loader.fields(cp) // 方法
c.Attributes = loader.attrs(cp) // 方法
return c, loader.err
}
进一步深入类信息可以看到其中包含了零个字段和两个方法:
上面的例子中,
仔细观察,会发现解析后的类中每个方法都有一个名为 “Code” 的属性。该属性带有一个字节的信息,其二进制输出如下:
<init>:
[0 1 0 1 0 0 0 5 42 183 0 1 177 0 0 0 1 0 7 0 0 0 6 0 1 0 0 0 1]
add:
[0 2 0 2 0 0 0 4 26 27 96 172 0 0 0 1 0 7 0 0 0 6 0 1 0 0 0 3]
如果仔细查看规范文档中的 bytecode 章节,会发现 “Code” 属性从 maxstack(2个字节)开始,接着是 maxlocals(2个字节),然后是代码长度(4个字节),最后是实际代码。示例如下:
<init>: maxstack: 1, maxlocals: 1, code: [42 183 0 1 177]
add: maxstack: 2, maxlocals: 2, code: [26 27 96 172]
每个方法只包含4和5个字节代码。这些字节代表什么含义?
正如之前提到的,JVM 是一个堆栈机器。每条指令都会被编码成单个字节,后面跟一些附加参数。如果仔细阅读规范会看到 “add” 方法包含了下面的指令:
26 = iload_0
27 = iload_1
96 = iadd
172 = ireturn
和文章开始的时候看到的 javap 输出的结果一样!接下来要如何执行?
JVM 内部执行方法时,每个方法有自己的堆栈用来存储临时操作数、本地变量和待执行的代码块。所有这些参数都存储在一个执行帧(Frame)中。此外,执行帧还包含当前指令指针和包含了该方法的类指针。后者用于访问类的常量池以及其他细节。
下面的代码中,方法为需要调用的方法创建了一个帧。这里使用 interface{} 作为值类型,当然 union 类型是另一种更安全的选择。
type Frame struct {
Class Class
IP uint32
Code []byte
Locals []interface{}
Stack []interface{}
}
func (c Class) Frame(method string, args ...interface{}) Frame {
for _, m := range c.Methods {
if m.Name == method {
for _, a := range m.Attributes {
if a.Name == "Code" && len(a.Data) > 8 {
maxLocals := binary.BigEndian.Uint16(a.Data[2:4])
frame := Frame{
Class: c,
Code: a.Data[8:],
Locals: make([]interface{}, maxLocals, maxLocals),
}
for i := 0; i < len(args); i++ {
frame.Locals[i] = args[i]
}
return frame
}
}
}
}
panic("method not found")
}
有了帧、初始化的局部变量、空堆栈、预加载的字节码,现在开始执行:
func Exec(f Frame) interface{} {
for {
op := f.Code[f.IP]
log.Printf("OP:%02x STACK:%v", op, f.Stack)
n := len(f.Stack)
switch op {
case 26: // iload_0
f.Stack = append(f.Stack, f.Locals[0])
case 27: // iload_1
f.Stack = append(f.Stack, f.Locals[1])
case 96:
a := f.Stack[n-1].(int32)
b := f.Stack[n-2].(int32)
f.Stack[n-2] = a + b
f.Stack = f.Stack[:n-1]
case 172: // ireturn
v := f.Stack[n-1]
f.Stack = f.Stack[:n-1]
return v
}
f.IP++
}
}
最后整合上面的工作,调用 add() 方法:
f, _ := os.Open("Add.class")
class, _ := Load(f)
frame := class.Frame("add", int32(2), int32(3))
result := Exec(frame)
log.Println(result)
// 输出
OP:1a STACK:[]
OP:1b STACK:[2]
OP:60 STACK:[2 3]
OP:ac STACK:[5]
5
最终,我们有了一个可以工作的 JVM。虽然它非常简陋,但仍然做了 JVM 该做的事情:加载字节码并进行解析(当然,真正的 JVM 完成的工作不止于此)。
还要支持200条指令、运行时、面向对象类型系统以及其他一些东西。JVM 总共有11组指令,其中大多数都很简单:
大多数指令的实现都很简单:从堆栈中取出一个或者两个个参数,对它们执行一些操作,然后把结果推入堆栈。这里需要记住的是:long 和 double 每个值在堆栈上要占用两个槽(Slot),因此可能需要额外增加 push() 和 pop()。这样会提高指令分组难度。
实现应用需要考虑对象模型:如何存储对象和它们的类,如何表达继承,如何存储实例与类的字段。
不仅如此,还要仔细设计方法分派:又很多不同的“invoke”指令,使用时要注意细微的区别:
如果 JVM 的实现语言不支持,还需要考虑如何实现垃圾回收:引用计数、标记和清除等。此外还要处理异常:通过 athrow 指令实现异常处理。在不同的调用帧中传递异常并通过异常表进行处理。
最后,如果没有运行时类 JVM 也没法使用。例如,没有 java/lang/Object ,就不可能使用 new 指令构造新对象。设计的运行时需要提供一些通用的 JRE 类,例如 java.lang、java.io 和 java.util 包提供的类。此外,还需要一些提供特定功能的类。这些类包含的方法,有一些不得不用非 Java 语言提供本地实现。这就为 JVM 引入了一些边界场景,需要考虑如何找到并且执行这些方法。
换句话说,实现一个 JVM 并不是那么简单,但理解它的实现机制也没那么复杂。
以上的工作只花了一个周末。虽然这个 JVM 看起来还有很长的路要走,但是结构看起来还是比较清晰的:https://github.com/zserge/tojvm(欢迎提交 PR)。
最终完成的代码不多,可以作为实现参考要点。
如果对 JVM 底层实现这个主题想要了解更多,推荐下面的 JVM 实现:
希望这篇文章不会为你带来困扰。虚拟机是一个很有意思的主题,JVM 的地位名副其实。
本文由哈喽比特于4年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/YnfqaBhVbUIRROd2uyth5w
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。