JVM 详解(JVM组成部分、双亲委派机制、垃圾回收机制)未完待续~~

2023-12-13 12:32:02

JVM

1、概念:

什么是 JVM ?

JVM 就是 Java 虚拟机,是用来解析和运行Java程序的。

JVM 的性能调优是指通过优化程序的代码和环境,来提高程序运行效率、速度和稳定性的过程。

JVM 的作用?

为什么Java项目打包成一个Jar,就可以在windows、linux、MacOS 系统上运行,这就要归功于 JVM 。

Java代码在编译之后,并不是直接编译成我们操作系统可以识别的机器码,而是编译成只有 JVM 能够识别的字节码。
所以无论 Java 程序在哪个环境里面去运行,只要这个操作系统能装 JVM ,那么这个 Java 程序就能够运行。
JVM 相当于在做翻译工作,动态的把我们的Java代码翻译成操作系统能够识别的机器码。这样我们的Java代码就能实现一次编译,处处运行。
(通过JVM 使 Java 语言在不同平台上运行时不需要重新编译)

比如 JDK 和 JRE 就包含了 JVM.
在这里插入图片描述

2、JVM 的主要组成部分?

类加载器、运行时内存区(数据区)、执行引擎 三部分组成。

运行时内存区:堆、栈、方法区 等。

类加载器(Class Loader):

负责加载 Java 类文件到 JVM 中,并将其转换为在 JVM 中使用的可执行代码。

运行时内存区(Runtime Data Area):

是 JVM 的内存空间,用于存储程序运行时所需的数据。
JVM在运行时划分了不同的内存区域来存储数据:
堆、栈、方法区

执行引擎:

是 JVM 的核心组件,负责解释和执行 Java 字节码指令

在这里插入图片描述

内存区的堆、栈、方法区:

栈 :

存放方法。每个线程在运行时都会创建一个栈,用于存储方法调用时的局部变量、方法参数、方法返回值等。栈是线程私有的内存区域

堆:

用于存放对象实例和数组

方法区 :

存放类的结构信息(类的字段、方法、构造函数等),
运行时常量池(各种常量:字符串常量、数值常量、类和接口的符号引用等),
方法区是各个线程共享的内存区域。

这个图就是运行时内存区的大致:
在这里插入图片描述
详细:
在这里插入图片描述

运行时内存区,也可以叫运行时数据区
在《深入理解Java虚拟机》第三版第43页的图也是如此划分,直接引用其他大佬画好的图片:
在这里插入图片描述

3、JVM 类加载器的作用?

简单来说:就是读取字节码,转换成.class这种类,然后再创建类的实例就可以了。

在这里插入图片描述

详细分析:

这张图可以看出类的一个生命周期:
从类加载到虚拟机内存开始,到卸载为止,生命周期一共分为以下七步,如图:
其中固定的顺序:加载–验证–准备–初始化–卸载
【解析】也可以在【初始化】之后再进行解析,这是为了支持运行时的一个动态绑定的特性。
在这里插入图片描述

各阶段分析:

1、加载阶段:

简单来说:就是把字节码从不同的数据源,比如说是class类文件、jar包、网络,最终所得到的是一个二进制的一个字节流,然后把它加载到内存里面,然后就可以生成一个class对象。

在这里插入图片描述
全限定类名(Fully Qualified Class Name)指的是一个类的完整名称,包括包名、类名和其内部类的名称。全限定类名可以用于唯一地标识一个类,包括标准类、抽象类、接口和枚举类型。

2、验证阶段

简单来说:验证阶段就是对我们传入的一个二进制字节流去进行校验,只有符合 JVM 字节码规范的,才能被 JVM 正确执行。这个阶段就是为了保证 JVM 安全的一个重要凭证。

在这里插入图片描述

在计算机领域中,“魔数”(Magic Number)是指一种特定的固定字节序列,用于识别文件格式或数据类型。它通常位于文件的开头,作为文件的标识符。

对于字节码文件(.class 文件)来说,魔数是指文件开头的四个字节,以十六进制表示为 “0xCAFEBABE”。这个特定的字节序列被设计为Java虚拟机用来识别和验证字节码文件的标识

3、准备阶段:

简单来说:JVM在这个准备阶段,对类的变量去进行一个分配内存和初始化。
注意:准备阶段的这个初始化是初始化默认值,比如我代码中定义一个 int a = 10 ,此时这个 a 是等于默认值 0 。因为 int 类型的默认值是0 。

真正把 10 赋值给 a 变量的,是在【初始化】阶段。

4、解析阶段:

简单来说:就是将常量池中的符号引用转换为直接引用。

符号引用:以一组符号,比如任何形式的字面量(常量),来描述引用的目标,这些字面量就叫做符号引用。

直接引用:通过对这个符号引用去进行一个解析,然后找到引用的一个实际内存地址在哪里并做一个关联,这个就叫做直接引用。

5、初始化阶段

简单来说:就是一个正常赋值的阶段,也可以理解是去执行类构造器方法的一个过程。

区别理解:
准备阶段:给变量赋予默认值
初始化阶段:给变量赋予真正的值。

6、使用阶段:

简单来说:当整个界面完成初始化阶段后,JVM 就开始从入口方法执行代码,比如从main方法开始执行我们程序的代码。

7、卸载阶段:

简单来说:当代码执行完成之后, JVM 就要开始销毁前面所创建的对象。

JVM的卸载阶段是一个高度优化的过程,并不是每个类或对象都会被立即卸载。JVM会根据一系列的条件和策略进行判定和调度,以提高执行效率和资源利用率。

需要明确的是,JVM的卸载阶段主要针对普通的应用类。而核心的系统类(如Java标准库中的类)通常会被JVM认为是永远可达的,不会被卸载。

卸载阶段主要是为了释放应用程序中自定义类所占用的内存和资源。

4、JVM 的类加载器有哪些?

从Java程序的角度看,有这三种类加载器:
启动类加载器、扩展类加载器、应用程序类加载器。

而从虚拟机的角度看,只有两种类加载器:
C++ 语言写的 启动类加载器,属于虚拟机自身的一部分。
Java 语言写的 扩展类加载器 和 应用程序类加载器,是独立于 JVM 的外部的。

在这里插入图片描述

启动类加载器:
这个类加载器是不能被Java程序直接引用的,就算是通过代码去获取到,也是一个null值。

扩展类加载器:
负责加载 lib包、ext扩展包的文件,可以被开发者直接使用的。

应用程序类加载器:
也被称为系统类加载器。
负责加载的是程序的一些类路径,比如第三方类库、ClassPath。
如果应用程序没有自定义自己的类加载器,那么就默认使用这个应用程序类加载器。

这三个类加载器是相互配合、互相工作的。如上图,各个类加载器是分层的,这些分层也被称为类加载器的双亲委派机制。

什么双亲委派机制

简单来说:双亲委派机制是 Java 虚拟机的一种类加载机制,它是实现 Java 安全沙箱和避免类重复加载的重要手段。

1、要求最顶层是启动类加载器,其余的类加载器都应该有自己的父类加载器。
比如扩展类加载器的父类就是启动类加载器。
应用程序类加载器的父类就是扩展类加载器

2、子类加载器和父类加载器并不是一个继承的关系,而是通过组合的关系,来复用父类加载器里面的代码。

安全沙箱(Security Sandbox)是一种安全机制,用于限制程序在执行过程中的操作权限,防止恶意代码对计算机系统造成破坏或滥用系统资源

双亲委派机制的工作过程

这个图也是双亲委派机制的流程图:
在这里插入图片描述

比如应用程序类加载器,收到了一个类加载的请求,它首先并不会去尝试自己加载这个类,而是把这个请求委派给父类加载器去完成,每一层都是如此。

如上图:
1、比如应用程序类加载器接收到一个类加载的请求,它不会马上去进行类加载操作,而是委派给自己的父类加载器->扩展类加载器。

2、扩展类加载器就会检查自己是否已经加载过这个类,如果已经加载,则直接返回对应的 Class 对象。如果自身没有加载过该类,则将加载请求再继续向上委派给父类加载器->启动类加载器。
(注意点:如果扩展类加载器没有加载过这个类,但是它自身具备加载这个类的能力,根据双亲委派模型的原则,扩展类加载器会继续委派加载请求给父类加载器。)

3、如此一层一层的检查和向上委派,直到委派到最顶层的启动类加载器后,
如果顶层的启动类加载器能够来加载这个类,那么就由启动类加载器来加载这个类。
如果这个启动类加载器不能加载这个类,那么它再把这个类加载的请求,往下去分派给它的子类加载器(扩展类加载器),如果扩展类加载器也加载不了,那么再往下分派给 应用程序类加载器。

只有当所有的父类加载器都无法加载时,才会由当前类加载器自行尝试加载。
如果加载失败,那就抛异常。

一般不会加载失败。
类加载失败的情况一般是:
在类路径中找不到指定的类文件、类文件格式错误、该类依赖的类不存在等情况


双亲委派机制的好处:

通过这种模型来组织类加载器之间的关系,
好处是:
比如有一个Object类,无论是哪个类加载器去加载这个类,最终都是由启动类加载器来加载的,因此这个 Object 类在我们程序里面, 不论什么样的环境,都是使用同一个 Object 类。

如果我们不使用这个双亲委派机制:
比如我想尝试自己定义一个Object类,然后存放在Classpath 里面, 那么系统就会出现多个 Object 类,程序就会混乱。

如果我们使用这个双亲委派机制:
如果我们定义了一个 rt.jar 里面已经有了的同名类,我们会发现,JVM可以进行正常编译,但是这个类永远无法被加载运行。
比如我们自己定义一个Object类,虽然这个类能被JVM编译,但是不会被加载和运行。


rt.jar 是 JDK(Java Development Kit) 中以前的一个核心库文件,它包含了大量 JDK 中的基础类库和 API,如 Java 标准库、Java 集合框架、Java 网络编程相关库等。rt 代表 “run-time”,也就是运行时库

在 JDK 9 及以后的版本中,rt.jar 被拆分成了多个模块(Module)和 JAR 包,并不再作为独立的库文件存在。这是为了更好地支持模块化开发,提高代码的可维护性和扩展性

总结:

这是jdk1.8的理解:

双亲委派机制就是 当类加载器接收到一个类加载的请求,这个类加载器不会马上就去进行加载操作,而是向上委托给自己的父类(也是类加载器),询问父类加载器是否已经加载这个类,如果加载过,则返回该类的Class对象,如果没加载过,但是本身具备加载该类的条件,该类加载器还是会继续向自己的父类去委托,直到委托给最顶层的启动类加载器。

最顶层的启动类加载器已经没有父类了,就只能自己来加载这个类,如果启动类加载器发现自己加载不了,它就会向下分派给自己的子类(也是类加载器)去加载。

用这个机制的好处就是如果出现恶意的和基础类库同名的类,这个类虽然会被JVM编译,但是不会被加载运行。

jdk1.9 以后:
双亲委派机制发生了变化,简单说:
就是没有了扩展类加载器,变成了平台类加载器,比如应用程序类加载器把类加载的请求委托给 平台类加载器,如果这个平台类加载器能找到该类,就直接进行加载的操作,不会跟jdk1.8一样委派给启动类加载器。

在这里插入图片描述

问题:
jdk1.9之后,如果应用程序类把一个类加载的请求发送给平台类加载器,平台类加载器没有加载过该类,但是自身具备加载该类的条件,那么它会自己加载还是继续委托给启动类加载器?

解释:

根据 JDK 9 之后的模块化系统,如果应用程序类把一个类加载的请求发送给平台类加载器,并且平台类加载器具备加载该类的条件,它会根据类所属模块的定义来决定加载行为。对于平台模块的类,平台类加载器会自己加载;对于应用程序模块的类,平台类加载器会继续委托给启动类加载器。

JDK 9 之后的类加载机制更加灵活和复杂,涉及到了模块化系统的概念。因此,具体的加载行为还取决于模块定义、模块路径和类路径的配置等因素



5、JVM 的内存结构(运行时内存区)

官方叫运行时数据区
在这里插入图片描述

线程共享数据:

堆、方法区、运行时常量池、直接内存

Java堆:

简单来说:堆就是从 JVM 划分出来的一块区域,是虚拟机所管理的内存中最大的一块,在虚拟机启动时创建,此内存区域的唯一目的就是用来用来存放实例对象,几乎所有的实例都会在这个 Java堆 进行内存分配。
还有一些小的对象,会在栈上分配。

堆是比较重要的部分,要了解好堆的区域划分情况:
堆的划分:
在这里插入图片描述

详细解释:

1、Java堆根据对象的存活时间的不同,还会再进行划分—> 年轻代 和 老年代 。

2、年轻代 划分了 Eden区 和 幸存者区,当有对象要进行内存分配的时候,是会优先分配在 Eden区 的,等到 Eden区 内存不够的时候,虚拟机就会去启动 GC(垃圾回收机制) ,此时 Eden区 没有被引用的对象,那这些对象的内存就会被回收掉。而一些存活时间比较长的对象,就会进入到老年代里面。

3、因为虚拟机里面的对象有的存活时间短,有的存活时间长,存活时间短的对象比较多。如果不进行分区,那么会导致很频繁的进行GC,
而GC 操作会对所有的内存进行扫描,就是每次GC都会把存活时间长的对象也进行扫描,这就会浪费时间。所以对堆空间进行区域划分,能提高GC的效率。

5、在 Java 虚拟机里面,默认年轻代的配置是 8 : 1 : 1 的空间分配,Eden区的比例是 8 ,幸存者区的比例是 1 。
根据统计, 80% 的对象的存活时间比较短,所以把Eden区设置为年轻代的 80% ,这样能减少内存空间的浪费,提高内存空间的使用率。

方法区:

一个java文件通过类加载器加载到内存里面,然后这个类的结构信息就会存在这个方法区里面。

是存放的是 Java类字节码数据的一块区域,存每个类的结构信息,包括字段、方法数据、构造方法等等。
方法区在 JDK1.7 称为 永久代,在 JDK1.8 称为 元空间。

线程私有数据:

程序计数器、虚拟机栈、本地方法栈、线程分配缓冲区

程序计数器:

简单来说:JVM的程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

这个字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
(个人理解就是,因为 Java虚拟机 的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的, 当这条线程执行后要切换下一条线程时,具体切换哪条线程,要从哪个位置开始执行,就得通过程序计数器来实现)

每条线程都有一个独立的程序计数器,作用是保证线程在切换后能恢复到正确的执行位置。

各线程之间的程序计数器互不影响、独立存储。

虚拟机栈

虚拟机栈 和 线程 是同一时间创建出来的,生命周期 和 线程 相同。

虚拟机栈描述的是 Java方法在执行时的线程内存模型:
每个方法被执行的时候,Java虚拟机 就会同步创建一个栈帧,这个栈帧用来存储 Java方法 执行时的局部变量、操作数栈、动态连接、方法出口等信息

每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

本地方法栈

本地方法栈和虚拟机栈发挥的作用是非常相似的,都是对方法执行期间的相关数据的存储。

区别:
虚拟机栈 为虚拟机执行Java方法(也就是字节码)进行服务;
本地方法栈 是为虚拟机使用到本地方法(Native)进行服务。

在 Java 中,本地方法声明通常使用 “native” 关键字:
public native void nativeMethod();

总结:

一个 Java文件,在 Java虚拟机 里面,通过 类加载器 加载到内存里面时,这个类的结构信息就会存在这个 方法区 里面,如果创建对象,那么这个对象的数据就放在 Java堆 里面,如果调用方法,那么就会用到 程序计数器、虚拟机栈或者本地方法栈

6、JVM 的垃圾回收算法









什么是 OOM ?

OOM(Out of Memory)是指程序在运行过程中无法分配到足够的内存空间而导致的错误。当应用程序需要更多的内存来创建对象或执行操作时,JVM会尝试分配内存,但如果可用的内存已经耗尽,将会发生OOM错误

文章来源:https://blog.csdn.net/weixin_44411039/article/details/134908341
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。