深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制

简介: 本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。

1. JVM 内存区域划分

  1. 程序计数器(空间比较小)。保存了下一条要执行的指令的地址(指向元数据区指令的地址)
  2. 堆。JVM 最大的空间,new 出来的对象都在堆上
  3. 栈。函数中的局部变量,函数的形参,函数之间的调用关系都存放在栈上。栈也可以分为 Java 虚拟机栈和本地方法栈,Java 虚拟机栈主要服务于 Java 方法的执行,而本地方法栈主要服务于本地方法(C/C++实现的方法)
  4. 元数据区(方法区)。存放 Java 程序中的指令(指令都是包含在类的方法中),也就是保存了代码中涉及到类的相关信息,同时也保存了类的 static 属性

在一个 Java 进程中,元数据区和堆是只有一份的,程序计数器和栈可能有多份(当一个 Java 进程中有多个线程的时候,每个线程都有自己的程序计数器和栈,因为每个需要记录自己接下来需要执行什么指令还有其中的函数调用关系),由于堆和元数据区是共用一份的,所以如果是在堆上 new 了一个对象,或是元数据区的一个 static 属性,每一个线程都可以访问。

来看下面创建的变量都处在哪个内存区域:

一个变量处在哪个内存区域和变量是不是“内置类型”无关,而是和变量的形态有关

局部变量:栈           成员变量:堆          静态成员变量:元数据区(方法区)

所以说 a,b在堆区 c,d 在方法区,e,f 在元数据区(方法区)

2. JVM 类加载的过程

我们平时写的 Java 代码是一个 .java 文件,通过 javac 编译成 .class 文件,既然是文件,那么都是存储在硬盘上的,当运行 Java 进程的时候,JVM 就需要读取 .class 中的内容,并且执行里面的指令,这就是类加载的过程,也就是把类涉及到的字节码从硬盘上读取到内存中(元数据区), .class 中的指令转变为类对象,类对象中就包含了 .class 文件的各种信息,比如:类的名字,类中定义的属性,方法,实现的接口和继承关系等。

下面介绍一下类加载的具体过程:

2.1. 加载

  1. 加载。找到对应的 .class 文件,通过类的全限定名打开并读取文件内容,获取定义此类的二进制字节流。

2.2. 验证

  1. 验证。验证是确保 Class 文件的字节流中包含的信息符合当前 JVM 的要求,在 Java 标准文档中,也明确定义了 Class 文件的格式

2.3. 准备

  1. 准备。准备阶段是正式为类中被 static 修饰的变量(类变量)在方法区中分配内存空间的,这些类变量在整个类的生命周期中都是有效的,并且被所有的实例对象所共享,根据上面读取到的内容来确定出需要的内存空间,然后申请内存空间,并把空间内所有的内容都初始化为 0

例如,对于 public static int value = 10;,在准备阶段,value 会被初始化为 0,而不是   10。真正的赋值 10 是在初始化阶段进行的。

2.4. 解析

  1. 解析。主要是针对类中的字符串常量进行处理,JVM 将常量池中的符号引用替换为直接引用的过程,也就是初始化常量的过程。

符号引用和内存引用:字符串常量已经在 Class 文件中了,这时是没有“地址”这样的概念的,地址是内存的地址,还没有加载到内存中,这时是用“文件偏移量”来表示的,当加载到内存空间之后就会使用地址来标识,也就是直接引用

2.5. 初始化

  1. 初始化:Java 虚拟机真正开始执⾏类中编写的 Java 程序代码,将主导权移交给应⽤程序。初始
    化阶段就是执⾏类构造器⽅法的过程 。例如创建类的实例,调用类的静态方法,访问类或接口的静态变量(不包括 final 修饰的静态变量,在编译阶段就确定了值),初始化子类时,父类会先被初始化,执行静态代码块。

普通的成员变量(非静态变量)不会在 JVM 类加载过程中的初始化阶段被初始化。类加载过程中的初始化阶段主要是针对类变量(被 static 修饰的变量)以及执行静态代码块。普通成员变量是在对象创建时,即通过构造函数或其他实例初始化方法进行初始化的。当使用 new 关键字创建对象时,会为对象在堆内存中分配空间,并对成员变量进行初始化等操作。

2.6. 双亲委派模型

双亲委派模型是 Java 类加载机制中的一种重要设计模式,也就是上述类加载中的第一步

JVM 默认是有三个类加载器的:

这里的父子关系并不是 Java 中的继承关系,而是类似与指向的一个 parent 引用

双亲委派模型工作原理:

当一个应用程序需要加载一个类时,应用程序类加载器首先会将加载请求委派给它的父类加载器扩展类加载器,扩展类加载器再委派给启动类加载器。如果启动类加载器无法加载该类,扩展类加载器会尝试加载。如果扩展类加载器也无法加载,最后应用程序类加载器才会尝试自己去加载。

那为什么不一开始就给启动类加载器来执行而是又绕了一圈?

  1. 避免类的重复加载:由于父类加载器优先加载,相同的类只会被加载一次,避免了类的重复加载和内存浪费。
  2. 保证核心类库的安全性:防止核心类库被恶意篡改(程序员自己写的类和标准库中的类名字一样,直接从启动类加载器加载会覆盖掉标准库的类),确保了 Java 程序的稳定性和安全性。

就类似与公司中的员工,领导和老板,当员工发现问题是需要上报给领导,领导再上报给老板,如果说这个问题比较重要,老板就亲自解决,如果不是那么重要,但也有一定分量,就交给领导解决,如果只是一些小事,就让员工自己解决

其实类加载器可以程序员自定义,自定义的类加载器可以遵守双亲委派模型也可以不遵守。

3. JVM 的垃圾回收机制(GC)

JVM 的垃圾回收主要是在堆内存进行的,这里存储的都是 new 出来的对象,当这些对象没有引用指向的时候,就成为了垃圾,如果放任不管,就会一直占用着内存,最终造成内存泄露

进行垃圾回收可能会触发 “Stop The World”(STW)问题:在执行垃圾回收的某个阶段,为了保证垃圾回收的准确性和一致性,需要暂停所有应用程序线程,使得整个应用程序出现短暂的停顿。

所以说 C++ 这种追求高性能的语言并没有使用垃圾回收机制

接下来看 JVM 中是如何进行垃圾回收的

首先,进行垃圾回收肯定需要先标记出哪些对象是垃圾

那么怎么去找哪个对象是垃圾呢?

3.1. 识别垃圾

方案一:引用计数。

给每一个对象分配一个计数器,来统计有多少引用指向,每增加一个引用,计数器 + 1,每减少一个引用,计数器 - 1,当计数器减为 0,就变成垃圾了

这个方法看似可行,python 等一些语言采用的就是这个,但是还存在一个问题:

无法解决循环引用的问题。例如,对象 A 和对象 B 相互引用,除此之外没有其他地方引用它们,此时它们的引用计数器都不为 0,但实际上它们已经不再被程序使用,应该被回收。

同时,引入计数器,也会消耗掉额外的空间

方案二:可达性分析

以一系列称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,是垃圾对象,也就是在类似图的结构中从多个根节点遍历一遍,如果发现有的对象没有任何路径可以到达,也就是没有引用指向了,那么就是垃圾

可达性分析有效的解决了循环引用的问题,可以作为根节点的可以是方法区中类静态属性引用的对象(static 修饰的变量引用的对象),常量池中引用的对象,方法中的局部变量引用的对象,本地方法栈引用的对象。

可达性分析由于需要每次都遍历一遍对象,所以是比较消耗时间的

3.2. 垃圾回收的方法

  1. 标记 - 清除法。

在上面遍历的时候标记出哪些是垃圾,然后回收所有被标记的对象所占用的空间,这种方法有很大的缺点:由于每次申请的内存都是一块连续的区域,所以会产生内存碎片,回收后的内存不完整,接下来再申请内存的话可能会申请失败

  1. 复制算法。

将堆空间分为两个大小相等的区域,一般称为 From 区和 To 区。当进行垃圾回收时,将 From 区中仍然被引用的对象(也就是需要继续使用的对象)复制到 To 区,然后清空 From 区。下次垃圾回收时,交换 From 区和 To 区的角色。

这种算法缺点也很明显,就是空间利用率低,一大块区域只有一半能用,同时如果存活下来的对象有很多,复制的成本也比较大

  1. 标记 - 整理算法。

标记阶段就是把哪些对象是垃圾标记出来,接下来整理就是把存活的对象移动到另一端,然后回收边界以外的空间

这种方法的缺点就是移动对象的时候比较消耗时间,特别是对象很多的情况下

3.3. 分代收集算法

根据对象的生命周期将堆空间分为新生代和老年代,也就是在可达性分析的过程中,每经过一轮扫描,对象仍然存活(不是垃圾),年龄就 + 1,新生代中的对象生命周期较短,很多对象短时间内就变成垃圾了,老年代中的对象生命周期较长。

新创建的对象首先在伊甸区分配内存,当伊甸区满时,触发一次新生代垃圾回收,将伊甸区和幸存区 1中仍然存活的对象复制到幸存区 2,然后清空伊甸区和幸存区 1。下次垃圾回收时,交换幸存区 1 和幸存区 2 的角色,继续重复上述操作,幸存区 1 到 幸存区 2 二者的转化会反复进行多次

由于新生代对象大多存活时间少,要复制的也比较少,效率并不会有多大影响,同时空间也没有太大浪费,两个幸存区的空间比较小,也不会有太大空间的浪费。

当一个对象在幸存区反复拷打,年龄不断增加,就进入了老年代,老年代的对象生命周期长,如果还采用复制算法的话,要复制的对象就很多,影响效率,所以就可以采用标记 - 整理算法,需要整理的次数也不多。

上述过程就很好的利用了各个算法的优势来结合,通过分代收集算法,提高了垃圾回收的效率

3.4. 垃圾回收器

JVM 中还提供了多种“垃圾回收器”,对上面介绍的分代回收算法进行了进一步的扩充和具体实现

CMS 垃圾回收器

先大概的进行一个初始标记,然后再通过多线程的方式并发的进行重新标记,标记之后再进行并发的清理,通过这样的方法,能和业务线程并发执行,使 STW 的时间尽可能的缩短

G1 垃圾回收器

G1 是把整个堆内存分层很多小块,进行垃圾回收时,不要求一个周期就把所有的垃圾全都回收,而是一轮周期回收一部分,通过限制每一轮 GC 花的时间在可控范围之内,同时划分为多个区域也是为了更好的使用多线程

相关文章
|
12天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
8天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2522 18
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
8天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1525 15
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
4天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
10天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
596 14
|
1月前
|
运维 Cloud Native Devops
一线实战:运维人少,我们从 0 到 1 实践 DevOps 和云原生
上海经证科技有限公司为有效推进软件项目管理和开发工作,选择了阿里云云效作为 DevOps 解决方案。通过云效,实现了从 0 开始,到现在近百个微服务、数百条流水线与应用交付的全面覆盖,有效支撑了敏捷开发流程。
19283 30
|
10天前
|
人工智能 自动驾驶 机器人
吴泳铭:AI最大的想象力不在手机屏幕,而是改变物理世界
过去22个月,AI发展速度超过任何历史时期,但我们依然还处于AGI变革的早期。生成式AI最大的想象力,绝不是在手机屏幕上做一两个新的超级app,而是接管数字世界,改变物理世界。
498 49
吴泳铭:AI最大的想象力不在手机屏幕,而是改变物理世界
|
1月前
|
人工智能 自然语言处理 搜索推荐
阿里云Elasticsearch AI搜索实践
本文介绍了阿里云 Elasticsearch 在AI 搜索方面的技术实践与探索。
18842 20
|
1月前
|
Rust Apache 对象存储
Apache Paimon V0.9最新进展
Apache Paimon V0.9 版本即将发布,此版本带来了多项新特性并解决了关键挑战。Paimon自2022年从Flink社区诞生以来迅速成长,已成为Apache顶级项目,并广泛应用于阿里集团内外的多家企业。
17530 13
Apache Paimon V0.9最新进展
|
3天前
|
云安全 存储 运维
叮咚!您有一份六大必做安全操作清单,请查收
云安全态势管理(CSPM)开启免费试用
368 4
叮咚!您有一份六大必做安全操作清单,请查收