JVM
是
Java
运行的基础,也是实现一次编译到处执行的关键,那么
JVM
是如何执行的呢?
JVM 执行流程
程序在执行之前先要把java代码转换成字节码(class
文件),
JVM
首先需要把字节码通过一定的
方式
类加载器(ClassLoader)
把文件加载到内存中
运行时数据区(Runtime Data Area)
,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器
执行引擎(Execution Engine)
将
字节码翻译
成底层系统指令再交由
CPU
去执行,而这个过程中需要调用其他语言的接口
本地库接口(Native Interface)
来实现整个程序的功能,这就是这
4
个主要组成部分的职责与功能。
![](https://img-blog.csdnimg.cn/direct/3a596b745302426a88d4b7b26126e7a1.png)
总结来看, JVM 主要通过分为以下 4 个部分,来执行 Java 程序的,它们分别是:1. 类加载器(ClassLoader)2. 运行时数据区(Runtime Data Area)3. 执行引擎(Execution Engine)4. 本地库接口(Native Interface)
JVM 运行时数据区
JVM
运行时数据区域也叫内存布局,但需要注意的是它和
Java
内存模型(
(Java Memory Model
,简称JMM)完全不同,属于完全不同的两个概念,它由以下
5
大部分组成:
![](https://img-blog.csdnimg.cn/direct/1d5ab703164f4f598e0bd76d041168e8.png)
2.1 堆(线程共享)
堆的作用:程序中创建的所有对象都在保存在堆中。
![](https://img-blog.csdnimg.cn/direct/61f13c271b7744d096ee72421d19c587.png)
堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定
GC
次数之后还存活的对象会放入老生代。新生代还有 3
个区域:一个
Endn +
两个
Survivor
(
S0/S1
)。
垃圾回收的时候会将
Endn
中存活的对象放到一个未使用的
Survivor
中,并把当前的
Endn
和正在使用的 Survivor
清楚掉。
2.2 Java虚拟机栈(线程私有)
Java
虚拟机栈的作用:
Java
虚拟机栈的生命周期和线程相同,
Java
虚拟机栈描述的是
Java
方法执行的 内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame
)用于存储局部变量表、操作数 栈、动态链接、方法出口等信息。咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈。
Java
虚拟机栈中包含了以下
4
部分:
![]()
- 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量。
- 操作栈:某些字节码指令把值压入操作数栈,其余指令将操作数取出栈。使用他们后再把结果压入栈。比如:执行复制、交换、求和等操作。
- 动态链接:指向运行时常量池的方法引用。
- 方法返回地址:PC 寄存器的地址。
什么是线程私有 ?由于 JVM 的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器 ( 多核处理器则指的是一个内核 ) 都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们就把类似这类区域称之为 " 线程私有 " 的内存
2.3 本地方法栈(线程私有)
本地方法栈和虚拟机栈类似,只不过
Java
虚拟机栈是给
JVM 使用的,而本地方法栈是给本地方法使用的.
即非Java编写的,如C或C++编写的)服务的。
2.4 程序计数器(线程私有)
程序计数器的作用:用来记录当前线程执行的行号的。
程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
如果当前线程正在执行的是一个
Java
方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是一个
Native
方法,这个计数器值为空。
程序计数器内存区域是唯一一个在
JVM
规范中没有规定任何
OOM
情况的区域!
2.5 方法区(线程共享)
方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
的。
在《
Java
虚拟机规范中》把此区域称之为
“
方法区
”
,而在
HotSpot
虚拟机的实现中,在
JDK 7
时此区域叫做永久代(PermGen
),
JDK 8
中叫做元空间(
Metaspace
)。
PS
:永久代(
PermGen
)和元空间(
Metaspace
)是
HotSpot
中对《
Java
虚拟机规范》中方法
区的实现,它们三者之间的关系就好比,对于一辆汽车来说它定义了一个部分叫做
“
动能提供装
置
”
,但对于不同的汽车有不同的实现技术,比如对于燃油车来说,它的
“
动能提供装置
”
的实现技
术就是汽油发动机(简称发动机),而对于电动汽车来说,它的
“
动能提供装置
”
的实现就是电动
发动机(简称电机),发动机和电机就相当于永久代和元空间一样,它是对于
“
制动器
”
也就是方
法区定义的实现。
1.
对于
HotSpot
来说,
JDK 8
元空间的内存属于本地内存,这样元空间的大小就不在受
JVM
最大内
存的参数影响了,而是与本地内存的大小有关。
2. JDK 8
中将字符串常量池移动到了堆中。
运行时常量池
运行时常量池是方法区的一部分,存放字面量与符号引用。
字面量
:
字符串
(JDK 8
移动到堆中
)
、
final常量、
基本类型的包装类。
java中基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean。这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。
两种浮点数类型的包装类Float,Double并没有实现常量池技术。
符号引用
:
类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
- 类和结构的完全限定名 :包括包名和类/接口名。
- 字段的名称和描述符:是一个简短的字符串,描述了字段的类型。
- 方法的名称和描述符: 是一个简短的字符串,描述了方法的返回类型和参数类型。
JVM 类加载
类加载过程
从上面的图片我们可以看出整个
JVM
执行的流程中,和程序员关系最密切的就是类加载的过程了,所以
接下来我们来看下类加载的执行流程。
对于一个类来说,它的生命周期是这样的
![](https://img-blog.csdnimg.cn/direct/1755bba4392344ca9d7578427ed95aa9.png)
其中前
5
步是固定的顺序并且也是类加载的过程,其中中间的
3
步我们都属于连接,所以对于类加载来
说总共分为以下几个步骤:
1.
加载
2.
连接
1. 验证
2. 准备
3. 解析
3.
初始化
下面我们分别来看每个步骤的具体执行内容。
1) 加载
加载即Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型——类模板对象。所谓类模板对象,其实就是 Java 类在 JVM 内存中的一个快照,JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到模板中,这样 JVM 在运行期便能通过类模板而获取 Java 类中的任意信息,能够对 Java 类的成员变量进行遍历,也能进行 Java 方法的调用。
在加载阶段,虚拟机需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2) 验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节 流中包含的信息符合《Java虚拟机
规范》的全部约束要求,保证这些信 息被当作代码运行后不会危害虚拟机自身的安全。
验证选项:
- 文件格式验证
- 字节码验证
- 符号引用验证
3) 准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值 的阶段。
比如此时有这样一行代码:
public static int value = 123;
它是初始化
value
的
int
值为
0
,而非
123
。
4) 解析
解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。
5) 初始化
初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。初始化阶段就是执行类构造器方法的过程。
双亲委派模型
什么是双亲委派模型?
如果一个类加载器收到了类加载的请求,(加载1)时)它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无 法完成这个加载请求(它的搜索范围中没有找到所需的类) 时,子加载器才会尝试自己去完成加载。
![](https://img-blog.csdnimg.cn/direct/50a4d34f4ed94d15ae6716e55ed2494e.png)
![](https://img-blog.csdnimg.cn/direct/ded408475a8147dcbdec3e91d63e92c8.png)
- 启动类加载器:加载 JDK 中 lib 目录中 Java 的核心类库,即$JAVA_HOME/lib目录。 扩展类加载器。加载 lib/ext 目录下的类。
- 应用程序类加载器:加载我们写的应用程序。
- 自定义类加载器:根据自己的需求定制类加载器。
双亲委派模型的优点
1.
避免重复加载类:比如
A
类和
B
类都有一个父类
C
类,那么当
A
启动时就会将
C
类加载起来,那么在 B
类进行加载时就不需要在重复加载
C
类了。
2.
安全性:使用双亲委派模型也可以保证了
Java
的核心
API
不被篡改,如果没有使用双亲委派模
型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为
java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的
Object
类,而有些
Object
类又是用户
自己提供的因此安全性就不能得到保证了。
![](https://img-blog.csdnimg.cn/direct/23cec3d00ff743e4ab40b91830879228.png)