JVM 内部原理(五)— 基本概念之 Java 虚拟机官方规范文档,第 7 版

时间:2021-01-13 20:06:37

JVM 内部原理(五)— 基本概念之 Java 虚拟机官方规范文档,第 7 版

介绍

版本:Java SE 7

每位使用 Java 的程序员都知道 Java 字节码在 Java 运行时(JRE - Java Runtime Environment)里运行。Java 虚拟机(JVM - Java Virtual Machine)是 Java 运行时(JRE)的重要组成部分,它可以分析和执行 Java 字节码。Java 程序员不需要知道 JVM 是如何工作的。有很多应用程序和应用程序库都已开发完成,但是它们并不需要开发者对 JVM 有深入的理解。但是,如果你理解 JVM ,那么就可以对 Java 更有了解,这也使得那些看似简单而又难以解决的问题得以解决。

在本篇文章中,我会解释 JVM 是如何工作的,它的结构如何,字节码是如何执行的及其执行顺序,与一些常见的错误及其解决方案,还有 Java 7 的新特性。

目录

  • 虚拟机(Virtual Machine)

  • Java 字节码

    • 症状

    • 原因

  • 类文件格式(Class File Format)

    • 症状

    • 原因

  • JVM 结构

    • 类装载器(Class Loader)

    • 运行时数据区

    • 执行引擎

  • Java 虚拟机官方规范文档,第 7 版

    • 分支语句中的字符串
  • 总结

内容

Java 虚拟机官方规范文档,第 7 版

在 2011 年 7 月 28 日,Oracle 发布了 Java SE 7 并更新了 JVM 官方规范文档至 Java SE 7 的版本。在 1999 年发布《Java 虚拟机官方规范文档,第二版》后,Oracle 花了 12 年时间做这版更新。更新版本的内容包括这 12 年来积累的各种变更修改,规范文档的描述更为清晰。除此之外,它还反映了《Java 语言规范文档,第七版》的内容。主要的更新概括如下:

  • Java SE 5.0 引入泛型,支持方法的参数变量。
  • 字节码验证过程的技术从 Java SE 6 开始发生变化。
  • 增加 invokedynamic 指令以及相关的类文件格式支持动态类型语言。
  • 删除了对于 Java 语言本身的概念性描述,并将其归入《Java 语言规范文档》中。
  • 删除了关于 Java 线程和锁的描述,并将其写入《Java 语言规范文档》。

最大的改变要数增加 invokedynamic 指令。这也意味着 JVM 内部指令集发生了变化,也就是说 JVM 从 Java SE 7 开始支持类型非固定的动态类型语言,如脚本语言,以及动态的 Java 语言。之前没有使用的操作码(OpCode)186 被应用到新指令 invokedynamic 以及新的类文件格式中以支持动态性,

由 Java 编译器 Java SE 7 创建的类文件版本是 51.0 。Java SE 6 的版本是 50.0 。类文件格式发生了很大变化,因此 51.0 版本的类文件不能运行于 Java SE 6 的 JVM 。

尽管有如此之多的变化,Java 方法的 65535 字节长度限制并没有被移除。除非 JVM 类文件格式发生了创新式的变化,否则它也不太可能在将来移除。

Oracle Java SE 7 VM 支持 G1 ,这个新的垃圾回收机制;不过,它仅限于 Oracle JVM ,所以 JVM 本身并不受限于任何垃圾回收机制。因此,JVM 官方规范文档并没有对此进行描述。

分支语句中的字符串

Java SE 7 增加了多种语法和特性。不过,与 Java SE 7 中语言发生的许多变化相比,JVM 并没有发生很多变化。那么,Java SE 7 的新特性是如何实现的呢?我们通过反编译看看 String 在分支语句中(一个将字符串传入 switch() 语句进行比较的功能)的实现方式。

有如下代码:

// SwitchTest
public class SwitchTest {
    public int doSwitch(String str) {
        switch (str) {
        case "abc":        return 1;
        case "123":        return 2;
        default:         return 0;
        }
    }
}

因为它是 Java SE 7 的新功能,所以它不能使用 Java SE 6 或更低版本的编译器来编译。用 Java SE 7 的 javac 编译它。用 javap -c 显示编译结果:

C:Test>javap -c SwitchTest.classCompiled from "SwitchTest.java"
public class SwitchTest {
  public SwitchTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return  public int doSwitch(java.lang.String);
    Code:
       0: aload_1
       1: astore_2
       2: iconst_m1
       3: istore_3
       4: aload_2
       5: invokevirtual #2                  // Method java/lang/String.hashCode:()I
       8: lookupswitch  { // 2
                 48690: 50
                 96354: 36
               default: 61
          }
      36: aload_2
      37: ldc           #3                  // String abc
      39: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      42: ifeq          61
      45: iconst_0
      46: istore_3
      47: goto          61
      50: aload_2
      51: ldc           #5                  // String 123
      53: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      56: ifeq          61
      59: iconst_1
      60: istore_3
      61: iload_3
      62: lookupswitch  { // 2
                     0: 88
                     1: 90
               default: 92
          }
      88: iconst_1
      89: ireturn
      90: iconst_2
      91: ireturn
      92: iconst_0
      93: ireturn

字节码的内容要比 Java 源代码多很多。首先,可以看到 lookupswitch 指令已经被 switch() 语句使用到字节码中。但是,有两个 lookupswitch 指令而不是一个。反编译后可以看到 int 传入 switch() 语句,但是只有一个 lookupswitch 指令用到了。也就是说 switch() 语句被拆分成了两个语句来处理字符串。查看 #5、#39 和 #53 号字节指令看 switch() 语句是如何处理字符串的。

在 #5 和 #8 号字节中,首先,hashCode() 方法被执行,然后 switch(int) 是通过使用 hashCode() 方法的运行结果来执行的。在 lookupswitch 指令的括号中,分支根据 hashCode 的值来定位到不同地方。字符串 “abc” 的 hashCode 结果是 96354 ,被定位到 #36 号字节。字符串 “123” 的 hashCode 结果是 48690 ,被定位到 #50 号字节。

在 #36、#37、#39 和 #42 号字节中,可以发现接收的 str 变量值作为参数与 String “abc” 和 equals() 方法比较。如果结果相同,“0” 被插入到 #3 号本地变量列表的索引位置,字符串被移动到 #61 号字节。

通过这种方式,在 #50、#51、#53 和 #56 字节中,可以发现接收的 str 变量值作为参数与 String “123” 和 equals() 方法比较。如果结果相同,“1” 被插入到 #3 号本地变量列表的索引位置,字符串被移动到 #61 号字节。

在 #61 和 #62 号字节中,以 #3 号本地变量列表的索引位置的值,如:“0”、“1” 或其他值,进行 lookupswitch 和分支处理。

换句话说,在 Java 代码中,接收的 str 变量值作为 switch() 参数使用 hashCode() 方法和 equals() 方法进行比较。switch() 方法根据 int 值的结果执行。

在这个结果中,被编译的字节码与之前的 JVM 规范文档并没有任何不同。Java SE 7 的新特性,字符串分支语句,是由 Java 编译器来处理的,而不是 JVM 本身。以类似的方式,Java SE 7 的其他新特性也是通过 Java 编译器进行处理的。

总结

这里评审 Java 语言是如何设计让 Java 可以更容易的使用没有太大必要。有很多 Java 程序员并没有对 JVM 有很深入的了解,却也开发出了很多优秀的应用和库。不过,如果能够深入理解 JVM ,我们就能处理这些例子中出现的问题。

除了这里提到的内容,JVM 有很多特性和技术。JVM 规范文档为 JVM 厂商提供了灵活的空间,让他们应用各种不同的技术从而提供更好的性能。特别是垃圾回收技术,它被大多数语言使用,提供与 VM 类似的使用方式以及最新最前沿的性能优化技术。但是,这些内容在很多卓越的研究中都会被讨论到,在此不作深入解释。

参考

参考来源:

JVM Specification SE 7 - Run-Time Data Areas

2011.01 Java Bytecode Fundamentals

2012.02 Understanding JVM Internals

2013.04 JVM Run-Time Data Areas

Chapter 5 of Inside the Java Virtual Machine

2012.10 Understanding JVM Internals, from Basic Structure to Java SE 7 Features

2016.05 深入理解java虚拟机

结束