深入拆解Java虚拟机笔记(1)虚拟机介绍、类的加载

一 Java 虚拟机具体是怎样运行Java 字节码的

以标准JDK 中的HotSpot 虚拟机为例

虚拟机视角

从虚拟机视角来看,执行Java 代码首先需要将它编译而成的class 文件加载到Java 虚拟机中。加载后的Java 类会被存放于方法区(Method Area)中。实际运行时,虚拟机会执行方法区内的代码
Java 虚拟机同样也在内存中划分出堆和栈来存储运行时数据
Java 虚拟机会将栈细分为面向Java 方法的Java 方法栈,面向本地方法(用C++写的native 方法)的本地方法栈,以及存放各个线程执行位置PC 寄存器。
在这里插入图片描述

在运行过程中,每当调用进入一个Java 方法,Java 虚拟机会在当前线程的Java 方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。这个栈帧的大小是提前计算好的,而且Java 虚拟机不要求栈帧在内存空间里连续分布
当退出当前执行的方法时,不管是正常返回还是异常返回,Java 虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。

硬件视角

Java 字节码无法直接执行。因此,Java 虚拟机需要将字节码翻译(前面是编译)成机器码。

在HotSpot 里面,上述翻译过程有两种形式:

  1. 第一种是解释执行,即逐条将字节码翻译成机器码并执行;
  2. 第二种是即时编译(Just-In-Time compilation,JIT),即将一个方法中包含的所有字节码编译成机器码后再执行。
    在这里插入图片描述
    硬件只管执行机器码

编译/解释混合模式

前者的优势在于无需等待编译(编译快),而后者的优势在于实际运行速度更快。HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。

运行效率

HotSpot 采用了多种技术来提升启动性能以及峰值性能,刚刚提到的即时编译便是其中最重要的技术之一。
即时编译建立在程序符合二八定律的假设上,也就是百分之二十的代码占据了百分之八十的计算资源。

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。

分层编译

从Java 7 开始,HotSpot 默认采用分层编译的方式:热点方法首先会被C1 编译,而后热点方法中的热点会进一步被C2 编译

  • C1 又叫做Client 编译器,面向的是对启动性能有要求的客户端GUI 程序,采用的优化手段相对简单,因此编译时间较短。
  • C2 又叫做Server 编译器,面向的是对峰值性能有要求的服务器端程序,采用的优化手段相对复杂,因此编译时间较长,但同时生成代码的执行效率较高

先快速启动,后慢慢提高峰值性能

二 类加载

class 文件内存中的类,按先后顺序需要经过加载、链接以及初始化三大步骤

2.1引用类型分类

Java 将其细分为四种:类、接口、数组类和泛型参数。由于泛型参数会在编译过程中被擦除,因此Java 虚拟机实际上只有前三种。在类、接口和数组类中,数组类是由Java 虚拟机直接生成的,其他两种则有对应的字节
流。
说到字节流,最常见的形式要属由Java 编译器生成的class 文件。除此之外,我们也可以在程序内部直接生成,或者从网络中获取(例如网页中内嵌的小程序Java applet)字节流。这些不同形式的字节流,都会被加载到Java 虚拟机中,成为类或接口.
无论是直接生成的数组类,还是加载的类,Java 虚拟机都需要对其进行链接和初始化。接下来,我会详细给你介绍一下每个步骤具体都在干些什么。
在这里插入图片描述

2.2加载

加载,是指查找字节流,并且据此创建类的过程。数组类是直接生成的,没有这一步!

启动类加载器

启动类加载器(boot classloader)。启动类加载器是由C++ 实现的,没有对应的Java 对象,因此在Java 中只能用null来指代。(所以我们找不到该对象)

Java 9之前,启动类加载器负责加载最为基础、最为重要的类,比如存放在JRE 的lib 目录下jar 包中的类(以及由虚拟机参数-Xbootclasspath 指定的类(人工指定加载))。除了启动类加载器之外,另外两个重要的类加载器是扩展类加载器(extension class loader)和应用类加载器(application class loader),均由Java 核心类库提供。

ClassLoader的各种子类

除了启动类加载器之外,其他的类加载器都是java.lang.ClassLoader 的子类,因此有对应的Java 对象。这些类加载器需要先由另一个类加载器,比如说启动类加载器,加载至Java 虚拟机中,方能执行类加载。(他们也要先被类加载器加载进来,才能去加载别的类)

  • 扩展类加载器的父类加载器是启动类加载器。它负责加载相对次要、但又通用的类,比如存放在JRE 的lib/ext 目录下jar 包中的类(以及由系统变量java.ext.dirs 指定的类)。
  • 应用类加载器的父类加载器则是扩展类加载器。它负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数-cp/-classpath、系统变量java.class.path 或环境变量CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。

双亲委派模型

每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。

自定义类加载器

除了由Java 核心类库提供的类加载器外,我们还可以加入自定义的类加载器,来实现特殊的加载方式。举例来说,我们可以对class 文件进行加密,加载时再利用自定义的类加载器对其解密。

命名空间

除了加载功能之外,类加载器还提供了命名空间的作用。
在Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。

2.3 链接

链接,是指将创建成的类合并至Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段

验证阶段

在于确保被加载类能够满足Java 虚拟机的约束条件。
通常而言,Java 编译器生成的类文件必然满足Java 虚拟机的约束条件。

准备阶段

目的是为被加载类的静态字段分配内存(因为静态字段是属于类的)。Java 代码中对静态字段的具体初始化(不在本阶段!!!),则会在稍后的初始化阶段中进行。
除了分配内存外,部分Java 虚拟机还会在此阶段构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的方法表。

解析阶段

目的,正是将这些符号引用解析成为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)

编译期是符号引用
在class 文件被加载至Java 虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个符号引用。在运行阶段,这个符号引用一般都能够无歧义地定位到具体目标上。

举例来说,对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。

Java 虚拟机规范并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码前,需要完成对这些符号引用的解析

在执行调用指令前,它所附带的符号引用需要被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用为目标方法的指针。对于需要动态绑定的方法调用而言,实际引用为辅助动态绑定的信息(方法表的索引)。

2.4 初始化

在Java 代码中,如果要初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码块中对其赋值。
如果直接赋值的静态字段被final 所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被Java 编译器标记成常量值(ConstantValue),其初始化直接由Java 虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被Java 编译器置于同一方法中.换句话说该阶段是执行类构造器<clinit>()方法的过程

就是静态变量初始化(非final修饰的)

Java 虚拟机会通过加锁来确保类的方法仅被执行一次。只有当初始化完成之后,类才正式成为可执行的状态。

2.5 类的初始化何时会被触发

  1. 当虚拟机启动时,初始化用户指定的主类;
  2. 当遇到用以新建目标类实例的new 指令时,初始化new 指令的目标类;
  3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
  4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;(静态内部类实现单例原理)
  5. 子类的初始化会触发父类的初始化
  6. 如果一个接口定义了default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
  7. 使用反射API 对某个类进行反射调用时,初始化这个类;
  8. 当初次调用MethodHandle 实例时,初始化该MethodHandle 指向的方法所在的类。

2.6 按顺序开始(交叉混合)

其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。另外,这个过程表示的是按顺序开始,不是所谓的第一步、第二步、第三步的关系,而往往是交叉混合进行,在一个阶段中可能调用或者激活另一个过程。

三 推荐阅读

发布了107 篇原创文章 · 获赞 1 · 访问量 3950

猜你喜欢

转载自blog.csdn.net/m0_38060977/article/details/104243716