深入解析JVM内存管理与垃圾回收机制

JVM Java虚拟机

jdk java开发工具包

jre java 运行是环境

jvm java虚拟机(解释执行java字节码)

JVM 中的内存区域划分

JVM 其实也是一个进程

进程运行过程中,要从操作系统这里申请一些资源

这些内存空间,就支撑了后续 java 程序的执行

比如 在 java 中定义变量(就会申请内存),内存其实就是 jvm 从系统这边申请到的内存

jvm 从系统申请了一大块内存,这一大块内存给 java 程序使用的时候,又会根据实际的使用用途,来换分出不同的空间,这个就是"区域划分"

这就像是买了个房,然后里面的区域布置就是根据自己需求来划分

1.堆(只有一份)

代码中 new 出来的对象,就都是在堆里

对象中持有的非静态成员变量,也就是在堆里

2.栈(可能有 N 份)

本地方法栈/虚拟机栈

包含了方法调用关系和局部变量

3.程序计数器(可能有 N 份)

这个区域比较小的空间,专门用来存储下一条要执行的 java 指令的地址

每个线程都有自己的程序计数器和栈(每个线程自己的执行流)

4.元数据区(只有一份)

"元数据" 是计算机中的一个常见术语

往往指的是一些辅助性质的,描述性质的属性

类的信息,方法的信息

一个程序,有哪些类,每个类都有哪些方法,每个方法里面都要包含哪些指令,都会记录在元数据区中

我们写的 java 代码,if,while,for 各种逻辑运算,这些操作最终都会被转换成 java 字节码

(javac 就会完成完成上述代码 => 字节码)

此时这些字节码在程序运行的时候就会被 jvm 加载到内存中,放到元数据区(方法区)中

此时,当前程序要如何执行,要做哪些事情,就会按照上述元数据区里记录的字节码依次执行了

硬盘上不仅仅要存文件数据本体,还需要存储一些辅助信息

比如,文件的大小,文件的位置,文件的拥有者,文件的修改时间,文件的权限信息

统称为"元数据"

static 修饰的变量,称为"类属性"

static 修饰的方法,称为"类方法"

非 static 的变量,称为"实例属性"

非 static 的方法,称为"实例方法"

上述带有 static 修饰的变量,就是在类对象中,也就是在 元数据区中

区分一个变量在哪个内存区域中,最主要就是看变量的形态(局部变量,成员变量,静态成员变量)

JVM 的类加载机制

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

.class 文件 => 类对象

硬盘 => 内存

类加载过程在 Java 官方文档中给出说明

类加载大体的过程可以分成 5 个步骤

1.加载

在硬盘上的 .class 文件找到打开文件,读取到文件内容

2.验证

当前需要确保读到的文件的内容,是合法的 .class 文件格式

3.准备 给类对象,申请空间

此时申请到的内存空间,里面的默认值,都是全 0 的

(这个阶段中,类对象里的静态成员变量的值也就相当于是 0 了)

4.解析 主要是针对类中的字符串常量进行处理
5.初始化 针对类对象完成后续的初始化

还要执行静态代码块的逻辑,还可能会触发父类的加载

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

双亲委派模型

描述了如何查找 .class 文件的策略

JVM 中进行类加载的操作,是有一个专门的模块,称为"类加载器"

JVM 中的类加载器默认是有三个的(也可以自定义)

上述的三个类加载器,存在"父子关系"

3.垃圾回收机制(GC)

引入这样的机制之后,就不需要靠手动来进行释放了,程序会自己判定,某个内存是否会继续使用

如果内存后续不用了,就会自己释放掉

垃圾回收中的一个很重要的问题:STW(stop the world)问题

触发垃圾回收的时候很可能会使当前程序的其他业务逻辑被暂停

垃圾回收,是回收内存,JVM 中的内存有好几块

1.程序计数器(不需要GC)

2.栈(不需要GC)

局部变量都是在代码块执行结束之后自动销毁,(这是栈自己的特点和垃圾回收没啥关系)

生命周期都非常明确

3.元数据区/方法区(一般不需要GC)

一般都是涉及到"类加载"很少涉及到"类加载"

4.堆 是GC 主要的战场

这里的回收内存,更准确的说是"回收对象"

每次垃圾回收的时候,释放的若干个对象(实际的单位都是对象)

垃圾回收,具体是怎样进行展开的

1.识别出垃圾,那些对象是垃圾(不再使用),那些对象不是垃圾
2.把标记为垃圾的对象的内存空间进行释放
1.识别出垃圾

半段这个对象后续是否要继续使用

在 Java 中,使用对象,一定需要通过引用的方式来使用(有一个例外,匿名对象)

new MyThread().start();

这行代码执行完,对行的 My Thread 对象就会被当做垃圾

如果一个对象没有任何引用指向它,就视为无法被代码中使用,就可以作为垃圾了

2.可达性分析

本质上是用 "时间" 换 "空间",相比与引用计数,需要消耗更多的额外的时间,但是总体来说,还是可控的,不会产生类似于"循环引用"这样的问题

在写代码的过程中,会定义很多的变量

比如,栈上的局部变量 / 方法区 中的静态类型的变量 / 常量池中引用的对象

就可以从这些变量作为起点,出发,尝试去进行"遍历"

所谓的遍历就是会沿着这些变量中持有的引用类型的成员,再进一步的往下进行访问

所有能被访问到的对象,自然就不是垃圾了,剩下的遍历一圈也访问不到的对象,自然就是垃圾

已二叉树举例

主要释放的方式有三种
1.标记 - 清除

把标记为垃圾的对象,直接释放掉

此时就是把标记为垃圾的对象对应的内存空间直接释放

上述释放方式,就可能会产生很多小的,但是离散的 空闲内存空间

就可能会导致后续申请内存失败

内存申请,都是一次申请一个连续的内存空间

申请 1M 内存空间,此时 1M字节都是连续的

如果存在很多内存碎片,就可能导致,总的空闲空间,远远超过 1MB,但是并不存在比 1MB 大的连续的空间,此时去申请空间就会失败

2.复制算法

复制算法,核心就是不直接释放内存,而是把不是垃圾的对象,复制到内存的另一半里

接下来把左侧空间整体释放掉

这确实能规避内存碎片问题,但是也是有缺点的
1.总的可用内存变少了

2.如果每次要复制的对象比较多,此时复制开销也就很大了,要是当前这一轮 GC 的过程中,大部分对象都释放,少数对象存活,这个时候适用使用复制

3.标记 - 整理 也能解决内存碎片问题

JVM 中没有直接使用上述的方案,而是结合上述思想,搞出了一个"综合性"方案,取长补短

分代回收(依据不同种类的对象,采取不同的方式)

引入概念,对象的年龄

JVM 中有专门的线程负责周期性扫描/释放

一个对象,如果被线程扫描了一次,可达了,年龄就 + 1(初始年龄相当于是 0)

JVM 中就会根据对象年龄的差异,把整个堆内存分成两个大的部分

新生代(年龄小的对象) / 老年代(年龄大的对象)

相关推荐
Scott9999HH2 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔2 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社3 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海3 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖4 小时前
JDK 26 新特性详解
java·开发语言
马优晨5 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区7 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大7 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai7 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说8 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json