美文网首页
深入JVM内部

深入JVM内部

作者: anvoid | 来源:发表于2017-08-11 15:05 被阅读0次

    简书 溪渠
    转载请注明原创出处,谢谢!
    如果读完觉得有收获的话,欢迎点赞加关注

    Virtual Machine

    JRE由Java API以及JVM组合而成。JVM负责的工作就是通过ClassLoader识别Java程序,并且使用Java API执行它。
    一个Virtual Machine可以理解为使用软件方式实现的机器,可以像一台真实的物理机一样执行应用程序。最初,Java设定的运行环境就是隔离物理环境的虚拟机,以满足WORA(Write Once Run Anywhere)。因而JVM可以运行在各种各样的硬件之上,执行Java字节码,却不需要改变Java程序代码。

    JVM的一些特性如下:

    • Stack-based virtual machine:大多数流行的计算机架构(如Intel x86和ARM)基于寄存器运行,然而JVM基于栈运行。
    • Symbolic reference:所有的类型(class和interface,不包括原始类型)都是通过符号引用引用,而不是基于内存地址的引用。
    • Garbage collection:一个类的实例被用户代码创建,并由垃圾回收机制自动回收。
    • Guarantees platform independence by clearly defining the primitive data type:像C/C++这类语言对于不同的平台,int类型有不同的大小。JVM明确定义了原始类型的大小以保证兼容性和平台无关性。
    • Network byte order:Java的Class文件采用网络字节顺序,即big endian,这是为了保证平台无关性。

    Sun公司开发了Java,但是任何人都可以遵循Java虚拟机规范开发JVM。因此市面上有多款虚拟机,比较出名的如Oracle Hotspot JVM和IBM JVM。谷歌安卓系统中的Dalvik VM也是一款JVM,尽管它没有遵循Java虚拟机规范,它也是基于寄存器架构的。

    Java字节码

    为了实现WORA,JVM采用了Java字节码,它是一种介于用户开发语言和机器语言之间的中间语言,它也是部署Java代码的最小单位。

    在解释字节码之前,我们先看一个真实发生在开发过程中的例子。

    问题

    一个曾经成功运行的程序在更新了依赖库之后发生了异常。

    Exception in thread "main" java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V
        at com.nhn.service.UserService.add(UserService.java:14)
        at com.nhn.service.UserService.main(UserService.java:19)
    

    程序代码如下,并且编写后没有修改过。

    // UserService.java
    public void add(String userName) {
        admin.addUser(userName);
    }
    

    更新后的库代码以及原来库的代码如下:

    // UserAdmin.java - Updated library source code
    …
    public User addUser(String userName) {
        User user = new User(userName);
        User prevUser = userMap.put(userName, user);
        return prevUser;
    }
    // UserAdmin.java - Original library source code
    …
    public void addUser(String userName) {
        User user = new User(userName);
        userMap.put(userName, user);
    }
    

    简单来说addUser()方法由一个无返回值方法变成了返回User实例的方法。

    直观的来看com.nhn.user.UserAdmin.addUser()方法仍然是存在的,那么为什么会出现NoSuchMethodError?

    原因

    原因很简单,就是应用程序代码没有使用新的库重新编译,在UserAadmin类的Class文件中使用的仍然是无返回值方法,而新的库提供的是带返回值的方法。异常中提示了错误的原因。

    java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V
    

    NoSuchMethodError是由于"com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V"方法找不到产生的。在Java字节码的描述中,"L<classname>;"表示类的实例,最后一个V是方法的返回值,这里表示无返回值。

    让我们接着聊Java字节码,Java字节码是JVM必要的组成要素。Java编译器并不直接将高级语言转换为机器语言(CPU指令),它将开发者理解的语言转换为JVM理解的字节码。由于Java字节码是一种平台无关的代码,在不同的硬件环境或者不同的操作系统之上只要安装了JRE环境,那么它就可以执行,且不需要任何其他改变。字节码的大小几乎与源代码大小相同。

    实际上Java中Class文件是二进制文件,无法直接理解,因此JVM供应商提供了javap这个工具,可以解析字节码文件。在上面的例子中,通过javap -c UserAdmin.class得到如下内容:

    public void add(java.lang.String);
      Code:
       0:   aload_0
       1:   getfield        #15; //Field admin:Lcom/nhn/user/UserAdmin;
       4:   aload_1
       5:   invokevirtual   #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)V
       8:   return
    

    可以看到addUser方法在第四行被调用,"5: invokevirtual"。"#23"表示与索引23相关的方法要被调用,并且可以看到由javap工具注释的方法。invokevirtual是Java字节码中的一个OpCode,用于方法调用。在字节码中包含四种方法调用的OpCode:invokeinterface,invokespecial,invokestatic,invokevirual,它们的含义如下:

    • invokeinterface: 调用接口方法
    • invokespecial: 调用构造器方法,private方法,或者超类方法
    • invokestatic: 调用静态方法
    • invokevirtual: 调用实例方法

    Java字节码指令集由OpCodeOperand(操作数)组成,像invokevirtual这种OpCode需要2字节的Operand。

    当使用新的库编译上面的程序代码之后,反汇编得到的内容如下:

    public void add(java.lang.String);
      Code:
       0:   aload_0
       1:   getfield        #15; //Field admin:Lcom/nhn/user/UserAdmin;
       4:   aload_1
       5:   invokevirtual   #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;
       8:   pop
       9:   return
    

    我们可以看到#23表示的方法返回值变成了“Lcom/nhn/user/User”。

    在上面反汇编的结果中,代码前面的数字代表了什么含义?

    数字表示字节数,或者说是字节下标。OpCode本身使用一个byte进行表示,所以最多有256中字节码指令,上面例子中的一些指令的字节码表示为:
    aload_0=0x2a,getfield=0xb4,invokevirtual=0xb6
    aload_0和aload_1不需要Operand,而getfield需要2字节的Operand,所以aload_1字节数是4。在16进制编辑器中查看字节码如下:

    2a b4 00 0f 2b b6 00 17 57 b1
    

    在Java字节码中,“L;”表示类的实例,“V”表示void类型,下面的表格总结了其他类型的表示。
    表1:字节码中的类型表示

    字节码 类型 描述
    B byte signed byte
    C char Unicode字符
    D double 双精度浮点
    F float 单精度浮点
    I int integer
    J long long integer
    L<classname> reference 类实例
    S short signed short
    Z boolean true or false
    [ reference one array dimension

    表2:字节码示例

    Java Code Java Bytecode Expression
    double d[][][]; [[[D
    Object mymethod(int i, double d, Thread t) (IDLjava/lang/Threadd;)Ljava/lang/Object

    更多关于Java字节码指令集可以查看"The Java Virtual Machine Specification"中“6. The Java Virtual Machine Instruction Set”。

    Class文件格式

    Class文件格式概括如下:

    ClassFile {
        u4 magic;
        u2 minor_version;
        u2 major_version;
        u2 constant_pool_count;
        cp_info constant_pool[constant_pool_count-1];
        u2 access_flags;
        u2 this_class;
        u2 super_class;
        u2 interfaces_count;
        u2 interfaces[interfaces_count];
        u2 fields_count;
        field_info fields[fields_count];
        u2 methods_count;
        method_info methods[methods_count];
        u2 attributes_count;
        attribute_info attributes[attributes_count];}
    

    上面的内容可以在"The Java Virtual Machine Specification, Second Edition"中“4.1。 The ClassFile Structure”找到。UserService.class前16个byte如下。
    ca fe ba be 00 00 00 32 00 28 07 00 02 01 00 1b
    下面根据这个例子说明一下class文件格式。

    • magic:class文件的前4个byte是魔数,这是预先定义好的,用于区分class文件,值总是0xCAFEBABE。当文件的前4个byte是0xCAFEBABE时,可以认为它是Java class文件。
    • minor_version,major_version:接下来的4个字节表示class版本。使用JDK1.6编译的class版本是50,使用JDK1.5编译的class版本是49。JVM保持向后兼容性,这表示低版本编译的代码可以运行在高版本虚拟机中,而高版本编译的代码不能跑在低版本的JVM中,将会抛出java.lang.UnsupportedClassVersionError。
    • constant_pool_count,constant_pool[]:接下来是class的常量池信息。constant_pool_count是0x28,表示constant_pool有(40-1)个indexes。
    • access_flags:表示类的修饰语,比如public,final,abstract,interface等。
    • this_class,super_class:类以及超类在常量池中的index
    • interfaces_count,interfaces[]:常量池中记录接口数量的index以及所有的接口信息
    • fields_count,fields[]:类包含的字段数量以及所有字段信息。字段信息包含名称,类型信息,修饰语和在常量池中的index。
    • methods_count,methods[]:类中方法的数量以及所有方法的信息。方法的信息包含名称,类型,参数数量,返回类型,修饰语,在常量池中index,方法可执行代码,异常信息。
    • attributes_count,attributes[]:field_info和method_info会用到attribute_info结构。

    UserService.class使用javap -verbose解析后,打印的内容可以被我们所理解。

    Compiled from "UserService.java"
     
    public class com.nhn.service.UserService extends java.lang.Object
      SourceFile: "UserService.java"
      minor version: 0
      major version: 50
      Constant pool:const #1 = class        #2;     //  com/nhn/service/UserService
    const #2 = Asciz        com/nhn/service/UserService;
    const #3 = class        #4;     //  java/lang/Object
    const #4 = Asciz        java/lang/Object;
    const #5 = Asciz        admin;
    const #6 = Asciz        Lcom/nhn/user/UserAdmin;;// … omitted - constant pool continued …
     
    {
    // … omitted - method information …
     
    public void add(java.lang.String);
      Code:
       Stack=2, Locals=2, Args_size=2
       0:   aload_0
       1:   getfield        #15; //Field admin:Lcom/nhn/user/UserAdmin;
       4:   aload_1
       5:   invokevirtual   #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;
       8:   pop
       9:   return  LineNumberTable:
       line 14: 0
       line 15: 9  LocalVariableTable:
       Start  Length  Slot  Name   Signature
       0      10      0    this       Lcom/nhn/service/UserService;
       0      10      1    userName       Ljava/lang/String; // … Omitted - Other method information …
    }
    

    由于篇幅的限制,这里只截取了部分输出。

    JVM结构

    Java代码执行的流程如下。


    Java代码执行流程

    类加载器将字节码加载到运行时数据区,然后执行引擎执行字节码。

    类加载器

    Java提供了动态加载特性,类加载器在运行时只在第一次指向类时加载和连接类。类加载器特性如下:

    • 层次结构:Java中的类加载器被组织为一种父子关系的层次结构。Bootstrap类加载器是所有类加载器的parent。
    • 委托模式:当需要加载一个类时,首先在父类加载器中查找是否存在该类,如果有那么直接使用,如果没有,再由该类加载器加载该类。
    • 可见性限制:子类加载器可以在父类加载器中查找类,反之不可以。
    • 不允许卸载:类加载器可以加载类,不可以卸载类。需要卸载类时,可以删除当前类加载器,创建一个新的类加载器。

    每个类加载器都有自己的命名空间,其中保存了所有加载的类。当一个类加载器加载一个类时,通过全限定名称查找该类。两个拥有相同全限定名的类在不同的命名空间时,这两个类是不一样的,也说明它们由不同的类加载器加载。

    下图说明了类加载器的委托模型。

    类加载器委托模型

    当需要加载一个类时,按照上图顺序检查每个类加载器缓存中是否存在该类,如果在Bootstrap类加载器中也找不到,那么需要当前类加载器从文件系统中加载。

    • Bootstrap class loader:JVM启动时被创建,负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类。
    • Extension class loader:
    • 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
    • System class loader:负责加载classpath指定的jar包
    • User-defined class loader:开发者自定义实现的类加载器,如tomcat和jboss都会根据j2ee规范自行实现类加载器。

    一个类加载后的具体流程如图。

    类加载阶段

    各个阶段流程说明:

    • Loading:从文件中获取类字节码,并加载到JVM内存。
    • Verifying:验证该类是否遵循Java语言规范以及JVM规范。这个过程是加载中最为复杂和耗时的阶段。
    • Preparing:为保存类的字段、方法、接口等内容的数据结构分配内存。
    • Resolving:改变常量池中的符号引用为直接引用。
    • Initializing:执行静态初始化块,初始化静态变量。

    Runtime Data Area

    运行时数据区

    运行时数据区是JVM启动后分配的内存区域,分为六个部分,其中每个线程都有自己的PC Register(程序计数器)、JVM Stack(虚拟机栈)、Native Method Stack(本地方法栈),Heap(堆)、Method Area(方法区)、Runtime Constant Pool(运行时常量池)是被所有线程共享的。

    • 程序计数器:每个线程有自己的程序计数器,记录当前正在执行的JVM指令地址,这里不会发生GC。
    • 虚拟机栈:每个线程有自己的虚拟机栈,伴随着方法的调用与退出,栈帧进栈出栈。当有异常发生时,printStackTrace方法可以打印所有的栈帧。
    虚拟机栈

    栈帧:栈帧是在方法执行时创建的,加入线程的虚拟机栈,在方法结束时,从栈中移除。栈帧包含局部变量数组引用、操作数栈引用、当前类的运行时常量池引用。局部变量以及操作数栈在编译时已经确定了,所以方法的栈帧大小也是确定的。
    局部变量数组:数组下标是从0开始,0表示类的实例引用,从1开始表示的是方法的传入参数,紧接着入参,就是方法体内部的局部变量了。
    操作数栈:表示方法实际的工作空间。

    • 本地方法栈:这个栈中调用的都是本地方法,或者说是通过JNI调用的C/C++代码。
    • 方法区:方法去包括运行时常量池、字段信息、方法信息、静态变量、类和接口的字节码。在Hotspot虚拟机中,方法区叫做永久代。对于各个虚拟机供应商而言,方法区的垃圾回收是可选的。
    • 运行时常量池:这个区域包含在方法去中,它的内容与class文件中的constant_pool息息相关。它包含了所有方法、字段的引用,每当需要使用一个方法或者字段时,通过它找到方法或者字段实际的内存地址。
    • 堆:类的实例对象存储的地方,也是垃圾回收的目标区域。

    回到前面我们讨论过的反汇编后的class文件内容。

    public void add(java.lang.String);
      Code:
       0:   aload_0
       1:   getfield        #15; //Field admin:Lcom/nhn/user/UserAdmin;
       4:   aload_1
       5:   invokevirtual   #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;
       8:   pop
       9:   return
    

    与x86架构中汇编代码相比,它们具有类似的格式,每行都有一个指令,但是Java字节码中没有使用内存地址,或者操作数的偏移。这是因为JVM使用栈,上图中的15和23表示的是类中运行时常量池的索引,它是自己管理内存的。简而言之,JVM为每一个类创建了运行时常量池,而常量池中存储了实际操作对象的引用。

    相关文章

      网友评论

          本文标题:深入JVM内部

          本文链接:https://www.haomeiwen.com/subject/mealrxtx.html