《深入理解Java虚拟机》笔记三

大部分内容都是《深入理解Java虚拟机上的内容》的总结,少部分内容是来自于网上或者自己的理解。读完应该会把没笔记的markdown文件放在 github上。

本部分笔记对应的是《深入理解Java虚拟机》第七章,第八章和第九章。

虚拟机类加载机制

加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using)和卸载(Unloading)7个阶段,其中验证,准备,解析3个阶段称为连接(Linking)。

加载,验证,准备,初始化和卸载这5个阶段的顺序是一定的。

加载的时机

  1. 遇到new,getstatic,putstatic,或invokestatic这4个字节码指令时,如果类没有进行过初始化,则需要先触发其初始化
  2. 使用反射类调用时,如果类没有进行初始化,则需要先触发其初始化
  3. 当初始化一个类的时候,发现其父类还没有初始化,则需要先触发父类
  4. 当虚拟机启动时,用户需要指定一个执行类的主类,虚拟机会先初始化这个主类
  5. 如果一个java.lang.invoke.MehtodHandle实例最后的解析结果REF_getStatic, REF_putStatic, REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先触发其初始化

类加载的过程

加载

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态数据结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问接口

验证(非常重要,但不必须)

  1. 文件格式的验证

    主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。通过这个验证之后,字节流才会进入内存的方法区中存储,所以后面的3个验证阶段都是基于方法区的存储结构进行的。

    1. 元数据验证

      对字节码描述的信息进行语义分析,保证不存在不符合Java语言规范的元数据信息。

    2. 字节码验证

      通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这个阶段对类的方法体进行校验分析。

    3. 符号引用验证

      确保解析动作能够正常的进行。

#### 准备

是正式为**类变量**分配内存并设置**类变量**初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里的初始值“通常情况下”是数据类型的零值。

public static int value = 123

这里value的值是0。

如果类字段的字段属性表中存在ConstantValue,则会在准备阶段就进行赋值

public static final int value = 123

这时value的值是123。

#### 解析

虚拟机将常量池内的符号引用替换为直接引用的过程。

**符号引用(Symbolic Reference)**

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位当前目标即可。**符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。**各个虚拟机实现的内存布局可以不同,但是它们能接受的符号引用必须都是一直的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件中。

**直接引用(Direct Reference)**

直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局有关。如果有了直接引用,那引用的目标并定已经在内存中实现了。

#### 初始化

**类初始化是类加载过程中的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全是由虚拟机主导和控制的。**

初始化阶段时执行类构造器()方法的过程。

()是有编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。

### 类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性

#### 双亲委派模型

从Java虚拟机的角度来讲

*   启动类加载器(Bootstrap ClassLoader)这个类加载器使用C++语言实现,是虚拟机的一部分。
    
*   所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并都继承抽象类java.lang.ClassLoader。
    

从Java开发人员角度分为是分为3种

*   启动类加载器,负责加载\\lib目录,或被-Xbootclasspath指定的路径,并被虚拟机识别的类库加载到虚拟机内存中。
    
*   扩展类加载器(Extension ClassLoader),负责加载\\lib\\ext目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
    
*   应用程序类加载器(Application ClassLoader),又叫系统类加载器,一般情况下这个就是程序中默认的类加载器。
    

**工作流程**

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己加载。

虚拟机字节码执行引擎
----------

### 运行时栈帧结构

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈道出栈的过程。

只有位于栈顶的栈帧才是有效,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法叫做当前方法(Current Mehod)。

#### 局部变量表

局部变量表(Local Varible Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性中确定了最大容量。

局部变量表的容量以容量槽(Varible Slot)为最小单位。

**由于局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连接的Slot是否为原子操作,都不会一起数据安全问题。**

在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程。先是分配参数,之后再根据方法体内部定义的变量顺序和作用域分配其余的Slot

#### 操作数栈

操作数栈(Operand Stack),当方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取数据。

#### 动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属的方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

#### 方法返回地址

第一种是执行引擎遇到了任意一个方法返回的字节码指令。这种退出方法称为正常完成出口(Normal Method Invocation Completion)

另一种,在方法执行遇到了异常,并且这个异常没有在方法体内得到处理。无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常。只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)

### 方法调用

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,暂时还不涉及方法内部的具体运行过程。

#### 解析

字节码中的方法调用指令就一常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转换为直接引用,这种转化称为**静态解析**。另外一部分将在每一次运行期间转化为直接引用,这部分称为**动态连接**。

在类加载阶段,会将其中的一部分符号引用,转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的目标,并且这个方法调用版本在运行期不可修改。主要包括静态方法和私有方法两大类。

**非虚方法**

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法,私有方法,实例构造器,父类方法,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法称为非虚方法。

final方法也是一种非虚方法。

### 分派

#### 静态分派

所有依赖静态类型来定位执行版本的分派动作,称为静态分派。静态分派的典型应用是**方法重载**。静态分派发生在**编译阶段**,**因此确定静态分派的动作实际上不是由虚拟机执行。**有时自变量不需要定义,所以字面量没有显示的静态类型,它的静态类型只能通过语言上的规则区理解和推断。

#### 动态分派

动态分派和**重写**有着密切关系。

**实现**

为类在方法区中建立一个虚方法表(Virtual Mehtod Table,在invokeinterface执行时也会用到接口方法表Inteface Method Table)。虚方法表中存放着各个方法的实际入口地址。

**方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完成。**

#### 单分派和多分派

方法的接收者与方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以分派划分为单分派和多分派。

单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。

#### 动态类型支持

java.lang.invoke

### 基于栈的字节码解释执行引擎

Java编译器输出的指令流,基本上是一种基于栈架构的指令集架构(Instruction Set Architecture IA),指令流中的指令大部分都是基于零地址指令,他们依赖于操作数栈进行工作的。

基于栈的指令集主要优点就是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免的受到硬件约束。主要缺点,执行速度相对会稍慢一点。

类加载及执行子系统的案例与实战
---------------

### Tomcat

在Tomcat目录结构中,有3组目录(“/common/\*”,“/server/\*”和“/shared/\*”)可以存放Java类库,另外还可以加上Web应用程序自身的目录“WEB-INF”

放置在/common目录中:类库可以被Tomcat和所有的Web应用程序共同使用

放置上/server目录中:类库可以被Tomcat使用,对所有的Web应用程序都可不见

放置在/shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见

放置在/WebApp/WEB-INF目录中:类库仅仅被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见

### OSGI

一种动态模块化规范

你可能感兴趣的:(java)