深入理解JVM内部结构(续)

如果你编译下面这个简单的类:
package org.jvminternals;

public class SimpleClass {

    public void sayHello() {
        System.out.println("Hello");
    }

}
之后你可以通过运行如下的javap命令,获得字节码信息,如:
javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class
 
public class org.jvminternals.SimpleClass
  SourceFile: "SimpleClass.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#17         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            //  "Hello"
   #4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            //  org/jvminternals/SimpleClass
   #6 = Class              #24            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/jvminternals/SimpleClass;
  #14 = Utf8               sayHello
  #15 = Utf8               SourceFile
  #16 = Utf8               SimpleClass.java
  #17 = NameAndType        #7:#8          //  "<init>":()V
  #18 = Class              #25            //  java/lang/System
  #19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;
  #20 = Utf8               Hello
  #21 = Class              #28            //  java/io/PrintStream
  #22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V
  #23 = Utf8               org/jvminternals/SimpleClass
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (Ljava/lang/String;)V
{
  public org.jvminternals.SimpleClass();
    Signature: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
        0: aload_0
        1: invokespecial #1    // Method java/lang/Object."<init>":()V
        4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0      5      0    this   Lorg/jvminternals/SimpleClass;

  public void sayHello();
    Signature: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
        0: getstatic      #2    // Field java/lang/System.out:Ljava/io/PrintStream;
        3: ldc            #3    // String "Hello"
        5: invokevirtual  #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0      9      0    this   Lorg/jvminternals/SimpleClass;
}
 
此类文件有三个主要部分:常量池、构造函数和sayHello方法。

  • 常量池, 它提供了与符号表相同的信息,这个将在后面进行详述。
  • 方法, 每个方法包含了四个部分:
    • 签名及访问标记
    • 字节码
    • 行号表, 它主要用于为调试器提供信息,行号表指示了每个字节码指令对应的行数,如Java代码中的第六行对应了sayHello方法中的字节码0,第七行对应了字节码8.
  • 局部变量表, 它列出了帧中提供的所有局部变量,在本例子中,只有this这一个局部变量。
此类文件中使用到的字节码操作数有:
aload_0
此操作码是格式为aload_<n>的操作码组的成员之一,它们都是用于加载一个对象引用到操作
对象栈中. <n>指示了对象在局部变量数组中的位置,可选的数字只能是0,1,2,3之一。还有其它类似的操作
码用于加载数值,而非对象引用。如iload_<n>,lload_<n>, fload_<n>和dload_<n>,其中i是指int类型
l是long,f是float,d是double类型。对于索引值超过3的局部变量,可以使用iload,lload,fload,dload进行
加载。这些操作码都只有一个操作数,用于指定要加载的局部变量的索引。
ldc
这个指令(操作码)用于将一个运行时常量池中的常量压入操作对象栈中
getstatic
此指令用于将运行时常量池的静态字段列表中的一个静态值压入到操作对象栈中。
invokespecial,
invokevirtual
这两个指令属于调用方法的指令组成员之一,调用方法的指令有:invokedynamic,invokestatic,
invokevirtual。在这个类文件中,invokevirtual用于基于对象的类进行方法调用,而invokespecial指令则用
于调用当前类的实例初始化方法,私有方法及父类方法。
return
此指令时返回型指令组成员之一,其它的还有ireturn,lreturn,freturn,dreturn,areturn及
return。每个指令都是一类返回不同类型数值的返回声明,i是int类型,l是long,f是float,d是
double,a则是对象引用。没有前置类型字符的return则是返回void类型。

在任何典型的字节码中,局部变量、操作对象栈和运行时常量池间操作数交互的主要过程如下:

       构造函数中有两条指令,首先是aload_0将this压入到操作对象栈中,之后调用父类的构造函数,并使用this对象,因此this从操作对象栈中出栈。
       sayHello()方法比构造函数复杂,因为它必须要使用运行时常量池将符号引用解析为直接引用,可参考上一篇翻译文章。第一个指令getstatic用于压入一个到System类的静态字段out的引用到操作对象栈中,下一个指令ldc把字符串“Hello”压入到操作对象栈中,最后的指令invokevirtual调用System.out的println方法,将“Hello”出栈并作为println方法的一个参数,为当前线程创建一个新的帧。整个过程如下:
      
(7) 类加载器
       JVM通过使用bootstrap类加载器加载一个初始类进行启动。初始类在调用public static void main(String [])方法之前被链接和初始化。main方法的执行最终驱使了其它需要的附加类和接口的加载、链接和初始化。
       加载是根据其特定的名字找到表示类或接口类型的类文件,并将其读取到byte数组中的过程。下一步将会解析byte数组,以确保它们表示的是一个类文件并具有正确的主及副版本号。此类的任何直接父类也会被加载。一旦加载过程完成,一个类或接口对象将被依据其二进制表示创建。
       链接是进行类或接口的验证,准备类型及其直接父类和实现的接口的过程。链接包含了三个步骤:验证、准备和可选的解析。
1. 验证,是确定类或接口的表示是结构正确的,且遵守Java编程语言及JVM的语义要求,比如说它应该具有合适的符号表。
2. 准备,它涉及了静态存储空间的分配和JVM使用到的任何数据结构,如方法表的分配。静态字段被创建并初始化为它们的默认值,然而,此时没有初始化块或代码被执行,因此这些是在初始化阶段进行。
3. 解析,它是一个可选的阶段,包含有通过加载引用类或接口来检查符号引用,确认引用是正确的。如果没有在这里进行符号引用的解析,则可以将其推迟到字节码指令使用此符号引用之前进行。

       类或接口的初始化包含执行类或接口的初始化方法<clinit>.
class loading, linking and initialization in the Java Virtual Machine (JVM)
       在JVM中有多个类加载器,它们扮演了不同的角色。每个类的加载被委托给它的父类加载器,除了bootstrap类加载器,因此它是最上层的类加载器。Bootstrap类加载器的责任是加载基本的Java API,如rt.jar, 它只加载在具有更高的信任级别的根类路径中发现的类,也正因如此,它省去了许多对于普通的类需要进行的验证工作。JVM还包含了一个扩展类加载器(extension class loader),它用于加载标准Java 扩展API中的类,如安全扩展功能。而系统类加载器(system class loader)是默认的应用类加载器,它从classpath中加载应用类。
       用户自定义类加载器也可以作为应用类加载器。使用用户自定义类加载器的原因有许多,如运行时重加载类,分开不同组的加载类,这个功能一般web服务器都需要用到,如Tomcat。
class loader hierarchy in the Java Virtual Machine (JVM)


(8) 加速类的加载
       从5.0版本开始,Hotspot JVM中引入了一个称为类数据共享(Class Data Sharing,CDS)的特色功能。在JVM的安装过程中,安装程序加载一系列的关键JVM类到一个内存映射的共享档案中(memory-mapped shared archive),如rt.jar。CDS可以减少用于加载这些类的时间,从而提高JVM的启动速度,并允许不同的JVM实例共享这些类,减少内存的使用。

(9) 方法区位于何处?
       Java虚拟机规范中清楚的说明了:“尽管方法区逻辑上是属于堆的一部分,简单实现也许会选择既不对其进行垃圾回收,也不进行压缩。”与jconsole中显示Oracle JVM中方法区属于非堆内存相反,OpenJDK的代码显示CodeCache是VM的ObjectHeap的一个独立字段。

(10) 类加载器的引用
        所有被加载的类都包含了一个到加载它们的类加载器的引用。反过来,类加载器同样包含了一个到它加载的所有类的引用。

(11) 运行时常量池
        JVM为每种类型都维护了一个对应的常量池,常量池是一个运行时数据结构,有点类似于符号表,不过他包含了更多的数据。Java中的字节码需要数据,通常这些数据太大而无法直接存储在字节码中,因此将它们存储在常量池中,而字节码包含一个到常量池的引用。
       有几种类型的数据会被存储在常量池中,如:
  • 数值型字面值
  • 字符串字面值
  • 类引用
  • 字段引用
  • 方法引用
       如下面的代码:
Object foo = new Object();
       其对应的字节码如下:
 0: 	new #2 		    // Class java/lang/Object
 1:	dup
 2:	invokespecial #3    // Method java/ lang/Object "<init>"( ) V
       new操作码后面跟着#2操作数,此操作数是一个常量池的索引,指向常量池中的第二个实体,第二个实体是一个类引用,此实体转而引用常量池中另一个包含类名字的实体,类的名字用一个值为java/lang/Object的UTF8字符串常量表示。之后,此符号链接可以用于查询java.lang.Object类。new操作码创建一个类实例并初始化它的变量,一个指向新创建的类实例的引用被添加到操作对象栈中。dup操作码则在操作对象栈顶创建此引用的两份拷贝。最后,通过invokespecial指令调用实例的初始化方法。这个指令的操作数同样包含了一个到常量池的引用,此初始化方法消耗操作数池顶端的一个引用作为方法的参数,最后,产生了一个指向已经被创建并初始化的新对象的引用。

(12) 异常表
       异常表存储了每个异常处理器信息,如:
  • 起始点
  • 结束点
  • 处理器代码的PC偏移值
  • 被捕抓的异常类的常量池索引
       如果一个方法定义了一个try-catch或try-finally异常处理器,则一个异常表将会被创建。异常表包含了每个异常处理器或者是finally块的信息,如异常处理应用的范围,那种类型的异常会被处理及异常处理代码所在的位置。
当一个异常被抛出时,JVM会在当前方法中查找匹配的异常处理器,

你可能感兴趣的:(深入理解JVM内部结构(续))