当前位置: 首页 > news >正文

【JVM】从零开始深度解析JVM

在这里插入图片描述

本篇博客给大家带来的是JVM的知识点, 重点在类加载和垃圾回收机制上.
🐎文章专栏: JavaEE初阶
🚀若有问题 评论区见
欢迎大家点赞 评论 收藏 分享
如果你不知道分享给谁,那就分享给薯条.
你们的支持是我不断创作的动力 .

王子,公主请阅🚀

  • 要开心
    • 要快乐
      • 顺便进步
  • 1. JVM简介
    • 1.1 HotSpot VM
    • 1.2 Taobao JVM(国产研发)
    • 1.3 JDK JRE JVM三者关系(经典面试题)
  • 2. JVM 运行流程
  • ★3. JVM运行时数据区(内存区域)
    • 3.1 堆(线程共享)
    • 3.2 Java虚拟机栈(线程私有)/本地方法栈
    • 3.3 程序计数器(线程私有)
    • 3.4 元数据区(方法区)
  • ★4. JVM类加载
    • 4.1 类加载过程
      • 4.1.1 加载
      • 4.1.2 验证
      • 4.1.3 准备
      • 4.1.4 解析
      • 4.1.5 初始化
    • 4.2 双亲委派模型
      • 4.2.1 什么是双亲委派模型?
      • 4.2.2 双亲委派模型的工作过程.
      • 4.3 双亲委派模型优点
      • 4.4 破坏双亲委派模型
  • 5. 垃圾回收机制
    • 5.1 死亡对象判断算法.
      • 5.1.1 引用计数算法.
      • 5.1.2 可达性分析算法
    • 5.2 垃圾回收算法
      • 5.2.1 标记-清除算法
      • 5.2.2 复制算法
      • 5.2.3 标记-整理算法
      • 5.2.4 分代算法

要开心

要快乐

顺便进步

1. JVM简介

JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。
虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。
常见的虚拟机:JVM、VMwave、Virtual Box.

JVM 和其他两个虚拟机的区别:
① VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
② JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪.

JVM 是一台被定制过的现实当中不存在的计算机.

1.1 HotSpot VM


HotSpot最初由一家名为“Longview Technologies”的小公司设计;

1997年,此公司被Sun收购;2009年,Sun公司被甲骨文收购。

JDK1.3时,HotSpot VM成为默认虚拟机

目前 HotSpot 占用绝对的市场地位,称霸武林。

不管是现在仍在广泛使用JDK6,还是使用比较多的JDK8中,默认的虚拟机都是HotSpot;

Sun/Oracle JDK和OpenJDK的默认虚拟机。从服务器、桌面到移动端、嵌入式都有应用。

名称中的HotSpot指的就是它的热点代码探测技术。它能通过计数器找到最具编译价值的代码,触发即时编译(JIT)或栈上替换;通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡。

1.2 Taobao JVM(国产研发)


阿里是国内使用Java最强大的公司,覆盖云计算、金融、物流、电商等众多领域,需要解决高并发、高可用、分布式的复合问题。有大量的开源产品。
基于OpenJDK 开发了自己的定制版本AlibabaJDK,简称AJDK。是整个阿里JAVA体系的基石;
基于OpenJDK HotSpot JVM发布的国内第一个优化、深度定制且开源的高性能服务器版Java虚拟机,
它具有以下特点(了解):


1. 创新的GCIH(GC invisible heap)技术实现了off-heap,即将生命周期较长的Java对象从heap中移到heap之外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收评率和提升GC的回收效率的目的;

2. GCIH中的对象还能够在多个Java虚拟机进程中实现共享;

3. 使用crc32指令实现JVM intrinsic降低JNI的调用开销;

4. PMU hardware的Java profiling tool和诊断协助功能;

5. 针对大数据场景的ZenGC。

taobaoJVM应用在阿里产品上性能高,硬件严重依赖intel的cpu,损失了兼容性,但提高了性能,目前已经在淘宝、天猫上线,把Oracle官方JVM版本全部替换了.

1.3 JDK JRE JVM三者关系(经典面试题)

JDK(Java Development Kit):Java开发工具包,提供给Java程序员使用,包含了JRE,同时还包含了编译器 javac 与自带的调试工具Jconsole、jstack等.

JRE(Java Runtime Environment):Java运行时环境,包含了JVM,Java基础类库。是使用Java语言编写程序运行的所需环境.

JVM:Java虚拟机,运行Java代码.

在这里插入图片描述


2. JVM 运行流程


JVM 是 Java 运行的基础,也是实现一次编译到处执行的关键,那么 JVM 是如何执行的呢?

Java程序在执行之前先要把 java 代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。

在这里插入图片描述

JVM 主要通过分为以下 4 个部分,来执行 Java 程序的,它们分别是:

1. 类加载器(ClassLoader)
2. 运行时数据区(Runtime Data Area)
3. 执行引擎(Execution Engine)
4. 本地库接口(Native Interface)

★3. JVM运行时数据区(内存区域)

JVM 运行时数据区域也叫内存布局,但需要注意的是它和 Java 内存模型((Java Memory Model,简
称 JMM)完全不同,属于完全不同的两个概念,它由以下 五 大部分组成:

在这里插入图片描述

3.1 堆(线程共享)

代码中 new 出来的对象,都是在堆里, 对象中持有的非静态成员变量,也在堆里.

堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象会放入老生代。新生代还有 3 个区域:一个 Endn + 两个 Survivor(S0/S1).

垃圾回收的时候会将 Endn 中存活的对象放到一个未使用的 Survivor 中,并把当前的 Endn 和正在使用的 Survivor 清除掉.

3.2 Java虚拟机栈(线程私有)/本地方法栈


Java 虚拟机栈的作用:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的内存模型: 每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表操作数栈动态链接方法出口等信息。咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈.

1. 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的空间是完全确定的,在执行期间不会改变局部变量表大小. 简单来说就是存放方法参数和局部变量.

2. 操作栈:每个方法会生成一个先进后出的操作栈.

3. 动态链接:指向运行时常量池的方法引用.

4. 方法返回地址:PC 寄存器的地址.

什么是线程私有?

由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们就把类似这类区域称之为"线程私有"的内存。

本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用的。

此处谈到的“堆” "栈"和数据结构中的"堆” "栈"是不同的! 面试中如果被问到堆和栈, 一定要反问面试官,搞清楚问的是哪个堆,哪个栈?

3.3 程序计数器(线程私有)


程序计数器的作用:用来记录当前线程执行的行号的. 也是用来存储下一条要执行的 java 指令的地址.


如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值为空.



3.4 元数据区(方法区)


方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的.

运行时常量池是方法区的一部分,存放字面量与符号引用.

字面量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值.

符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符.

★4. JVM类加载


类加载指的是, java 进程运行的时候,需要把 .class 文件从硬盘读取到内存,并进行一系列的校验解析的过程.

4.1 类加载过程


整个 JVM 执行的流程中,和程序员关系最密切的就是类加载的过程了,所以接下来我们来看下类加载的执行流程。

对于一个类来说,它的生命周期是这样的:

前 5 步是固定的顺序并且也是类加载的过程,其中中间的 3 步我们都属于连接,所以对于类加载来说总共分为以下几个步骤:
1. 加载
2. 连接
a. 验证
b. 准备
c. 解析
3. 初始化

4.1.1 加载


“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,它和类加载Class Loading 是不同的,一个是加载 Loading 另一个是类加载 Class Loading,不要把二者搞混了。


在加载 Loading 阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流.
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构.
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口.

4.1.2 验证


验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全.
简而言之,就是确保当前读到的文件内容是合法的.

文件格式验证
字节码验证
符号引用验证…

4.1.3 准备


准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。

比如此时有这样一行代码:

public static int value = 888;

它是初始化 value 的 int 值为 0,而非 888.

4.1.4 解析


解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。

在硬盘或者文件中不存在地址这样的概念, 虽然没有地址但是可以引入一个偏移量这样的概念来记录字符串常量的位置. 此时的偏移量就可以认为是符号引用.

4.1.5 初始化


初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。初始化阶段就是执行类构造器方法的过程.

简单总结:
加载: 找到 .class 文件,并且读文件内容.

验证: 校验 .class 文件的格式是否符合 JVM 规范要求.

准备: 给类对象分配内存(此时内存空间是全0的 =>类的静态成员也就是全 0的值,即 int 默认值为0).

解析: 针对类中的字符串常量进行处理.

初始化: 把类对象的各个部分的属性进行赋值填充 =>触发对父类的加载,初始化静态成员,执行静态代码块.

4.2 双亲委派模型


提到类加载机制,不得不提的一个概念就是“双亲委派模型”。

站在 Java 虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap
ClassLoader),这个类加载器使用 C++ 语言实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader.

站在 Java 开发人员的角度来看,类加载器就应当划分得更细致一 些。自 JDK 1.2 以来,Java 一直保持着三层类加载器、双亲委派的类加载架构器。

4.2.1 什么是双亲委派模型?


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

在这里插入图片描述

BootstrapClassLoader
负责查找标准库的目录.

ExtensionClassLoader
负责查找扩展库的目录.

ApplicationClassLoader
负责查找当前项目的代码目录
以及第三方库的目录.

4.2.2 双亲委派模型的工作过程.


1.从 ApplicationClassLoader 作为入口,先开始工作.

2. ApplicationClassLoader 不会立即搜索自己负责的目录,而会把搜索的任务交给自己的父亲:

3.代码就进入到 ExtensionClassLoader 范畴了ExtensionClassLoader 也不会立即搜索自己负责的目录也要把搜索的任务交给自己的父亲.

4.代码就进入到 BootstrapClassLoader 范畴了BootstrapClassLoader 也不想立即搜索自己负责的目录也要把搜索的任务交给自己的父亲.

5.BootstrapClassLoader 发现自己没有父亲才会真正搜索负责的目录 (标准库目录)通过全限定类名,尝试在标准库目录中找到符合要求的 .class 文件. 如果找到了,接下来就直接进入到打开文件/读文件等流程中如果没找到,回到孩子这一辈的类加载器中,继续尝试加载.

6. ExtensionClassLoader 收到父亲交回给他的任务之后,自己进行搜索负责目录(扩展库的目录)…

7. ApplicationClassLoader 收到父亲交回给他的任务之后自己进行搜索负责的目录(当前项目目录/第三方库目录)…

4.3 双亲委派模型优点


1. 避免重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了.

2. 安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户自己提供的因此安全性就不能得到保证了.

4.4 破坏双亲委派模型


上述这一系列规则,只是 JM 自带的类加载器遵守的默认规则. 如果咱们自己写类加载器,也可以打破上述规则比如自己写类加载器, 指定这个加载器就在某个目录中尝试加载. 此时如果类加载器的 parent 不去和已有的这些类加载器连到一起, 此时就是独立的,不涉及到双亲委派了.


5. 垃圾回收机制

堆是垃圾回收的主战场.

Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前, 首先要判断这些对象哪些还存活,哪些已经"死去". 判断对象是否已"死"有如下几种算法.

在 Java 中,所有的对象都是要存在内存中的(也可以说内存中存储的是一个个对象),因此我们将内存回收,也可以叫做死亡对象的回收。


5.1 死亡对象判断算法.

在 Java 中,使用对象一定需要通过引用的方式来使用. (有个例外,匿名对象,但是它执行完之后就被回收了). 如果一个对象没有任何引用指向它, 就视为是无法在代码中使用, 就可以回收掉.

5.1.1 引用计数算法.

给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;
任何时刻计数器为0的对象就是不能再被使用的,即对象已"死".

引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如Python语言就采用引用计数法进行内存管理.


在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题.

为了解释循环引用问题, 写出下列伪代码:

class Test {Test t;
}
main() {Test a = new Test();Test b = new Test();a.t = b;b.t = a;
}

在这里插入图片描述

令 a = null, b = null;

在这里插入图片描述

此时虽然Test对象的引用计数为 1, 但确实无法使用, 也无法被回收.

5.1.2 可达性分析算法


在上面讲了,Java并不采用引用计数法来判断对象是否已"死",而采用可达性分析来判断对象是否存活(同样采用此法的还有C#、Lisp-最早的一门采用动态内存分配的语言).


此算法的核心思想为 : 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。以下图为例:

在这里插入图片描述

对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象。


JVM 自身知道一共有哪些对象, 通过可达性分析的遍历, 把可达的对象都标记出来, 剩下的自然就是不可达的.

5.2 垃圾回收算法


将死亡对象标记出来了,标记出来之后我们就可以进行垃圾回收操作了.

5.2.1 标记-清除算法


标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象. 后续的收集算法都是基于这种思路并对其不足加以改进而已.

"标记-清除"算法的不足主要有两个 :

1. 效率问题 : 标记和清除这两个过程的效率都不高.

2. 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

在这里插入图片描述

5.2.2 复制算法


"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉. 这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行 , 高效。


在这里插入图片描述

5.2.3 标记-整理算法

引入概念: 对象的年龄.
JVM 中有专门的线程负责周期性扫描/释放一个对象(初始年龄相当于是 0), 如果被线程扫描了一次,不是垃圾,年龄就+1.
JVM 中就会根据对象年龄的差异,把整个堆内存分成两个大的新生代(年龄小的对象)和老年代(年龄大的对象).


复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法.

针对老年代的特点,提出了⼀种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存.

在这里插入图片描述

5.2.4 分代算法


分代算法和上面讲的 3 种算法不同,分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收.

这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"算法。

老年代中有大量对象存活,是因为如果要死亡, 在新生代中早就死了, 能活到老年代的对象,说明其生命周期长.

总结来看:

新生代中, 只有少量存活,因此我们采用复制算法; 老年代中有大量存活, 采用"标记-清理"算法.







本篇博客到这里就结束啦, 感谢观看 ❤❤❤

🐎期待与你的下一次相遇😊😊😊

相关文章:

  • 论微服务架构设计及应用
  • 【AI News | 20250506】每日AI进展
  • 【Python系列】Python 中的 HTTP 请求处理
  • LLM损失函数面试会问到的
  • 【Azure Redis 缓存】关于Azure Cache for Redis 服务在传输和存储键值对(Key/Value)的加密问题
  • 在Spring Boot 中如何配置MongoDB的副本集 (Replica Set) 或分片集群 (Sharded Cluster)?
  • HTTP协议网络读卡器通讯报文
  • WebSoket的简单使用
  • Rice Science∣武汉大学水稻研究团队发现水稻壁相关激酶OsWAKg16和OsWAKg52同时调控水稻抗病性和产量
  • 关系型数据库与非关系型数据库深度对比:从设计哲学到应用场景的全解析
  • 在Star-CCM+中实现UDF并引用场数据和网格数据
  • 【C语言】初阶数据结构相关习题(一)
  • Leetcode 刷题记录 07 —— 链表
  • 小土堆pytorch--transform
  • 关于loadstartcode使用
  • 使用 Poco C++ 库构建轻量级 HTTP 服务器
  • 小微企业SaaS ERP管理系统,SpringBoot+Vue+ElementUI+UniAPP
  • Oracle中游标和集合的定义查询及取值
  • Flutter开发HarmonyOS实战-鸿蒙App商业项目
  • 极速轻量,Rust 网络开发新选择:Hyperlane 框架深度解析
  • 降准又降息!央行发布3类10项措施
  • 缅甸国防军继续延长临时停火期限至5月31日
  • 广东省联社:积极推动改制组建农商联合银行工作
  • 特朗普宣布对进口电影征收100%关税
  • 牧草之王苜蓿的江南驯化史
  • 五四青年节|青春韵脚