简析堆内存

简介: 转自  http://blog.csdn.net/dongpy/article/details/4555875 感谢原博主贡献精彩的文章! 堆内存是程序设计中使用最多的,操作灵活,可分配空间大,但管理麻烦,搞不好还会内存泄露和产生内存碎片。

转自  http://blog.csdn.net/dongpy/article/details/4555875

感谢原博主贡献精彩的文章!

堆内存是程序设计中使用最多的,操作灵活,可分配空间大,但管理麻烦,搞不好还会内存泄露和产生内存碎片。

1.  接口

       用户空间示意图如下,堆区从数据段尾端开始生长,动态分配普通大小的内存,超大块内存则直接在共享内存区(文件映射区)映射空间。

image 

1       进程地址空间示意图

内核记录了进程堆空间的位置,并提供系统调用brk/sbrk给用户,动态伸缩该空间。


image

 2            堆区生长示意图

用户程序通过mallocC库交互,后者再通过brk/sbrk与内核交互。用户调用C库函数malloc时,如果没有找到合适的空闲内存块,则调用brk/sbrk扩展堆空间。调用free时,也可能调用brk/sbrk收缩堆空间。

image

3       malloc调用示意图

2.  结构

       C库把堆空间当做一个数据结构来管理,一般包含空闲表和隐式表,空闲表记录空闲内存块,隐式表将分割的内存块按地址顺序连接起来。

一个比较典型的堆结构示意图如下,具体实现可参考《【例子】“首次匹配法”动态内存分配的实现》。

 image

4       典型的堆结构示意图

malloc时,在空闲表中查找合适的内存块,如果找到的内存块比申请的大很多,还要切割内存块,将切下来的空闲块加入空闲表和隐式表。

free时,先检查是否可以合并隐式表上的相邻空闲块,然后将合并后的内存块加入空闲表。

image

                                                                   图5       free操作示意图

free1后,库函数将它链入空闲表;free2后,库函数也将它链入空闲表,由于块2在堆顶,因此内核有可能会回收该空间,此时块2所在空间的映射被断开。

       所以,通常free(ptr)后,随着时间推移,ptr指向的地址空间通常有4种状态

1在空闲表中(free后,还未被重新分配出去)。

2在隐式表中,但不在空闲表中(free后,又被重新分配出去)。

3在隐式表中(部分字节已用作堆结构的控制数据,如隐式表的表头或表尾)。

4不在隐式表中(free后,被内核回收)。

如果free(ptr)后,过段时间再来访问ptr,针对上述4种状态,将会有以下结果:

1:读写都能成功;读取的是之前保留下来的值,写操作无副作用。

2:读写都能成功;读取的是不可预期的值,写操作会影响其他对象,导致程序错误。

3:读写都能成功;读取的是不可预期的值,写操作会破坏堆结构,导致程序错误。

4:读写都失败,导致进程立刻退出。

       一旦用户疏忽造成了上述错误,在测试时,

1—若出现情况4,则跟踪调试即可,因为出错的地方就是案发第一现场。

2—若出现情况3,因为破坏堆结构后,程序不会立刻出错,而要当再次访问堆结构(malloc/free)时才会出错,而且可能每次出错地点不相同。因此找到第一现场有些难度。

3—若出现情况2,与上一条类似,只是程序不一定会出错退出,而会产生错误结果。

4—若测试期间情况234都未出现,而是如情况1所述,那是最糟糕的,因为问题最终很可能会暴露在客户那里。

3.  碎片

内存碎片是指进程地址空间中的空闲内存,由于种种原因,导致这些内存无法被内核收回,形成内存碎片。

久而久之,当malloc一块大内存时,堆结构空闲表没有合适的空闲块,malloc通过系统调用sbrk扩展堆空间,但此刻空闲物理内存短缺,从而导致OOM Killer错误(暴露在写时拷贝操作,可参考《【基础】关于Linuxmalloc的写时拷贝(延迟分配)》),虽然总的空闲内存【空闲物理内存+进程释放但内核未能回收的内存】足够。

内存碎片(这里指片外碎片)的原因主要有:

1.未释放的块妨碍内核回收空闲物理内存;

2.未释放的块妨碍C库合并相邻空闲块。

image

6       内存碎片示意图

如图6所示,堆顶的内存块妨碍了内核回收空闲物理内存,2个空闲块间的内存块妨碍了C库合并空闲块。

4.  定制

如果内存碎片不及时处理,那么随着时间推移,情况可能会越来越糟,最终将导致系统瘫痪。这对于那些长时间运转的系统(如工业控制系统)来说,是致命的。因此对于一些专用系统,需要定制合适的内存管理器。

自定义的动态内存管理可以是基于C库的二次管理,也可以直接分配静态内存进行管理,后者就干脆屏蔽了堆内存的使用。

一般我们可以采用前者,即在C库的基础上,再自定义内存管理器,管理算法有很多,其中内存池比较常用(同时还可使用一些简单的垃圾回收机制),主要适用于有小内存块频繁分配和释放的系统。主要思路是:将小内存块集中在堆的底部分配,避免小内存块散落在大内存块之间而造成碎片。具体实现可参考Slab算法。

相关文章
|
11月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
1796 1
|
监控 Java
压力测试Jmeter的简单使用,性能监控-堆内存与垃圾回收 -jvisualvm的使用
这篇文章介绍了如何使用JMeter进行压力测试,包括测试前的配置、测试执行和结果查看。同时,还探讨了性能监控工具jconsole和jvisualvm的使用,特别是jvisualvm,它可以监控内存泄露、跟踪垃圾回收、执行时内存和CPU分析以及线程分析等,文章还提供了使用这些工具的详细步骤和说明。
压力测试Jmeter的简单使用,性能监控-堆内存与垃圾回收 -jvisualvm的使用
|
10月前
|
算法 Java
堆内存分配策略解密
本文深入探讨了Java虚拟机中堆内存的分配策略,包括新生代(Eden区和Survivor区)与老年代的分配机制。新生代对象优先分配在Eden区,当空间不足时执行Minor GC并将存活对象移至Survivor区;老年代则用于存放长期存活或大对象,避免频繁内存拷贝。通过动态对象年龄判定优化晋升策略,并介绍Full GC触发条件。理解这些策略有助于提高程序性能和稳定性。
|
10月前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
236 5
|
12月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
545 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
缓存 Java 测试技术
谷粒商城笔记+踩坑(11)——性能压测和调优,JMeter压力测试+jvisualvm监控性能+资源动静分离+修改堆内存
使用JMeter对项目各个接口进行压力测试,并对前端进行动静分离优化,优化三级分类查询接口的性能
455 10
谷粒商城笔记+踩坑(11)——性能压测和调优,JMeter压力测试+jvisualvm监控性能+资源动静分离+修改堆内存
|
12月前
|
算法 Java
JVM进阶调优系列(3)堆内存的对象什么时候被回收?
堆对象的生命周期是咋样的?什么时候被回收,回收前又如何流转?具体又是被如何回收?今天重点讲对象GC,看完这篇就全都明白了。
|
12月前
|
C++
析构造函数就是为了释放内存,就是在局部指针消失前释放内存,拷贝构造函数就是以构造函数为模块,在堆里面新开一块,同一个变量在堆里面的地址
本文讨论了C++中构造函数和析构函数的作用,特别是它们在管理动态内存分配和释放中的重要性,以及如何正确地实现拷贝构造函数以避免内存泄漏。
129 2
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
存储 程序员 编译器
堆和栈内存的区别是什么
【8月更文挑战第23天】堆和栈内存的区别是什么
1070 4