JVM指令集
在jvm中,定义了有205条指令,从0x00 -> 0xCA , 0xFE -> 0xFF。我们在方法中写的所有逻辑,最终都会转换成这些指令,而虚拟机的最重要的一个任务就是解释执行这些指令。
指令类型种类很多
比较常见的类型有:
- load : 数据从局部变量表 -> 操作数栈中
- store : 数据从操作数栈 -> 局部变量表中
- references : 方法调用、类型强转、monitor锁等
- math :运算操作相关
- comparisons : 逻辑操作,if,while,for,goto等
指令格式
指令都是由两部分组成,操作码和操作数。
- 操作码: 用于表示是什么指令. 0x1a , 0x45 ,0x57 是操作码
- 操作数: 有些指令需要操作局部变量表和操作数栈,需要知道索引的位置.
//匹配指令
switch opcode {
...
case 0x1a:
return iload_0
...
case 0x45:
return fstore_2
...
case 0x57:
return pop
}
超级指令
解释的速度通常比较慢,原因之一是解释器在判断指令类型的时候涉及到分支,cpu层面分支预测失败和指令缓存未命中的开销都很大。
解决办法很容易想到,把两个或者多个代码合并成一个代码,在一趟处理中可以直接获取然后执行指令,这样就减少了分支的次数,这些指令便称为超级指令、快速指令、虚拟指令。
...
case 0x59:
return dup
...
case 0x60:
return iadd
...
case 0x84:
return iinc
...
例如
- dup: 指令表示将操作数栈顶的元素复制一份。
- iinc: 表示局部变量表+1,省略了经过操作数栈+1,然后再保存的过程。
指令流获取
在类加载过程-加载步骤。在字节码文件中,指令的数据保存在在methods的的属性表的code字段中。在类加载阶段,读取字节码文件时候会生成一份等价的对象,对象的构建就是从头到尾的把所有数据都进行读取,然后解析。每个不同组件都有不同的方法读取。
// 读取length长度的数据
func parseMethod(count uint16, reader *loader.ClassReader, pool *constant_pool.ConstantPool, k *Klass) Methods {
methods := make([]*MethodKlass, count)
for i := range methods {
method := &MethodKlass{}
//设置常量池
method.ConstantPool = pool
//设置访问标记
method.accessFlag = reader.ReadUint16()
//name常量池索引
method.nameIdx = reader.ReadUint16()
//描述符常量池索引
method.descriptorIdx = reader.ReadUint16()
//属性表数量
method.attrCount = reader.ReadUint16()
// 解析方法表中的属性表字段
method.AttributesInfo = attribute.ParseAttributes(method.attrCount, reader, pool)
// 绑定klass&method
method.Klass = k
// 本地方法注入字节码
method.InjectCodeAttrIfNative()
}
return methods
}
func (c *AttrCode) parse(reader *loader.ClassReader) {
c.MaxStack = reader.ReadUint16()
c.MaxLocals = reader.ReadUint16()
//code长度
c.codeLen = reader.ReadUint32()
//读取len长度的字节
c.code = reader.ReadBytes(c.codeLen)
c.ExceptionTable = parseExceptionTable(reader)
c.attrCount = reader.ReadUint16()
c.AttributesInfo = ParseAttributes(c.attrCount, reader, c.ConstantPool)
}
指令的解释过程
指令的执行是由解释器负责的,解析器的流程简单说就是:
graph LR
n1(读取操作码) --> n2(根据操作码创建指令) --> n3(根据指令确定操作数) --> n4(执行指令)
指令解释就是将这三步重复循环,其中要注意的点是栈帧中的pc和整体pc寄存器的值要保持一致。
//循环解释
func loop(thread *runtime.Thread) {
methodReader := &base.MethodCodeReader{}
for {
// 更新pc寄存器
updatePC(thread)
// 更新方法code
updateCodeArrt(thread, methodReader)
//执行指令
execInst(thread, methodReader)
if finished(thread) {
break
}
}
}
//执行
func execInst(thread *runtime.Thread, reader *base.MethodCodeReader) {
curFrame := thread.PeekFrame()
//获取操作码
opcode := reader.ReadOpenCdoe()
//创建指令
inst := instructions.NewInstruction(opcode)
//获取操作数
inst.FetchOperands(reader)
curFrame.SetFramePC(reader.MethodReaderPC())
//执行指令
inst.Execute(curFrame)
}
指令解释执行
获取到指令后,inst.Execute(curFrame)
执行指令。每条指令都有各自的执行逻辑
每条指令都有不同的执行逻辑。
func iload_0(frame *runtime.Frame) {
val := frame.LocalVars().GetInt(0)
frame.OperandStack().PushInt(val)
}
func iload_1(frame *runtime.Frame) {
val := frame.LocalVars().GetInt(1)
frame.OperandStack().PushInt(val)
}
i++ & ++i
int i = 1;
print(i++);
print(++i);
编译后字节码两者是不同的,原因与java编译器的遍历AST(javac采用逆波兰表达算法)有关。
func (i *IINC) Execute(frame *runtime.Frame) {
idx := i.Index >> 8 //局部变量表下标
toAdd := i.Index & 0x0011 //值
old := frame.LocalVars().GetInt(uint(idx))
frame.LocalVars().SetInt(uint(idx), int32(toAdd)+old) //设置新的值
}
i++ :先把i保存到了操作数栈,然后再对局部变量表的i+1。
++i :先对局部变量表的i+1,然后再把i保存到了操作数栈。
if..else..关键字
public static void if_test() {
int n = 10;
GvmOut.to("testing if");
GvmOut.to("10 > 9 ?");
if (n > 9) {
GvmOut.to("yes");
} else {
GvmOut.to("no");
}
}
for..关键字
public static void for_test() {
GvmOut.to("testing for");
for (int i = 0; i < 5; i++) {
GvmOut.to(i);
}
}
// Execute to branch if and only if val1 great or equals val2
func (icmp *IfIcmpge) Execute(frame *runtime.Frame) {
val2 := frame.PopInt() //弹出栈顶的int
val1 := frame.PopInt() //弹出栈顶的int
goNext := icmp.Index //获取挑战地址
if val1 >= val2 { //比较
base.Branch(frame, int(goNext))
}
while
public static void while_test() {
GvmOut.to("testing while");
int x = 100;
while (x < 105) {
x++;
GvmOut.to(x);
}
}
泛型
Java里泛型,与c#比起来,更算是一种语法糖。关于语法糖,是编译时的一种“小把戏”,目的是让使用者可以有更多的手段是编写代码。
好处是效率,了解Rust的可以感受到,编写Rust的代码极其高效的,代价就是学习成本非常高,原因之一就是大量的语法糖。
在java 中:
public void foo(Person<String> p){}
public void foo(Person<Long> p){}
泛型擦除-字节码分析
public void foo(Person<String> p){} ==> public void foo(Person<Object> p){}
public void foo(Person<Long> p){} ==> public void foo(Person<Object> p){}
下面这个类
public class Person<T>{
T name;
public static void main(String[] args) {
Person<String> p = new Person<String>();
p.name = "张三";
GvmOut.to(p.name);
}
public void foo(T t){}
}
字段的签名和方法的签名都是Object对象,那为什么还需要范型,直接用Object声明不就好了吗。答案是约束,使用泛型可以保证类型符合要求。
在字节码指令中,会添加checkcast命令去检查类型是否匹配。
泛型上下边界-字节码分析
泛型常见的场景之一是使用extends,可以约束参数的范围。
public class Son<T extends Person>{
T father;
public static void main(String[] args) {
Son<Person> son = new Son<>();
son.father = new Person();
}
public T name(T t){
return t;
}
}
本质上,编译后,泛型依然被擦除了,但是不再是Object类,而是Person类。
为什么选择这种方式实现范型
向下兼容。向下兼容。向下兼容。
参考:
-《虚拟机规范(SE8)》
-《自己动手些虚拟机》