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虚拟机

结束

更多相关文章

  1. java中匹配字符串中的中文字符(含中文标点的)
  2. Java中字符流和字节流到底有什么区别!!!
  3. String字符串的应用
  4. Java IO流系列(四)—— 从字节流及其缓冲区到转换流
  5. java 正则表达式查找某段字符串中所有小写字母开头的单词并统计
  6. Javascript 检查字符串是否是数字的几种方法
  7. JavaScript替换字符串中最后一个字符
  8. Java中怎么把字符串数组转为整形数组
  9. java里如何取出一个字符串中的数字?

随机推荐

  1. PHP 服务器端处理跨域问题
  2. OWASP 维护的 PHP 安全配置速查表
  3. Javascript 到 PHP 加密通讯的简单实现
  4. PHP+Ajax如何实现上传文件进度条动态显示
  5. php统计文件中的代码行数
  6. 如何用PHP迭代器来实现一个斐波纳契数列
  7. PHP中常用的加密解密方法总结
  8. php获取当前执行的php文件的文件名
  9. 如何解决php Function split() is deprec
  10. php调取摄像头实现拍照功能的方法