Android工程师学习JVM(六)-字节码执行引擎

1,617 阅读8分钟

前言

在学习JVM这个系列文章中,已经讲解了JVM规范、Class文件格式以及如何阅读字节码、ASM字节码处理、类的生命周期及自定义类加载器等。本篇介绍字节码执行引擎,理解java程序在实际运行中涉及到的栈帧的结构,局部变量存储,操作数存储,动态分派等内容

如果你对JVM、字节码、Class文件格式、ASM字节码处理、类加载及自定义类加载器、内存分配有兴趣的话,可以看之前的文章哈,相信会收获更多哦

Android工程师学习JVM(五)-内存分配基础知识

Android工程师学习JVM(四)-类加载、连接、初始化、卸载

Android工程师学习JVM(三)-字节码框架ASM使用

Android工程师学习JVM(二)-教你阅读Java字节码

Android工程师学习JVM(一)-JVM概述

1、字节码执行引擎概述

JVM的字节码执行引擎,功能基本就是输入字节码后对字节码进行解析并处理,最后输出执行的结果

当前实现方式有两种,一种是通过解释器直接解释执行字节码,第二种是通过即时编译器产生本地代码,也就是编译执行。有可能两种都存在,这部分涉及到虚拟机的实现,有可能执行次数较少的采用解释执行,执行次数较多的采用编译执行,能使性能在一定程度上得到优化。

2、栈帧

在上篇文章我们第一次介绍到Java虚拟机栈中每个方法是一个栈帧。本小结将细致介绍栈帧。

2.1、栈帧概述

栈帧是用于支持JVM进行方法调用和方法执行的数据结构,栈帧随着方法调用而创建,随着方法结束而销毁。栈帧里面存储了方法的局部变量、操作数栈、动态链接、方法返回地址等信息

栈帧及帧栈模型如下图:

上一篇文章我们讲到栈是线程私有的,在这里可以进行加深理解。当方法调用时,方法相关的栈帧进栈,调用完成后出栈。一个一个的栈帧实际上就可以等同于程序的运行,每个线程独立运行自己的逻辑,也就很好理解每个线程有自己独立的帧栈了哈。下面我们来着重介绍栈帧中的具体内容。相信看完后就能大致体会到是程序是如何执行的

2.2、局部变量表

局部变量表:用来存放方法参数方法内部定义的局部变量的存储空间

1、局部变量表存储是以变量槽slot为单位,目前一个slot存放32位以内的数据类型,对于超过32位的,放多个槽位,如double放2个槽位

2、对于实例方法,第0位slot存放的是this,然后从1到n,依次分配给参数列表

3、分配slot时,是根据方法体内部定义的变量顺序和作用域来分配的

下面从实际案例看下:

public class Test {

    public int add(int a, int b) {
        int c = a + b;
        return a+b+c;
    }

}

编译成class字节码后,使用javap -v Test.class命令,从汇编看add方法字节码如下:

从图中看,最后的LocalVariableTable就是我们上文介绍的局部变量表。可以看到,当前有四个槽位,第0个槽位放的是this,第1和第2个槽位是方法参数a和b。第3个槽位存放方法中的局部变量c,是有顺序的安排的。

再看一下汇编代码中Code部分,这部分就是我们写的代码在字节码中的体现。

0: iload_1------------------加载第1个局部变量
1: iload_2------------------加载第2个局部变量
2: iadd------------------将第一个局部变量和第二个局部变量相加
3: istore_3------------------将相加的结果存储到第三个局部变量
//也就对应我们的代码c=a+b

也就是说,局部变量表就是用来存储方法参数和方法内局部变量的。但是在a+b+c中,我们看汇编实际执行时,加载了a,b执行了a+b,然后加载c,再执行一次加法。那这中间a+b的结果并不是我们的局部变量,这又是怎么进行的呢?这就需要了解操作数栈的知识了,这块将在下个章节介绍。

slot这部分还有个复用的特点需要着重介绍一下,有时候一些奇怪的现象就是复用导致的。

    public static void main(String[] args) {
        {
            byte[] bs = new byte[2*1024*1024];
        }
        //这行代码有无会影响到bs的内存是否能释放
        //int a = 1;
        System.gc();
        System.out.println("totalMemory===" + Runtime.getRuntime().totalMemory()/1024.0/1024.0);
        System.out.println("freeMemory===" + Runtime.getRuntime().freeMemory()/1024.0/1024.0);
        System.out.println("maxMemory===" + Runtime.getRuntime().maxMemory()/1024.0/1024.0);
    }

运行这段程序之前,先配置下打印GC日志,配置方式为:

多次测试注释掉a=1的情况,得到的内存结果都类似,选取其中一次,情况如下:

totalMemory===123.0

freeMemory===120.07547760009766

maxMemory===1820.5

程序大概占用了3M空间,bs这块内存是一直没有释放的。这里的内存和我的机器有关的,只需关注大体情况哈

把int a = 1这行代码的注释去掉。运行结果:

totalMemory===123.0

freeMemory===122.07544708251953

maxMemory===1820.5

bs占用的2M被gc回收了~,多次测试的结果都是一致的。为什么呢????为啥写个a=1居然影响了gc。

实际上是因为slot是复用的,当出了bs的作用域后,它的槽位可以被复用。

在注释掉a=1的情况下,它依然指向了2M的内存空间,那么2M的内存空间依然有引用存在,不能回收。

在去掉注释a=1的情况下,a复用了bs的槽位,那么2M的空间没有引用了,可以被回收。内存就释放掉了。

是不是很神奇呢?所以我们常说当大数据不用了要置位为null,是很有道理的。在上面的情况下,把bs=null写上才是最佳做法哈。

2.3、操作数栈

操作数栈:用来存放方法运行期间,各个指令操作的数据

还是用这个案例:

0: iload_1------------------加载第1个局部变量
1: iload_2------------------加载第2个局部变量
2: iadd------------------将第一个局部变量和第二个局部变量相加
3: istore_3------------------将相加的结果存储到第三个局部变量

这里的加载第1个局部变量,实际上说的就是将第1个局部变量放到操作数栈当中去,再次翻译一下这段代码,以操作数栈的角度来看

0: iload_1------------------将局部变量1压入栈
1: iload_2------------------将局部变量2压入栈
2: iadd------------------弹出栈中两个数相加,并将结果压入栈
3: istore_3------------------将栈顶的值出栈,赋值给第3个局部变量
4: iload_1------------------将局部变量1压入栈
5: iload_2------------------将局部变量2压入栈
6: iadd------------------弹出栈中两个数相加,并将结果压入栈
7: iload_3------------------将局部变量3压入栈
8: iadd------------------弹出栈中两个数相加,并将结果压入栈
9: ireturn------------------将栈顶的值出栈,返回结果

2.4、动态链接

每个栈帧持有一个指向运行时常量池中该栈帧所属方法的引用,以支持方法调用过程的动态链接

那么虚拟机是如何查找程序运行时的方法的呢?程序中我们经常会重载方法,重写方法。

方法调用:方法调用就是确定具体调用哪一个方法,并不涉及方法内部的执行流程

一般有两种情况:

1、部分方法是直接在类加载的解析阶段,就确定了直接引用关系。如静态方法,私有方法,父类方法

2、对于实例方法,也称虚方法,因为重载和多态,需要运行期动态委派

对应这两种情况又有两种定位方法的方法:

1、静态分派:依赖于静态类型来定位方法执行版本的分派方法,如:方法重载

2、动态分派:根据运行时的实际类型来定位方法执行版本的分派方式,比如方法重写

2.5、方法返回地址

指的是方法执行后返回的地址。

public static void main(String[] args) {
    Test test = new Test();
    //test.add方法执行后要返回main方法这里,才能够继续往下执行System.out.println执行打印结果
    int result = test.add(1, 2);
    System.out.println("result == " + result);
}

3、小结

字节码执行引擎这部分最重要的知识就是理解栈帧了。本篇需要重点理解的知识有:

1、帧栈和栈帧的关系

2、栈帧数据结构包含哪些内容

3、局部变量表的作用以及操作数栈的作用,这两者的区别

4、虚拟机中的方法调用,这块主要学习几个概念,如静态分派,动态分派等