BiBi - JVM -10- 虚拟机字节码

From:深入理解Java虚拟机

物理机:执行引擎直接建立在处理器、硬件、指令集和操作系统层面上。
虚拟机:执行引擎由自己实现,可以自行制定指令集。

1. 栈桢

栈桢是虚拟机方法调用过程的数据结构,主要包括:局部变量表、操作数栈、动态链接、方法返回地址等。在编译代码的时候,栈桢中需要多大的局部变量表,多深的操作数栈都已经全部确定了,并且写入到方法表的Code属性之中【其中max_loclas代表局部变量表的最大容量】,因此一个栈桢需要分配多少内存,不会受到程序运行期变量数据的影响。

  • 局部变量表

存放方法参数和方法内部定义的局部变量。局部变量表的容量以【变量槽,Slot】为最小单位,一个Slot可以存放一个32位以内的数据类型。对于long和double 64位的数据会以高位对齐的方式分配两个连续的Slot空间。在方法执行时,虚拟机使用局部变量表完成【参数值】到【参数变量列表】的传递。

Slot复用影响垃圾回收行为:

//方式一:变量b还在作用域之内,所以不会回收b的内存。
public static void main(String[] args) {
  byte[] b = new byte[1024 * 1024 * 64];
  System.gc();
}
//方式二:由于Slot复用的原因,b不会被回收
public static void main(String[] args) {
  {
    byte[] b = new byte[1024 * 1024 * 64];
  }
  System.gc();
}
//方式三:b被回收
public static void main(String[] args) {
  {
    byte[] b = new byte[1024 * 1024 * 64];
  }
  int a = 0;
  System.gc();
}

上面三种方式中b能否被回收的根本原因:局部变量表中的Slot是否还存有b数组对象的引用。方式二虽然已经离开了b的作用域,但在此之后没有任何对局部变量表的读写操作,b原本所占用的Slot还没有被其它变量所复用,所以作为GC Roots一部分的局部变量表仍然保持着对他的关联。【也可以通过手动设置b = null,来达到回收的效果】但,方式二,经过JIT编译后,可以回收,所以无需像方式三那样处理。

注意:局部变量没有默认值。

  • 操作数栈

在编译的时候将最大深度写入到Code属性的max_stacks数据项中。两个栈桢作为虚拟机元素,是完全独立的。但在大多虚拟机的实现里都会做一些优化处理,令两个栈桢一部分重叠,这样在方法调用时可以共用一部分数据,无需进行额外的参数复制传递。

Java虚拟机是基于栈的执行引擎,其中所指的栈就是【操作数栈】。

  • 动态连接

每个栈桢都包含一个指向运行时【常量池】中该栈桢所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存有大量的【符号引用】,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分在类加载阶段或第一次使用的时候就转化为直接引用,这种转化称为【静态解析,如:静态方法、私有方法、实例构造器、父类方法。即编译期可知,运行期间不可变】;另外一部分在每次运行期间转化为直接引用,这种转换称为【动态连接】。

2. 方法调用

invokestatic
invokespecial
invokevirtual
invointerface
invokedynamic【分派逻辑不是由虚拟机决定的,而是由程序员决定】

Class文件的编译过程中不包含传统编译中的连接步骤一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址。此特性可以在类加载期间,或在运行期间再确定目标方法的直接引用,使动态扩展能力更强。

静态解析的字节码指令:invokestatic 、 invokespecial【实例构造器、私有方法、父类方法】和被final修饰的方法。在类加载的时候把符号引用解析为直接引用。

  • 静态分派【针对重载】
public class StaticDispatch {
  static abstract class Human {
  }

  static class Man extends Human {
  }

  public void say(Human obj) {
    System.out.println("human");
  }

  public void say(Man obj) {
    System.out.println("man");
  }

  public static void main(String[] args) {
    Human man = new Man();
    StaticDispatch st = new StaticDispatch();
    st.say(man); //human
  }
}

输出结果为:human。
Human man = new Man()中,Human为变量的【静态类型】;Man为变量的【实际类型】。静态类型在编译期可知,而实际类型在运行期才可确定。重载是通过参数的静态类型而不是实际类型作为判断依据。所以,在编译阶段就决定了使用哪个重载版本。

  • 动态分派【针对多态的覆盖】

运行期间根据【实际类型】确定方法执行版本的分派过程称为动态分配。
在类方法区中建立一个【虚方法表】,使用虚方法表索引来确定各个方法的实际入口。具有相同签名的方法,在父类、子类的虚方法表中都具有一样的索引号

3. 动态语言支持

JDK7字节码指令集中添加【invokedynamic指令】来支持动态类型语言,也是为JDK8中的Lambda表达式做准备。

动态语言关键特征:类型检查在运行期而不是编译期。
特点:变量无类型而变量值才有类型。对Java虚拟机而言,它可以同时支持静态语言和动态语言【Groovy、JRuby】。

由于invokestatic、invokespecial、invokevirtual、invointerface的第一个参数都是【被调用方法的符号引用】,该符号引用在编译时产生,而动态语言只有在运行期间才能确定接收者的类型,这种底层问题只有在虚拟机层次上去解决才是最合适的,因此才有invokedynamic指令诞生。

java.lang.invoke包在之前单纯依靠符号引用来确定调用的目标方法之外,提供了一种新的动态目标方法机制,称为【MethodHandle,类似于函数指针】。

MethodHandler效果与Reflection反射类似都是在模拟方法调用,其区别如下:
1)Reflection在模拟Java代码层次的方法调用,而MethodHandler在模拟字节码层次的方法调用。
2)Reflection是重量级的,它包含方法的签名、描述符、方法属性表等;而MethodHandler是轻量级的,只包含执行该方法相关信息。
3)Reflection只是为Java语言设计的,而MethodHandler可服务于所有Java虚拟机上的语言

  • 问题:通过super可以调用父类中的方法,如何调用祖父类中的方法呢?
package com.ljg;

import android.os.Build;
import android.support.annotation.RequiresApi;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;

import static java.lang.invoke.MethodHandles.lookup;

public class Test {
  class GrandFather {
    void say() {
      System.out.println("grandFather");
    }
  }

  class Father extends GrandFather {
    void say() {
      System.out.println("father");
    }
  }

  class Son extends Father {
    void say() {
      //调用父类中的方法
      super.say();
      //调用祖父类中的方法
      try {
        MethodType methodType = MethodType.methodType(void.class);
        MethodHandle methodHandle =
            lookup().findSpecial(GrandFather.class, "say", methodType, getClass());
        methodHandle.invoke(this);
      } catch (Throwable throwable) {
        throwable.printStackTrace();
      }
    }
  }
}

4. 基于栈的指令集与基于寄存器的指令集

主流的PC机的指令集架构是依赖【寄存器】进行工作的。

  • 1+1基于栈的指令集:
    iconst_1
    iconst_1
    iadd
    istore_0【把栈顶的值放到局部变量表的第0个Slot中】

  • 1+1基于寄存器的指令集:
    mov eax, 1
    add eax, 1【结果保存在EAX寄存器中】

  • 基于栈的指令集的【优点】:
    1)可移植。寄存器是由硬件直接提供的,而使用栈架构的指令集,用户程序不会直接使用寄存器,虚拟机可自行将一些访问频繁的数据【如:程序计数器、栈顶缓存】放到寄存器中以获得更好的性能。
    2)代码紧凑
    3)编译器实现简单

  • 基于栈的指令集的【缺点】:
    执行速度慢。原因:
    1)指令数量一般比寄存器架构多,因为出栈入栈操作就会产生很多指令。
    2)栈实现在内存中,频繁的栈访问也就意味着频繁的内存访问。尽管虚拟机可以采用【栈顶缓存】的手段,把最常用的操作映射到寄存器中避免直接访问内存,但这只是优化措施而不是解决问题的本质。
    所以,由于指令数量和内存访问的原因,导致栈架构的指令集执行速度慢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容