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

From:深入理解Java虚拟机

  • 目录
    BiBi - JVM -0- 开篇
    BiBi - JVM -1- Java内存区域
    BiBi - JVM -2- 对象
    BiBi - JVM -3- 垃圾收集算法
    BiBi - JVM -4- HotSpot JVM
    BiBi - JVM -5- 垃圾回收器
    BiBi - JVM -6- 回收策略
    BiBi - JVM -7- Java类文件结构
    BiBi - JVM -8- 类加载机制
    BiBi - JVM -9- 类加载器
    BiBi - JVM -10- 虚拟机字节码
    BiBi - JVM -11- 编译期优化
    BiBi - JVM -12- 运行期优化
    BiBi - JVM -13- 并发

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

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)栈实现在内存中,频繁的栈访问也就意味着频繁的内存访问。尽管虚拟机可以采用【栈顶缓存】的手段,把最常用的操作映射到寄存器中避免直接访问内存,但这只是优化措施而不是解决问题的本质。
    所以,由于指令数量和内存访问的原因,导致栈架构的指令集执行速度慢。

你可能感兴趣的:(BiBi - JVM -10- 虚拟机字节码)