老马的JVM笔记(五)----虚拟机字节码的执行

5.1 运行时栈帧结构

栈帧(Stack Frame)用于存储方法的信息。方法有关的才说栈。帧就是一块装栈的帧。栈帧用于存储方法的局部变量栈,操作数栈,动态连接,方法返回地址等。虚拟机栈里就都是这种栈帧,栈中有帧,帧中有栈。线程中充满了方法栈,方法调用方法,一个线程就会有一连串的栈帧。

1.局部变量表:方法中存有很多变量。在Java文件编译成Class文件时,虚拟机就为该方法需要的局部变量表的最大容量进行了确定(代码是静态的,运行是动态的,最大容量一开始就定下来,爆了怎么办?)。这个容量用很久以前说过的slot来规定。一个slot可以存放一个标准类型的变量。32位变量1slot,64位变量2slot,连续且不可分割。

局部变量表第0个变量是this,当然了,不能使static的方法,因为static没有this。第一个开始才是正经变量。

* 不使用的大对象应该手动处理:

public void hello(){
    {
        byte[] holder = new byte[64*1024*1024];
    }
    System.gc();
}
public void hello(){
    {
        byte[] holder = new byte[64*1024*1024];
    }
    int i = 0;
    // could be: holder = null;
    System.gc();
}

第一段的gc无效,第二段虽然加了没啥用的一句话,但就gc了。第一段代码框内部分执行完,虽然走出了holder的作用范围,但没有对局部变量表作任何事情,所以holder占的slot还在。所以gc不能进行。随便加一句话,走出范围并改动了局部变量表,slot可以被清除重用。所以大对象不用了,一定要处理掉啊,不能攒着。但是直接设为null确实有点暴力了。

类变量的准备阶段会给每个变量赋个初值,但局部变量没有这个方便,所以一定要搞出来一个初始值。

2.操作数栈:

和汇编课学的差不多,后进先出。操作就是方法中赋值啊,计算啊之类的。栈会自己找出一些类型问题,操作error。

3.动态连接:

用于指向运行时常量池中自己属于的方法。

4.方法返回地址:

方法只有两种情况会退出,第一是返回指令,return;第二种是异常。栈帧中要存有这两种方法各自返回的地址。返回结束,当前栈帧就算推出方法栈了。

5.2 方法调用

找方法。由于多态,方法名对不代表方法对。方法调用就是找我到底调用那个方法。Class文件中的常量池中存放的并不是方法,而是方法的符号引用。在类的解析阶段会解析出来一点很明确的方法(eg:静态方法和私有方法)。其余的都要动态找。

1.解析:

类解析阶段可以解析出来的非常明确的方法。静态方法,私有方法,实例构造器,父类方法。这都是实打实的老实方法。

2.分派(dispatch):

分派针对虚拟机解决Java的多态问题。

class A{
    static class Human{}

    static class Man extends Human{}

    static class Women extends Human{}

    public static void main(String[] args){
        Human man = new Man();
        Human women = new Women();
    }
}

Human man = new Man(); 在这句话里,Human叫变量的静态类型(static type),也叫外观类型(Apparent Type)。Man才是变量的实际类型(Actual Type)。静态类型编译时可知,实际类型只有运行时可知。可以理解,静态类型你只会定义一次,就是在声明对象时。但实际类型随时可以改,只要家谱里有静态类型就可以。

// 静态类型变化
sayHello((Man) man);
// 实际类型变化
man = new Woman();

到底调用谁,取决于虚拟机中调用方的调用方式。如果吃静态类型,就只看静态类型的方法,如果吃动态类型变量,就只看动态类型的方法。这个调用过程也是自下而上的。 

方法的接收者和方法的参数统称为方法的宗量(argument)。在编译阶段,方法解析要考虑调用者的静态类型,以及方法参数,所以说静态为多分派。在运行阶段,方法参数已定,只有方法的调用者会影响方法的选择,所以运行时动态单分配。 

public Father father = new Son("first"); // 既要看Father型,又要看方法中的参数

father.getSon(); //调用时看调用者

5.4 动态类型

动态类型语言:在运行期才确定类型,例如javascript, 万物都是var。Java,C++都是在编译时就要确定变量方法的类型,属于静态类型语言。实现原理为变量本身无类型,但变量的值有类型。

例如var str = "abcdefg":单看str,无法确定类型,但后面的值看似String,那就将他划为String类型值。

静态类型编译器可以发现错误,适合大规模程序设计;动态类型简单易写,适用于高效率开发。

JVM为了支持动态类型语言,设计了一些机制。java.lang.invoke。为了实现类似函数指针的功能,java提供了MethodHandle。

static class ClassA{
    public void println(String s){
        // simulate System.out
        System.out.println(s)
    }
}

public static void main(String[] args) throws Throwable{
    Object obj = System.currentTimeMills() % 2 == 0 ? System.out : new ClassA();
    // randomly pick one class

    getPrintlnMH(obj).invokeExact("icyfenix");
}

// notice the return type, it's "MethodHandle"
private static MethodHandle getPrintlnMH(Object receiver) throws Throwable{
    MethodType mt = MethodType.methodeType(void.class, String.class);
    // first param is return type, second is concrete input param
    
    // lookup()->MethodHandles.lookup, looks up some method qualified
    return lookup().findVirtual(receiver.getClass(), "println", mt).bindTo(receiver);
    // return a concrete method, rather in System.out or in ClassA
    // depends on type of receiver
    // after all, each type contains a "println"

    // All in all, this example shows that a function can be decided dynamically
}

与反射机制(reflection)相比,反射实在模拟Java代码层,而MethodHandle在模拟字节码层。(反射日后细谈)

实际例子:

class Test{
    class GrandFather{
        void thinking(){
            System.out.println("I'm grandfather.");
        }
    }

    class Father extends GrandFather{
        void thinking(){
            System.out.println("I'm father.");
        }
    }

    class Son extends Father{
        void thinking(){
            try{
                // here, we want to invoke gf's thinking
                MethodType mt = MethodType.methodType(void.class);
                MethodHandle mh = lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());
                mh.invoke(this);
                // invoke "grandfather's this"
            } catch(Throwable e){}
        }
    }

    public static void main(String[] args){
        (new Test.new Son()).thinking();
        // I'm grandfather.
    }
}

5.5 字节码解释执行引擎

由于笔者对JVM接触尚少,对本章节的实际应用不甚了解,所以代码和很多具体原理都是浅尝辄止,只求看懂,暂时不求应用。

解释执行:通过解释器执行;编译执行:通过即时编译器,产生本地代码,再执行。

在物理机执行代码的过程中,要经历词法分析,语法分析,生成抽象语法树。这是编译过程。C/C++将这些过程独立于执行引擎,Java把抽象树之前步骤独立,算是半独立引擎。Javac编译器完成词法分析,语法分析,抽象语法树,再生成线性字节码指令流,而解释器在JVM中,所以是半独立。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章