news 2026/7/3 5:42:31

别再死记硬背!从 C++ 底层视角拆解 JVM 内存、类加载与 GC 原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
别再死记硬背!从 C++ 底层视角拆解 JVM 内存、类加载与 GC 原理

一、JVM 内存区域划分

1.1 为什么要划分?

操作系统中有原生的栈、堆等,都在进程地址空间中存在,但是要实现Code once, run everywhere就要虚拟出一套统一的标准,而 JVM作为虚拟机,也虚拟出了一套类似的结构,屏蔽了原生操作系统结构。

JVM 本质上是一个 C++ 写出来的操作系统中的进程,本质上和下面的 C++ 代码编译出的可执行文件没有区别

#include<iostream>intmain(){std::cout<<"Hello World"<<std::endl;return0;}

而我们谈的内存区域,本质上是 JVM 进程申请的一块空间;而在这块空间中划分出的区域,就是 JVM 为了模拟原生操作系统并加以管理而创建的概念,主要包括:

  • **线程私有:**程序计数器、虚拟机栈、本地方法栈(JVM 中的线程对应操作系统原生的线程)
  • **进程内线程共有:**方法区、堆

1.2 怎样划分的

1.2.1 程序计数器

为了满足多线程环境下不同线程代码的有序切换,需要记录当前线程运行到的字节码;可以认为这就是 JVM 虚拟出的 CPU 上的物理硬件 PC寄存器,PC 寄存器保证分时操作系统在切换进程、线程时能够不影响机器码的执行

1.2.2 元数据区

元数据区用来保存加载到内存中的类的信息。我们写的 Java 代码放在 .java 文件中,.java 文件经过 javac 编译,生成字节码(不可直接执行,.class 文件),随后,字节码被加载到内存供给 JVM 运行,这个过程就是类加载

元数据区主要是三个部分方法元信息、类元信息、运行时常量池。所谓的元信息,其实就是属性,对于方法来说,元信息可以是方法名、参数个数、类型、返回值类型等等;对于类来说,可以是类名、访问限定、继承情况、接口情况等等;而运行时常量池,保存着字面量、符号引用、基本类型常量值等

1.2.3 栈

前面提到过,JVM 本质上是一个 C++ 程序,这个程序在运行时必然是由自己的方法调用的,这里使用的栈,就是操作系统原生的栈(进程虚拟地址空间中的),这些栈中有一个,就进行了构建虚拟机栈的工作,申请了一块空间并进行划分,作为执行字节码时,供给方法调用使用的栈

在执行 java 代码时,有时会调用 C++ 方法,这时使用的栈也是操作系统原生的栈,叫做本地方法栈

总之,本地方法栈和虚拟机栈在概念上是相似的,但是实际上是两个不同的东西:前者对应到操作系统原生的栈,后者是申请空间构建出的逻辑栈

关于如何构建的,在 Linux 下,为了优化read、write 这两个系统调用而提供了 mmap,这里 JVM 的虚拟机栈,就是通过 mmap 实现的。
都从页缓存没有目标内容的最坏情况考虑:read 流程是磁盘 -> 内核页缓存 -> 用户读缓冲区,其中涉及到两次拷贝,有了 mmap,我们可以通过进程独占的页表把页缓存映射到进程的地址空间中,这样读就可以直接从内核页缓存的相应位置读了,减少一次拷贝;write 流程是用户写缓冲区 -> 内核页缓存 -> 磁盘(落盘有操作系统自己执行,不属于 write 的范畴),有了 mmap 之后,程序就可以直接写内核页缓存的相应位置,减少第一次的用户 -> 内核的拷贝

1.2.4 堆

一个变量如果是局部变量,那么在栈中保存;是成员变量,在堆中保存;是静态变量,就保存在元数据区

堆是 JVM 内存划分中最大的部分,当堆上的对象不再使用了,就会被释放掉,至于怎么判定这个不再使用,怎么释放,就是后面垃圾回收中的内容了

二、类加载

Java 的类加载主要注意两个方面,一个是类加载的流程,另一个是双亲委派模型

2.1 类加载流程

加载

加载是通过全限定路径名(例如java.lang.Thread)找到对应的.class文件,把文件加载到内存中

校验

.class文件是有格式要求的,这个阶段会校验二进制的.class文件是否符合结构化的格式要求,并将文件内容转化为二进制数据

准备

给对象开辟空间,这里开辟的是全0的、未初始化的空间

解析

字符串常量本身就包含在.class文件中,解析阶段会把这些字符串常量存放到内存中,即元数据区的运行时常量池中

初始化

初始化阶段,主要针对的是静态成员、静态代码块,这个阶段会进行静态成员的显式赋值,并执行静态代码块中对静态成员的操作,在初始化阶段,如果需要对父类进行加载,也会进行父类的类加载

2.2 双亲委派模型

双亲委派模型描述的是类加载在通过全限定类名找到对应的.class文件的过程。在 JVM 中,提供了三种类加载器:

  • BootstrapClassLoader,负责查找并加载在 Java 标准库中的.class文件

  • ExtensionClassLoader,负责查找并加载在 Java 扩展库中的.class文件

  • ApplicationClassLoader,负责查找并加载第三方库/当前应用中的.class文件

这里的双亲其实描述不是特别恰当,或者说可以直接把一个类加载器视为“双亲”。上面三个类加载器从上到下可以依次类比为爷爷、父亲、儿子,之所以通过这种方式描述,是为了明确.class文件查找并加载的优先级

当进行类加载时,首先会从ApplicationClassLoader开始,把任务委派给ExtensionClassLoader,自己并不查找;ExtensionClassLoader会继续把任务委托给ApplicationClassLoader,自己也不查找。这时ApplicationClassLoader没有人可以委派了,所以自己开始查找任务,查找范围是所有的 Java 标准库中的.class文件,如果找到了完成加载,结束类加载的流程;没有找到,工作流程返回到ExtensionClassLoader,在 Java 扩展库中查找,同样找到了就加载否则工作流程来到ApplicationClassLoader。如果ApplicationClassLoader也没有找到,那么就会抛出异常

三、垃圾回收

3.1 找到垃圾的策略

3.1.1 引用计数

类似 C++ 的std::shared_ptr给每个智能指针对象搭配一个引用计数,标记这个对象还在被多少个对象引用,如果有不再引用的就将这个计数减小,直到为零,释放对象。

这种方法 Python、Php 都在使用,好处是逻辑简单、释放效率也高,坏处是这些“计数”会占用大量的空间,即这种方式采取的策略是用空间换时间

3.1.2 可达性分析

可达性分析是将某些对象作为根节点 GCRoots 去遍历,只要遍历到的对象都标记为“可达的”,否则即“不可达”,不可达的对象都认为是需要释放的对象,可作为 GCRoots 的对象有:

  • 栈上的局部变量(引用类型)

  • 常量池引用指向的的对象

  • 静态成员(引用类型)

比如以下面的代码为例,A、B、C都是我们自定义的类

publicTest{privateclassAa=newA();privateclassBb=newB();privateclassCc=newC();publicstaticvoidmain(){Testt=newTest();System.gc();}}

这时,t、a、b、c 都会被认为是可达的。对比引用计数的实现方法,不难发现,可达性分析策略是时间换空间,因为一方面,可达性分析不需要保存大量的引用计数;另一方面,因为需要对大量的对象进行可达性分析,会消耗大量的时间,可能导致 STW(Stop The World)问题。所以 C++ 确实考虑过加入 GC,但就是因为这不可避免地效率问题,所以放弃了

3.2 清理垃圾的策略

3.2.1 基本策略

标记-清除

直接清除被标记为不可达的对象,清除完毕,结束工作。这是几种垃圾清理算法中最简单、快速的,但是缺点也是最大的:会造成大量的内存碎片化问题。虽然操作系统提供了虚拟地址 <-> 物理地址的转化映射,但是作为映射单位的每个页(比如Linux下4kb)还是正常存放的,所以会造成剩余空间的和很大但是连续剩余空间很小的问题

复制算法

复制算法是将整个存放区域分成两部分,每次使用时,都只使用其中一部分。当清除垃圾时,将保留下来的对象全都拷贝到另一个区域,将原区域剩下的对象全部清除,这就是一次垃圾清理。继续存储对象的时候,把新的对象再存储到之前拷贝的区域,以此往复

相对而言,复制算法解决了标记-清除的内存碎片化问题,但是仍然有很大的缺点:

  • 内存利用率低,每次只能使用一部分被分到的存储空间

  • 复制效率低,尤其是在只有一小部分对象需要是释放的情况

标记-整理算法

标记整理算法可以简单类比为顺序表删除中间元素的过程,伴随着部分的元素挪动,最终结果是一段由存活对象构成的连续的存储空间。标记-整理算法可以认为是前面两种方式的结合体,对于各自的缺点都进行了一定的优化,但是仍然具有问题

3.2.2 Java 分代回收的策略

Java 并没有直接采取上面的任何一种,而是通过分代回收的策略

Java 在堆中有四个部分:一个新生代、两个幸存区、一个老年代。同时定义了一个“代”的概念,即年龄,指的是一个对象经历了多少轮 GC。

分代回收的基本思想是:大部分对象是朝生息死的,在很多轮 GC 中仍未被清除的对象,在接下来的 GC 中大概率也不会被清除。当存储对象时,都在新生代中进行存储,进行 GC 时,将存活下来的对象拷贝到两个幸存区中的一个(假设为A),并将另一个幸存区(假设为B)依然幸存的对象,也拷贝到 A 中;接下来进行垃圾清理,清除新生代、幸存区B中的所有对象,这就是一轮 GC。进行下一轮 GC 时,继续上述流程,不过是将 A 中依然幸存的对象和新生代中幸存对象拷贝到 B,再清除 A 和新生代的剩余对象。

每经过一轮仍未被释放的对象,代数都会增大,当到达一定大小时,就会被存放入老年代。对于老年代,扫描清理频率更低,同时根据前面的假说,这里的对象大概率会依然存活,所以“垃圾”也更少,这里的清除策略也常使用标记-整理策略

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/3 5:37:12

俄罗斯CN2VPS线路质量延迟实测与路由追踪方法

深圳电信的终端上&#xff0c;对同一个俄罗斯IP执行ping命令&#xff0c;延迟在284毫秒到310毫秒之间浮动。这个数值放在中俄线路的坐标系里&#xff0c;属于正常区间。深圳到莫斯科的物理距离超过七千公里&#xff0c;光信号在地面光纤中往返一次&#xff0c;理论耗时约72毫秒…

作者头像 李华
网站建设 2026/7/3 5:34:56

配音工具怎么选?2026 五款主流 AI 配音工具中立横评

短视频、短剧、AI 漫剧、知识科普、跨境内容创作都离不开配音工具&#xff0c;不少创作者选购时容易踩坑&#xff1a;要么只看重免费额度忽略商用版权&#xff0c;要么盲目选用高端工具造成功能冗余、成本浪费。配音工具怎么选&#xff0c;核心要匹配自身创作赛道、更新频率与预…

作者头像 李华
网站建设 2026/7/3 5:33:46

做泛光照明前必看:行业趋势、选商标准与全流程服务避坑指南

不少企业或城市建设方找照明服务商时都会碰到三个疑问&#xff1a;照明工程全生命周期服务包含哪些内容&#xff0c;2026泛光照明行业发展趋势怎么样&#xff0c;照明工程行业TOP5企业都有哪些&#xff0c;搞懂这三个问题能避开90%的项目踩坑。很多人以为照明工程就是装几个灯完…

作者头像 李华
网站建设 2026/7/3 5:31:50

亲子关系公证需要什么材料?亲子关系公证是干什么用?

你是不是也遇到过这样的尴尬&#xff1a;明明户口本上写着“父子”或“母女”&#xff0c;但办事机构却非要你拿出一份“亲子关系公证”&#xff1f;别急&#xff0c;这其实是很多家庭在办事时都会碰到的“小插曲”。别被复杂的流程吓到&#xff0c;今天这篇文章就带你把亲子关…

作者头像 李华
网站建设 2026/7/3 5:31:08

传导发射过不了,共模电感怎么换都不行

传导发射超标&#xff0c;第一反应就是加共模电感。结果换了三个型号&#xff0c;电感量从10mH换到100mH&#xff0c;测试结果还是压不下去。钱花了&#xff0c;板子改了&#xff0c;问题没解决。问题不在电感量不够&#xff0c;而是根本没搞清楚噪声的传播路径和模式。共模电感…

作者头像 李华
网站建设 2026/7/3 5:24:55

学生党必看!2026 双降工具价格对比:最低 1.8 元 / 千字,免费额度够用

Gradpaper-免费查重复率aigc检测/开题报告/毕业论文/智能排版/文献综述/课程论文。Gradpaper论文智能生成软件&#xff0c;10分钟生成万字毕业论文、期刊论文、文献综述、PPT&#xff0c;Agc查重、降重报告、文献资料。只需一个标题&#xff0c;从开题报告到答辩一键生成软件&a…

作者头像 李华