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

初识JVM

这篇博客主要介绍JVM中的内存区域划分、双亲委派模型、垃圾回收机制。

一、JVM简介

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

  1. VMwave 与 VirtualBox 是通过软件模拟物理 CPU 的指令集,物理系统中会有很多的寄存器;
  2. JVM 则是通过软件模拟 Java 字节码的指令集,JVM 中只是主要保留了 PC 寄存器,其他的寄存器都进行了裁剪。

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

二、内存区域划分

JVM是虚拟的,仿造了操作系统在进程运行时的区域划分。

JVM内存区域划分,相当于就是JVM进程从操作系统申请到了一部分空间,然后将这个空间划为不同模块,执行不同功能。

就像房间的布局:

JVM划分出的四个核心区域:

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

它是一个很小的空间,用于记录cpu指令执行到哪一个地址了。

(二)方法区(线程共享)

方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。 在《Java虚拟机规范中》把此区域称之为“方法区”,而在 HotSpot 虚拟机的实现中,在 JDK 7 时此区域叫做永久代(PermGen),JDK 8 中叫做元空间(Metaspace)。

当我们写一段代码,代码执行的流程是:

.java->.class->加载到内存中,元数据区就是用来存放当前类被加载好的数据。

(三)java虚拟机栈(线程私有)

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

(四)堆(线程共享)

堆的作用:程序中创建的所有对象都在保存在堆中。

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

三、JVM类加载

(一)类加载的步骤

1.加载

根据类的全限定名(包名+类名,形如java.lang.String),找到并打开文件,读取文件内容到内存里。

2.验证

校验.class文件读到的内容是否是合法的,并且把这里的内容转化成结构化的数据。

3.准备

给类对象申请内存空间并设置类变量初始化。

public static int value = 123;

初始化value的值为0,并非123。

4.解析

从.class文件里解析出来的字符串常量,放到内存空间里,并进行初始化,也就是初始化常量的过程。

5.初始化

针对刚才谈到的类对象进行最终的初始化,对类对象的的各种属性进行填充(包括这个类中的静态成员,如果这个类还有父类,而且父类还没加载,此环节也会触发父类的类加载)。

(二)双亲委派模型

提到类加载机制,不得不提的一个概念就是“双亲委派模型”。 站在 Java 虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++ 语言实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。 站在 Java 开发人员的角度来看,类加载器就应当划分得更细致一些。自 JDK 1.2 以来,Java 一直保持着三层类加载器、双亲委派的类加载架构器。

⭐️双亲委派模型的过程

进行类加载,通过全限定类名找 .class 文件时,会从 ApplicationClassLoader 作为入口开始。


然后把加载类的任务,委托给 “父亲”ExtensionClassLoader 来进行。ExtensionClassLoader 也不会立即进行查找,而是同样委托给 “父亲”BootstrapClassLoader 来进行。


BootstrapClassLoader 也想委托给 “父亲”,但由于没有 “父亲”,只能自己进行类加载。它会根据类名,在标准库范围内查找是否存在匹配的 .class 文件。若 BootstrapClassLoader 没有找到,会把任务还给 “孩子”ExtensionClassLoader,接下来由 ExtensionClassLoader 负责找 .class 文件的过程。找到就加载,没找到就把任务还给 “孩子”ApplicationClassLoader。最后由 ApplicationClassLoader 负责找 .class 文件,找到就加载,没找到就抛出异常。

⭐️双亲委派模型的优点

  1. 避免重复加载类:比如A类和B类都有一个父类C类,那么当A启动时就会将C类加载起来,那么在B类进行加载时就不需要在重复加载C类了。
  2. 安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户自己提供的因此安全性就不能得到保证了。

四、JVM中的垃圾回收机制(GC)

在JVM划分的内存区域中,程序计数器中的数据在线程销毁的时候就自然释放掉了。

栈中的栈帧在方法结束,栈帧就释放了。

元数据区存放的是类加载好的数据,一般不会释放。

GC回收的是JVM中堆中的数据。

(一)找到垃圾

有以下几种方式:

1.引用计数

引用计数描述的算法为:

给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如Python语言就采用引用计数法进行内存管理。

但是引用计数不能解决循环引用的问题。

2.可达性分析

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

对象 Object5-Object7 之间虽然彼此还有关联,但是它们到 GC Roots 是不可达的,因此他们会被判定为可回收对象。
在 Java 语言中,可作为 GC Roots 的对象包含下面几种:

  1. 虚拟机栈 (栈帧中的本地变量表) 中引用的对象;
  2. 方法区中类静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中 JNI (Native 方法) 引用的对象。

(二)回收垃圾

JVM使用可达性分析算法,就可以将要回收的垃圾进行标记,标记之后就能进行回收了。

1.标记-清除算法

把标记为垃圾的内存,直接进行释放。

因为在申请内存的时候,是申请的连续的空间,如果直接进行释放,就会造成内存碎片问题。

2.复制算法

当为对象申请空间时,额外申请一倍的空间。

申请的对象只会在一个空间里,当GC时会标记清除所有的垃圾,再把剩下的移到右边的新空间里,这样就能确保对象所在的内存是连续的。

缺点:内存的空间利用率是很低的。

一但不是垃圾的对象较多,那么复制空间的成本就很高。

3.标记-整理算法

在标记-清除算法的基础上,回收垃圾之后,将不是垃圾的对象进行“插空转移”,解决了内存碎片化问题。缺点是复制成本依旧很大。

4.分代算法

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

哪些对象会进入新生代?哪些对象会进入老年代?

  • 新生代:一般创建的对象都会进入新生代;
  • 老年代:大对象和经历了 N 次(一般情况默认是 15 次)垃圾回收依然存活下来的对象会从新生代移动到老年代。

新生代区又有两个部分:伊甸区(Eden)和幸存区(S0、S1)。

新创建的对象就先放在伊甸区,经过一次GC后,存活下来的就放入幸存区。

在幸存区中执行的就是复制算法,当在幸存区中经过GC达到一定次数后,就进入老年区。

http://www.xdnf.cn/news/1417789.html

相关文章:

  • Personality Test 2025
  • 正则表达式与grep文本过滤详解
  • 【C++游记】AVL树
  • 刷题日记0901
  • (3dnr)多帧视频图像去噪 (二)
  • MySQL内置的各种单行函数
  • 强化学习实战:从零搭建自主移动机器人避障仿真(1)— 导论篇
  • 【LeetCode热题100道笔记+动画】乘积最大子数组
  • AI+PLM如何重构特种/高端复杂装备行业的工艺管理?
  • 再见 K8s!3款开源的云原生部署工具
  • 开源模型应用落地-模型上下文协议(MCP)-为AI智能体打造的“万能转接头”-“mcp-use”(十二)
  • [开源项目] Tiny-RAG :一套功能完善、高度可配的本地知识库问答解决方案
  • 深度学习篇---ShuffleNet网络结构
  • 广电手机卡到底好不好?
  • 科学研究系统性思维的方法体系:数据收集
  • 【Audio】切换至静音或振动模式时媒体音自动置 0
  • docker安装redis,进入命令窗口基操练习命令
  • 优化括号匹配检查:从Stack到计数器的性能提升
  • MOS管学习
  • Linux 进程状态 — 僵尸进程
  • FDTD_梯度波导学习(1)
  • HOW - 前端团队产出评定方案参考
  • 携程旅行 web 验证码 分析
  • JavaEE 进阶第一期:开启前端入门之旅(上)
  • GitLab 18.3 正式发布,更新多项 DevOps、CI/CD 功能【二】
  • 餐饮门店的小程序怎么做?如何开发餐饮店下单小程序?
  • C++11模板优化大揭秘:让你的代码更简洁、更安全、更高效
  • CICD实战(2) - 使用Arbess+GitLab+SonarQube实现Java项目快速扫描/构建/部署
  • 简单实现Ai音乐suno-api
  • TCP粘包