深入解析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 中就会根据对象年龄的差异,把整个堆内存分成两个大的部分

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

相关推荐
还梦呦2 分钟前
2025年09月计算机二级Java选择题每日一练——第一期
java·开发语言
与火星的孩子对话10 分钟前
Unity高级开发:反射原理深入解析与实践指南 C#
java·unity·c#·游戏引擎·lucene·反射
♞沉寂20 分钟前
信号以及共享内存
linux·c语言·开发语言
花开富贵ii31 分钟前
代码随想录算法训练营四十六天|图论part04
java·数据结构·算法·图论
Miraitowa_cheems35 分钟前
LeetCode算法日记 - Day 15: 和为 K 的子数组、和可被 K 整除的子数组
java·数据结构·算法·leetcode·职场和发展·哈希算法
答题卡上的情书35 分钟前
java第一个接口
java·开发语言
王廷胡_白嫖帝1 小时前
Qt密码生成器项目开发教程 - 安全可靠的随机密码生成工具
开发语言·qt
RainbowSea1 小时前
伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 06
java·spring boot·后端
CHENFU_JAVA1 小时前
EasyExcel 合并单元格最佳实践:基于注解的自动合并与样式控制
java·excel
hllqkbb2 小时前
从 SGD 到梯度累积:Epoch、Batch、Step 的关系全解析
开发语言·人工智能·opencv·计算机视觉·batch