我理解中的JVM

前阵子amazon搞优惠活动,随便浏览了一下,发现了几本想看的书,其中一本就是《自己动手写Java虚拟机》,一是最近正好在学java,二是对go语言比较好奇,于是就下单了。书挺薄的,300页不到,期末考完花了一个多星期的时间看完了,本文算是对这本书的一点总结吧。

全书主要讨论了以下几个方面:

类加载器

顾名思义,类加载器就是把类从文件加载到内存中。Java程序通常是一个类放一个文件,并且会被编译成平台无关的字节码,成为一个个独立的class文件。类加载器工作步骤如下:

  1. 获得类名,通常是java/lang/Object.class的形式
  2. path/to/jre/libpath/to/jre/lib/ext和用户指定的classpath搜索类文件,找到就读入到内存中,否则出错
  3. 读入内存中的是原始的二进制数据,然后就需要对这一串数据进行解析,将其解析成一个结构体,大致如下:(书中先解析成class_file结构体,然后又进一步解析成了class结构体)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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];
    }
  4. 最后进行链接。链接包括验证和准备阶段,验证阶段对类进行验证以确保安全性,准备阶段给类变量分配空间并且给予初值。

这一部分比较容易,涉及到一点设计模式,例如组合模式。但有一个需要注意的地方是,数组和字符串相比于普通的类有点不一样。
数组的类由JVM在运行时生成,并且创建数组的方式和创建普通对象的方式不同,以及数组和普通对象存放的数据也是不同的。加载数组类的时候,需要自行完善class结构体的各个部分。
对于字符串,在class文件中是以MUTF-8格式保存的,在JVM运行期间,字符串被表示成java.lang.String对象的形式存在,在String对象内部,字符串又是以UTF-16格式保存的。新建字符串对象时,首先加载java.lang.String类,调用其构造函数,然后将读取到的数据以UTF-16格式存进去。

运行时数据区

JVM在运行的时候,需要一个地方,来存放各种各样的数据,比如类信息、各种变量等,这部分功能由运行时数据区来实现。运行时数据区可以分成两类:多线程共享的线程私有的

  1. 多线程共享的运行时数据区
    这一部分主要存放两类数据,类数据类实例。类实例存放在堆中,类数据存放在方法区中。根据书上的图来理解的话,多线程共享的运行时数据区就是一个堆,里面包含了方法区,同时还有其他对象实例。堆由垃圾收集器定期清理。

  2. 线程私有的运行数据区
    线程私有的运行时数据区用于辅助执行字节码。这里以线程为单位,每个线程拥有自己的JVM栈和PC寄存器,PC寄存器用来保存当前正在执行的指令的地址,JVM栈中保存了一个个JVM栈帧,一个帧中包含了一个局部变量表和操作数栈。
    操作数栈主要用来进行一些运算,有相应的指令把函数传入的参数和局部变量表中的数据推入操作数栈,进行运算时,就从操作数栈中弹出相应数量的操作数,进行运算,同时中间结果继续压入操作数栈,最后的结果会保存到局部变量表中。
    局部变量表主要用来存放局部变量的值。需要用到局部变量值的时候,会根据索引将其取出,并压入操作数栈。

这一部分占了很多代码,一开始看很乱,来来回来看了好多遍。大致理解了一下,其实有了线程私有的运行数据区就可以执行单个方法了,只要从类数据中找到某个方法,然后直接从那部分字节码开始执行就可以。顺序执行还是跳转,都由相应的指令控制,指令会设置线程的PC寄存器。方法区是为了方法调用准备的。
同时这一部分还讲到了类和字段符号引用的解析。因为在定义变量的时候,需要用到很多类名和字段等。类符号引用的解析,除了得到类数据外,还要考虑当前执行代码的类是否有权限访问那个类。字段也是如此,但还要考虑继承关系,找不到的情况下要在超类中递归查找。

方法调用

方法调用是很有意思的一部分。用作者的话来说,实现了方法调用,这个JVM“从只能在地上爬的baby变成了能够到处跑”。
从调用的角度来看,方法可以分为静态方法和实例方法,静态方法是静态绑定的,实例方法是动态绑定的。从实现的角度来看,用Java语言实现的方法叫做Java方法,其他无法用Java语言实现而用本地语言实现的方法叫做本地方法。

Java方法调用的过程

  1. 调用方法首先涉及到的一个问题就是方法符号引用的解析。其中,非接口方法符号引用接口方法符号引用的解析又是不同的。非接口方法符号引用的解析首先需要解析得到相应的类,根据方法名和描述符查找方法,考虑继承关系、接口以及访问权限。接口方法符号引用的解析则先在接口中查找,查不到再去超接口。
  2. 解析完成,就需要做方法调用和参数传递。JVM需要给这个方法创建一个新的JVM栈帧并推入栈顶,然后传递参数。传递的参数首先放在调用者的操作数栈中,只需要计算出参数的个数然后将其放到新JVM栈帧的局部变量表中就可以了。
  3. 方法执行完毕需要返回。返回指令通常把要返回的结果存入调用者的操作数栈,并且将之前新建的用于调用方法的栈帧从栈中弹出。

本地方法

  1. 本地方法注册,通过一个Map将key(类名+方法名+描述符)和实现函数建立一一对应的关系。
  2. 本地方法查找,从Map中查找本地方法。
  3. 本地方法调用。Java虚拟机规范并没有规定如何实现和调用本地方法,书的作者是通过定义虚拟机规范未定义的操作码指令来实现本地方法调用的。可以通过方法的访问标志来判断是不是本地方法,如果是本地方法的话,在新建方法对象放入方法区的时候就注入字节码(操作码+若干信息字节),在执行本地方法的时候,会跳到我们注入的字节码,其实就是一条指令,通过这条指令来调用本地方法。

指令集和解释器

解释器其实以上就有涉及,字节码的执行就是解释器来完成的。书的作者通过实现各种指令集来完成指令的解码。在读取字节码的过程中,先读取操作码,然后通过操作码生成新的指令,执行,计算下一条指令地址,重复上述过程,直至程序结束。书的作者实现了约200条指令,是个复杂并且需要细心耐心的工作。

总结差不多就到这吧,感谢作者。对我来说,这本书算是对JVM的一个启蒙教育吧。感觉时间越来越不够用,还是要不断学习呀。

文章目录
  1. 1. 类加载器
  2. 2. 运行时数据区
  3. 3. 方法调用
    1. 3.1. Java方法调用的过程
    2. 3.2. 本地方法
  4. 4. 指令集和解释器
|