JVM探索--虚拟机的结构
1.概述
Java虚拟机是整个Java平台的基石,可以把它理解为一个抽象的计算机,它有各种指令集和各种运行时数据区域。
实际上Java虚拟机有很多版本,如:Oracle的HotSpot、IBM的J9 VM等等,他们都是严格遵循Java虚拟机规范实现的,最主流的是HotSpot,大部分技术文章都是讲解HotSpot的,本文也是如此。
Android中的Dalvik和ART虚拟机并不属于JVM体系,因为没有严格遵循虚拟机规范实现,将在下一篇文章单独讲解。
当我们执行一个Java程序时,他的执行流程如下图所示:
可以看到执行流程分为2部分:编译时环境和运行时环境,java文件经过java编译器编译后生成class文件,程序执行时Java虚拟机会加载class文件并执行具体逻辑。class文件是二进制文件,与语言没有必然联系,Kotlin、Groovy、Scala等语言编译后也会生成class文件,都可以被虚拟机加载并执行。
2.体系结构
这里讲的体系结构指的是Java虚拟机的抽象行为,在Java虚拟机规范中定义,先看一下结构图:
上图中灰色框属于线程共享区域,白色框属于线程私有区域,类加载子系统并不属于JVM的内部结构。
下面具体讲解一下个区域的含义
2.1 类的生命周期
一个class文件被加载到虚拟机内存中到从内存中卸载的过程被称为类的生命周期。类的生命周期包括5个阶段:加载、链接、初始化、使用和卸载,其中链接又包括3个阶段:验证、准备和解析。如下图所示:
下面具体说明一下各个阶段的逻辑:
-
加载:查找并加载Class文件,主要做3件事
· 根据特定名称查找类或接口类型的二进制字节流。
· 将这个二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构。
· 在内存中生成一个代表这个类的 java.lang. C las s 对象,作为方法区这个类的各种数据的访问入口。 -
链接:包括验证、准备和解析
· 验证:确保被导入类型的正确性
· 准备:为类的静态字段分配内存,并用默认初始化这些字段
· 解析:将常量池中的符号引用替换位直接引用 -
初始化:将类的变量初始化为正确的初始值
其中第一个阶段是由java虚拟机外部的类加载子系统完成的,下面具体讲解类的加载子系统
2.2类加载子系统
类加载子系统使用各种类加载器来查找和加载指定的Class文件到java虚拟机中。Java虚拟机的类加载器分为2类:系统加载器和自定义加载器。系统加载器又包括以下3种:
1. 引导类加载器(Bootstrap ClassLoader)
用C/C++代码实现的加载器,用于加载指定的JDK核心类库,如:/JAVA_HOME/jre/lib 目录和-Xbootclasspath 参数指定的目录。
Java虚拟机的启动就是通过引导类加载器创建一个初始化类来完成的,由于使用C/C++代码实现,该加载器不能被Java访问到,但是可以查询某个类是否被引导类加载器加载过。
2. 拓展类加载器(Extentions ClassLoader)
用于加载Java的拓展类,如:加载$JAVA_HOME/jre/lib/ext 目录和系统属性 java.ext.dir 所指定的目录。
3. 应用程序加载器(Application Class Loader)
该加载器又叫系统类加载器,因为可以通过ClassLoader的getSystemClassLoader方法获取,可以加载以下目录:当前应用程序 Classpath 目录和系统属’性 java.class . path 指定的目录。
自定义加载器是通过继承java.lang.ClassLoader类的方式来实现自己的类加载器,可以实现特殊的加载逻辑。
2.3运行时数据区域
根据java虚拟机规范的定义,运行时数据区域包括:程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区,下面分别讲解。
1.程序计数器
程序计数器又叫PC寄存器,是一块较小的内存空间。在虚拟机概念模型中字节码解释器工作时时通过改变程序计数器来获取下一条字节码指令并执行,Java虚拟机的多线程是通过轮流切换分配处理器执行时间的方式实现的,在一个确定的时刻只有一个处理器执行一个线程中的指令,切换线程后必须能恢复到正确的执行位置,为了保证程序能够连续地执行下去,处理器必须具有某些手段来确定下一条指令的地址,而程序计数 器 正是起到这种作用 。每一个线程都有一个独立的线程计数器,如果线程执行的方也不是 Native 方泣,则程序计数器保存正在执行的字节码指令地址,如果是 Native 方怯则程序计数器的值为空。程序计数器是 Java 虚拟机规范中唯一没有规定任何 OutOfMemoryError 情况
的数据区域。
2.Java虚拟机栈
每一 条 Java 虚拟机线程都有 一个线程私有的 Java 虚拟机栈 (Java Virtual MachineStacks )。它的生命周期与线程相同,与线程是同时创建的 。 Java 虚拟机栈存储线程中 Java方怯调用的状态,包括局部变量、参数、返回值以及运算的中间结果等。 一个 Java 虚拟机栈包含了多个栈帧, 一个栈帧用来存储局部变量表、操作数栈、动态链接、方法出口等信
息。当线程掉用一个 Java 方法时,虚拟机压入一个新的栈帧到该线程的 Java 虚拟机栈中,在该方法执行完成后,这个栈帧就从 Java 虚拟机栈中弹出 。 我们平 常所说的栈内存( Stack)指的就是 Java 虚拟机栈。 Java 虚拟机规范中定义了两种异常情况 。
·如果线程请求分配的枝容量超过 Java 虚拟机所允许的最大容量 , Java 虚拟机会抛出StackOverflow Error 。
·如果 Java 虚拟机技可以动态扩展(大部分 Java 虚拟机都可以动态扩展),但是扩展时无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的Java 虚拟机栈,则会抛出 OutOfMemoryError 异常 。
3.本地方法栈
Java 虚拟机实现可能要用到 C Stacks 来支持 Native 语言 ,这个 C Stacks 就是本地方法栈( Native Method Stack )。它与 Java 虚拟机栈类似,只不过本地方法栈是用来支持 Native方法的。如果 Java 虚拟机不支持 Native 方法,并且也不依赖于 C Stacks ,可以无须支持本地方法栈。在 Java 虚拟机规范中对本地方法栈的语言和数据结构等没有强制规定,因此具体的 Java 虚拟机可以自由实现它,比如 HotSpotVM 将本地方法栈和 Java 虚拟机栈合二为一 。与 Java 虚拟机栈类似,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常 。
4.Java堆
Java 堆 (Java Heap )是被所有线程共享 的运行时内存区域。 Java 堆用来存放对象实例,几乎所有的对象实例都在这里分配内存。 Java 堆存储的对象被垃圾收集器管理,这些受管理的对象无法显式地销毁。从内存回收的角度来分, Java 堆可以粗略地分为新生代和老年代,从内存分配的角度 Java 堆中可能划分出多个线程私有的分配缓冲区。不管如何划分,Java 堆存储的内容是不变的,进行划分是为了能更快地回收或者分配 内存 。Java 堆的容量可以是固定的,也可以动态扩展。 Java 堆所使用的内存在物理上不需要连续,逻辑上连续即可。 Java 虚拟机规范中定义了 一种异常情况:如果在堆中没有足够的内存来完成实例分配,并且堆也无怯进行扩展时,贝 lj 会抛出 OutOfMemoryError 异常。
5.方法区
方住区( Method Area )是被所有线程共享 的运行时内存区域,用来存储已经被 Java虚拟机加载的类的结构信息,包括运行时常量地、字段和方告信息、静态变量等数据。方法区是 Java 堆的逻辑组成部分,它一样在物理上不需要连续,并且可以选择在方法区中不实现垃圾收集 。方法区并不等 同于永久代,只是因为 HotSpot VM 使用永久代来实现方法区,对于其他的 Java 虚拟机,比如 19 和 JRockit 等,并不存在永久代概念。在 Java 虚拟机规范中定义了 一种异常情况:如果方法区的内存空间不满足内存分配需求时, Java 虚拟机会抛出 OutOfMemoryError 异常。
6.运行时常量池
运行时常量地( Runtime Constant Pool )并不是运行时数据区域的其中 一份子,而是方法区的一部分。 C lass 文件不仅包含类 的版本、接口、宇段和方住等信息 ,还包含常量池,它用来存放编译时期生成的 字面量和符号引用,这些内容会在类加载后存放在方法区的运行时常量池中 。运行时常量地可以理解为是类或接口的常量池的运行时表现形式。在 Java 虚拟机规范中定义了一种异常情况 : 当 创建类或接 口时,如果构造运行时常量地所需的内存超过了方怯区所能提供的最大值, Java 虚拟机会抛出 OutOfMemoryError 异常。