JVM: 方法执行简述

816 阅读6分钟

项目代码:github.com/zexho994/gv…

虚拟机整体的流程

graph LR
n1(java代码) -->|javac|n2(class字节码) --> |类加载器| n3(虚拟机内部class对象) -->|解析| n4(klass和oop对象) --> |解释指令|n5(解释器)

不同类型的虚拟机

从解释器的实现方式上来看,有两种类型的虚拟机执行架构。

  1. 基于栈结构
  2. 基于寄存器

jvm家族的虚拟机基本都是基于栈的,基于栈的实现稍微简单,移植性高。基于寄存器的虚拟机和cpu的执行方式类似,性能更快,例如v8。Lua虚拟机从5.0之前是栈虚拟机,5.0时转变为寄存器虚拟机

Lars Bak 是v8的作者,也是hotspot的作者之一,还是Dart语言的作者。

栈虚拟机

一段java代码

public void foo(){
	int a = 1;  
    int b = 2;  
    int c = a*b;
}

在不同类型的虚拟机中,要执行这段代码,首先编译的方式也会有不同,在基于栈的虚拟机中,编译后的指令为

public void foo();
    Code:
       0: iconst_1   //常量1存入到操作数栈
       1: istore_1   //操作数栈顶pop到局部变量表1
       2: iconst_2   
       3: istore_2
       4: iload_1    //局部变量表索引1数push到操作数栈
       5: iload_2    
       6: iadd       //操作数栈顶两个数相加
       7: iconst_5
       8: imul       //操作数栈顶两个数相乘
       9: istore_3
      10: return  
          

寄存器虚拟机

和汇编的风格类似,因为cpu本质上也是一个基于寄存器的解释器。寄存器虚拟机最大的优点就是性能好,支持随机访问,相比起来栈虚拟机的指令执行就显得十分麻烦的。

在基于寄存器的虚拟机中:

add ax bx	//其中AX寄存器的值为1,BX寄存器的值为2,将结果放入AX

虚拟机类型的抉择

为什么JVM选择了使用栈虚拟机的方式? 原因有很多,从历史的角度上看:

  • 栈虚拟机中,指令的平台无关性好。
  • 栈虚拟机的优点之一是指令更短,只用1字节,而基于寄存器的需要额外保存地址,一般为2字节。在当时,内存是比较重要的。
  • James Gosling 对这种方式的实现比较熟悉(之前他实现了PostScript虚拟机)。

对Java的影响

指令重排序之一的编译器重排序,是IR优化阶段的一种手段,叫表达式提升、表达式下沉。

优化的原因之一就是考虑栈的特性。

a=1;         b=2;
b=2;         a=1;
c=a+1;  ==>  c=a+1;

方法执行

栈帧

栈帧是方法的执行单位,一个方法对应一个栈帧。

//栈帧
type Frame struct {
	framePC   uint
	nextFrame *Frame
	*LocalVars
	*OperandStack
	*klass.MethodKlass
	*Thread
}

虚拟机栈

虚拟机栈是线程私有的,用于存放栈帧。当一个方法准备执行时候,栈帧push进行,完成

//线程模型
type Thread struct {
    ...
	*Stack  //虚拟机栈,存放栈帧
    ...
}
//虚拟机栈
type Stack struct {
    ...
	// 栈最大大小
	maxSize uint
	// 当前栈的大小
	size uint
	// 顶层帧
	top *Frame
    ...
}

局部变量表和操作数栈size

局部变量表和操作数栈的大小如何确定?在code属性表中,MaxStack表示栈的最大深度,MaxLocals表示局部变量表的最大深度。

type Attr_Code struct {
	NameIdx uint16
	name    string
	AttrLen uint32
	cp      constant_pool.ConstantPool
	// 方法的操作数栈在任何时间点的最大深度,在编译期就可以确定
	MaxStack uint16
	// 局部变量表大小,包括方法的参数
	MaxLocals uint16
	codeLen   uint32
	code      []byte
	// 异常表
	ExceptionTable []*ExceptionTable
	// 属性表
	attrCount uint16
	attrInfo  AttributesInfo
}

两个值的大小已经在编译期间确定,在字节码中保存,最后在类加载过程中,获取这两个字段。

func (c *Attr_Code) parse(reader *classfile.ClassReader) {
	c.MaxStack = reader.ReadUint16()
	c.MaxLocals = reader.ReadUint16()
	c.codeLen = reader.ReadUint32()
	c.code = reader.ReadBytes(c.codeLen)
	c.ExceptionTable = parseExceptionTable(reader)
	c.attrCount = reader.ReadUint16()
	c.attrInfo = ParseAttributes(c.attrCount, reader, c.cp)
}

方法调用

Invoke 指令簇

  • invokeinterface : 调用接口方法
  • invokespecial : 调用实例方法,(父类方法、私有方法、实例初始化方法)
  • invokevirtual : 调用虚方法,会根据实例的类型进行分派
  • invokestatic : 调用静态方法
  • invokedynamic : 调用动态方法(java7新增,支持动态语言的方法调用)

调用执行逻辑

public class Invokevirtual {
    
    public static void main(String[] args) {
        Invokevirtual invokevirtual = new Invokevirtual();
        int res = invokevirtual.additive(1, 2);
        GvmOut.to(res);
    }

    public int additive(int x, int y) {
        int r = x + y;
        return r;
    }

}

java中函数调用如何实现? main方法的code指令流:

additive方法的code指令流:

image.png

静态绑定与动态绑定

网上很多文章对于这两者的解释为重载是静态绑定(编译时多态),重写是动态绑定。其实不完全正确,重载也可能被子类重写的情况,一样需要在运行期间判断。

准确地说,Java 虚拟机中的静态绑定指的是在解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况。概括就是

  • 静态绑定只在编译期间就可以知道具体要调用的方法。
  • 动态绑定指要到运行期间才能知道要调用的方法。

在invoke命令中,invokestatic对应的为静态绑定(使用static修饰),invokevirtual、invokeinterface为动态绑定的。

用代码进行说明就是

invokestatic的逻辑

func (i *INVOKE_STATIC) Execute(frame *runtime.Frame) {
	cp := frame.Method().CP()
	contantMethod := cp.GetConstantInfo(i.Index).(*constant_pool.ConstantMethod)
	className := contantMethod.ClassName()
	perm := jclass.GetPerm()
	class := perm.Space[className]
	if class == nil {
		class = jclass.ParseInstanceByClassName(className)
	}
	name, _type := contantMethod.NameAndDescriptor()
	methodInfo, err := class.FindStaticMethod(name, _type)        //获取目标方法
	if err != nil {
		panic("[gvm]" + err.Error())
	}
	if !jclass.IsStatic(methodInfo.AccessFlag()) {
		panic("[gvm] invoke static error")
	}
	methodInfo.SetJClass(class)
	base.InvokeMethod(frame, methodInfo, true)
}

func (j JClass_Instance) FindStaticMethod(name, descriptor string) (*MethodInfo, error) {       // 在本类中获取
	for i := range j.Methods {
		methodInfo := j.Methods[i]
		if !IsStatic(methodInfo.accessFlag) {
			continue
		}
		mName := j.ConstantPool.GetUtf8(methodInfo.nameIdx)
		mDesc := j.ConstantPool.GetUtf8(methodInfo.descriptorIdx)
		if name != mName || mDesc != descriptor {
			continue
		}
		return j.Methods[i], nil
	}
	return nil, exception.GvmError{Msg: "not find static method it name " + name}
}

invokevirtual的逻辑

func (i *INVOKE_VIRTUAL) Execute(frame *runtime.Frame) {
	constantMethod := frame.Method().CP().GetConstantInfo(i.Index).(*constant_pool.ConstantMethod)
	methodNameStr, methodDescStr := constantMethod.NameAndDescriptor()
	exception.AssertTrue(methodNameStr != "<init>" && methodNameStr != "<clinit>", "IncompatibleClassChangeError")

	classNameStr := constantMethod.ClassName()
	permSpace := jclass.GetPerm().Space
	jc := permSpace[classNameStr]
	if jc == nil {
		jc = jclass.ParseInstanceByClassName(classNameStr)
	}
	exception.AssertTrue(jc != nil, "NullPointerException")
    
    //*****查找目标实例方法*****
	methodInfo, err, _ := jc.FindMethod(methodNameStr, methodDescStr)     
	exception.AssertTrue(err == nil, "no find the method of "+methodNameStr)
	exception.AssertFalse(jclass.IsStatic(methodInfo.AccessFlag()), "IncompatibleClassChangeError")

	if jclass.IsProteced(methodInfo.AccessFlag()) {
		// todo if is proteced , need to judge the relation between caller and called
	}

	base.InvokeMethod(frame, methodInfo, false)
}


func (j *JClass_Instance) FindMethod(name, descriptor string) (*MethodInfo, error, *JClass_Instance) {
	for i := range j.Methods {
		methodInfo := j.Methods[i]
		if IsStatic(methodInfo.accessFlag) {
			continue
		}
		mName := j.ConstantPool.GetUtf8(methodInfo.nameIdx)
		mDesc := j.ConstantPool.GetUtf8(methodInfo.descriptorIdx)
		if mName == name && mDesc == descriptor {
			return j.Methods[i], nil, j
		}
	}
	// 在父类中遍历查找
	m, err, jc := j.SuperClass.FindMethod(name, descriptor)
	if err == nil {
		return m, nil, jc
	}
	// 在接口中遍历查找
	for i := range j.Interfaces {
		m, err, jc := j.Interfaces[i].FindMethod(name, descriptor)
		if err == nil {
			return m, nil, jc
		}
	}
	return nil, exception.GvmError{Msg: "not find method it name " + name}, nil
}

JNI方法简述

有一些场景中,java本身没有能力去完成,最常见的就是当要系统调用时候,就要使用JNI(Java Native Interface)这种方式。

例如nio中的Selector

private native int poll0(long var1, int var3, long var4);

unsafe.cas

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

这种方式本质上也是调用另一个地方的函数,链接的工作由虚拟机完成。GvmOut是invoke调用时基于Go的Print方法实现,使用native方法可以进行非常多的扩展操作。

public class GvmOut{
    public native static void to(int i);
    public native static void to(float i);
    public native static void to(double i);
    public native static void to(boolean i);
    public native static void to(long i);
    public native static void to(String i);
}

参考: