简介
本文章主要介绍了内存的基本知识,简单的判断内存是否耗尽、是否出现内存泄漏等问题。其他硬件性能分析如下:
1. 硬件性能 - CPU瓶颈分析
2. 硬件性能 - 磁盘瓶颈分析
3. 硬件性能 - 网络瓶颈分析
目录
1. 内存的定义
1.1. 内存的基本概念
1.2. 内存的特点
1.3. 内存的单位
1.4. 内存的相关术语
2. 内存的层次结构
3. 内存管理
3.1. 地址空间
3.2. 内存分配
3.3. 虚拟内存
3.4. 内存算法
4. 内存的读写原则
5. 内存优化技巧
6. 监控内存
6.1. free 命令
6.2. top 命令
6.3. sar 命令
7. 简单分析内存
7.1. 判断内存不足
7.2. 判断内存溢出
内存(Memory)是指计算机中用于临时存储数据和程序的设备,它与计算机中的CPU紧密结合,用于存储当前正在执行的程序指令和处理的数据。
- 暂时性存储:内存的主要用于存储计算机正在运行的程序数据,如果在程序运行过程中计算机出现故障(断电、异常),数据将会丢失。而保存数据信息的则是磁盘(永久性存储,即使关机也不会丢掉信息),也就是说内存中的数据转移到了磁盘后,则不会出现数据丢失的情况;如果在将数据转移到磁盘的过程中(部分数据未转移完成)计算机故障,那么这部分的数据仍然会丢失。
- 快速访问:内存具有非常快的读写速度,可以迅速读取和写入数据。相对于磁盘或其他介质,内存的访问无疑是最快的。
- 随机访问:内存是一种 "随机存取存储器",可以直接通过内存地址来快速访问和修改存储在内存中的数据。
- 容量大小:相对于磁盘的存储设备,内存的容量通常较小。而内存的容量是决定计算机能够同时处理程序规模和数据量的决定性因素。
1024B = 1KB = 2^10字节
1024KB = 1MB = 2^20字节
1024MB = 1GB = 2^30字节
1024GB = 1TB = 2^40字节
1024TB = 1PB = 2^50字节
1024PB = 1EB = 2^60字节
1024EB = 1ZB = 2^70字节
1024ZB = 1YB = 2^80字节
- 位(bit):信息的基本单位(在内存中由0和1表示)。
- 字节(byte):内存中最小的寻址单元(通常表示8位)。
- 容量(capacity):内存的大小(常见单位:字节B、千字节KB、兆字节MB、吉字节GB太字节TB等)。
- 地址(address):每个内存单元的唯一标识(计算机通过地址来读取和写入内存中的数据)。
- 字组(word):多个字节组成的内存存储单元(字组大小根据计算机体系而异)。
- 内存地址空间(Memory Address Space):计算机系统中可用的存储数据和程序的地址范围(地址空间大小取决于系统架构。32位系统为2^32=4GB,64位系统为2^64)。
- 缓存(cache):位于CPU和主存之间的高速缓存,用于暂时存放CPU频繁访问的数据和指令,以提高数据的访问速度。
- 虚拟内存(Virtual Memory):将物理内存和磁盘空间相结合的一种技术。通过磁盘空间扩展可用内存的容量,通过虚拟内存计算机可以运行比实际内存更大的程序。当然,虚拟内存依赖于磁盘等较慢的存储介质,所以访问速度比物理内存慢。
- 内存分页(Memory Paging):虚拟内存管理技术中的一种方式。将物理内存和虚拟地址空间划分为固定大小的页面(page),操作系统按需要将页面加载到内存或写入磁盘
- 内存分段(Memory Segmentation):虚拟内存管理技术中的一种方式。将地址空间划分为不同大小的段(Segment),每个段用于存放特定类型的数据或程序。
层次分布如下图:
寄存器(Registers):寄存器是位于CPU内部的最快速的存储设备,用于暂时存储指令、数据和地址等。寄存器的容量较小,但非常接近CPU,因此访问速度非常快。
高速缓存(Cache):缓存是位于CPU和主存之间的高速存储器。它用于存储最近被访问的数据和指令,以提高CPU对数据的访问速度。缓存被分为多级(L1、L2、L3等),层次越高,容量越大,但访问速度相对下降。
内存(Main Memory):内存是计算机系统中存储程序指令和数据的主要设备。内存容量较大,但相对于寄存器和缓存,访问速度较慢。
外存(Secondary Storage):包括硬盘驱动器、固态硬盘(SSD)、光盘等。它们用于长期存储数据和程序,具有较大的容量,但访问速度更慢。
内存地址空间(Memory Address Space)是计算机体系结构中用于存储和访问数据的地址范围。它定义了计算机体系结构所支持的内存地址范围和访问能力,每个计算机体系结构都有自己的内存地址空间大小和寻址方式。地址空间被划分为不同的部分,用于存储不同类型的数据和程序,常见的:
- 栈(stack):栈是一种数据结构,用于存储函数的局部变量、参数、返回地址等。栈的操作是先进先出(LIFO),通常位于地址空间的高地址部分。
- 堆(heap):堆是用于动态分配内存的区域,用于存储复杂数据结构、动态创建对象和数据。堆的容量通常比栈大,位于地址空间的低地址部分。
- 代码区(Code Segment):代码区是存储可执行程序的指令的部分,也称文本段,用于存储程序的指令。位于地址空间的高地址部分。
- 数据区(Data Segment):数据区包含程序的静态变量和全局变量,通常分为已初始化和未初始化的数据。
- 常量区(Constant Segment):常量区用于存储常量的只读数据(例如:字符串文字、全局常量)。
静态分配
静态分配是指在编译或加载时,有编译器或链接器为程序分配固定大小的内存空间。这些内存包括:全局变量、静态变量和编译时分配的数组等。静态分配的内存空间在程序的整个生命周期中保持不变,程序运行前就已经分配完成。
动态分配
动态分配是指程序运行时根据需要向程序分配和释放内存空间。动态分配通过动态内存管理机制实现,主要包括以下几种方式:
堆(Heap)分配:堆是动态分配内存最常用的方式,程序可以在运行时请求一定大小的内存块,并在不需要时释放。在堆分配中,通常使用函数如malloc()、free()(C语言)或new、delete(C++语言)来进行内存分配和释放。
栈(Stack)分配:栈分配是一种自动分配和释放内存的方式,由编译器自动管理。在函数调用时,函数的参数、局部变量等在栈上分配内存,当函数执行完毕后,内存会被自动释放。栈分配速度快,但内存空间有限。
池(Pool)分配:池分配是预先分配一块固定大小的内存池,然后根据需要从池中分配内存空间。池可以是固定大小的块,也可以是固定个数的对象。池分配可以提高内存分配和释放的效率,但需要事先确定池的大小。
引用计数(Reference Counting):引用计数是一种轻量级的内存管理技术,通过记录每个对象的引用计数器,当计数器为零时,自动释放内存空间。引用计数需要精确地进行引用计数更新,以避免内存泄漏和野指针问题。
垃圾回收(Garbage Collection):垃圾回收是一种自动内存管理技术,通过追踪和回收不再使用的内存空间。垃圾回收器会定期或按需扫描内存中的对象,识别出无法被访问到的对象,并自动回收它们所占用的内存。垃圾回收机制对程序员透明,但可能会引入一定的运行时开销。
在动态分配中,合理管理分配和释放内存的操作非常重要,以避免内存泄漏、内存溢出和野指针等问题。在使用动态分配时需要负责管理内存的分配和释放,确保内存资源的有效使用和回收。
虚拟内存的工作原理
地址映射(Address Mapping):操作系统将程序使用的虚拟地址(Virtual Address)转换为物理地址(Physical Address)。地址映射通常通过页表(Page Table)来实现,将虚拟地址划分成固定大小的内存页面(Page),并将每个页面映射到物理内存或存储设备上的相应位置。
页面换入/换出(Page In/Out):当程序访问一个尚未加载到内存的页面时,操作系统将该页面从磁盘加载到物理内存中,称为页面换入。如果内存空间不足,操作系统可能需要将一些不常访问的页面换出到磁盘上,以腾出空间给新的页面加载,称为页面换出。
页面访问权限(Page Access Permissions):操作系统可以设置页面的读、写和执行权限,以保护页面的安全性。对于只读数据或代码,可以将页面设置为只读权限,防止程序对其进行修改。
虚拟内存的主要优点和功能
多程序共享内存:虚拟内存允许多个程序同时运行,并使用各自独立的虚拟地址空间,从而实现了内存的共享和隔离。每个程序的地址空间相互独立,不会互相干扰。
资源抽象与扩展:虚拟内存将磁盘空间作为扩展内存的备用空间,当物理内存不足时,可以将部分数据保存在磁盘上,并根据需要进行页面换入换出,实现有效的内存管理与扩展。
内存隔离与安全性:虚拟内存可以将操作系统与每个程序的内存区域隔离开来,以确保程序之间的安全性和稳定性。即使一个程序产生错误或崩溃,不会影响到其他程序和操作系统本身。
页面共享:虚拟内存可以将多个程序所使用的相同数据页面映射到相同的物理内存页面上,实现页面的共享,以减少内存占用和加快数据访问速度。
常见算法
首次适应算法(First Fit):首次适应算法从内存的起始位置开始查找,找到第一个能够容纳所需内存块大小的空闲区域进行分配。这种算法简单直观,但可能会导致较大的内存碎片。
最佳适应算法(Best Fit):最佳适应算法在所有空闲区域中找到最小的可以满足所需内存块大小的空闲区域进行分配。这种算法可以最小化内存碎片,但需要遍历整个空闲列表,效率较低。
最坏适应算法(Worst Fit):最坏适应算法选择最大的空闲区域来分配所需内存块。这种算法可以避免产生太多的小碎片,但可能导致较大的外部碎片。
快速适应算法(Quick Fit):快速适应算法使用多个链表来管理不同大小的空闲内存块,以提高分配和回收的效率。每个链表对应一定大小范围的内存块,根据所需大小选择对应的链表进行分配。
分区算法(Partitioning):分区算法将内存分为固定大小的区域,每个区域可以分配给一个程序。分区算法可以使用等大小的分区(固定分区)或动态分区,根据程序的内存需求动态划分区域大小。
页面置换算法(Page Replacement):页面置换算法在虚拟内存中用于选择将哪些页面置换到磁盘上,以腾出内存空间给新的页面加载。常见的页面置换算法包括最近最少使用(LRU)、先进先出(FIFO)、时钟(Clock)等。
垃圾回收算法(Garbage Collection):垃圾回收算法用于自动检测和回收不再使用的内存对象。常见的垃圾回收算法包括标记清除(Mark and Sweep)、复制收集(Copying)和标记整理(Mark and Compact)等。
内存读原则
- 确保读取的数据不会越界访问已分配的内存空间,防止发生内存访问异常;
- 确保读取的数据是已经初始化或有效的,避免读取未初始化的内存或无效的数据;
- 在多线程或多进程环境中,需要进行适当的同步和互斥操作,以防止竞态条件和数据一致性问题。
内存写原则
- 确保写入的数据不会越界访问已分配的内存空间,防止发生缓冲区溢出等问题;
- 避免写入不可变的数据,即尽量避免对常量或只读数据进行写操作;
- 确保写入的数据是正确、完整和合法的,以避免写入无效、不一致或不符合预期的数据。
内存管理原则
- 分配合适的内存空间,既不超过需求,也不过度浪费,避免内存浪费和过多的系统调用;
- 及时释放不再使用的内存,防止内存泄漏,避免系统资源的浪费;
- 避免频繁的内存分配和释放操作,可以通过对象池、缓存或内存汇总等技术来减少内存管理的开销;
- 对于较大的数据结构或资源,可以使用延迟加载或按需分配的方式进行内存管理,以提高效率和节省内存空间。
减少内存分配次数
- 避免频繁的小内存分配和释放操作,可以考虑使用对象池、缓存或内存汇总等技术,复用已分配的内存,减少内存分配的次数。
- 尽量减少临时变量的使用,特别是在循环中,避免在每次迭代时都进行内存分配和释放,可以提前分配好足够的内存空间。
合理使用数据结构和算法
- 使用合适的数据结构和算法,可以减少内存占用。例如,对于大量的键值对数据,可以选择使用字典树(Trie)或哈希表来存储,而不是使用列表或数组。
- 避免不必要的数据复制,例如,使用引用类型(如指针或引用)来避免大规模数据的复制。
及时释放不再使用的内存
- 避免内存泄漏,及时释放不再使用的内存资源。确保及时调用释放资源的相关方法(如free()函数或delete操作符)以释放已分配的内存。
- 对于长时间运行的程序,可以周期性地检查内存的使用情况,发现并修复内存泄漏问题。
压缩内存占用
- 对于需要存储大量相似数据的场景,可以考虑使用压缩算法来减少内存占用。例如,使用位图(Bitmap)或压缩编码(如变长编码)来存储较大的数据集合。
使用内存管理工具和性能分析工具
- 使用内存管理工具(如内存分析器)来检测和分析内存泄漏,了解程序的内存使用情况,并找出潜在的内存优化点。
- 使用性能分析工具(如性能分析器)来识别内存使用较高的部分,优化性能瓶颈和减少内存开销。
适当使用虚拟内存机制
- 对于内存占用较大的程序,可以充分利用虚拟内存机制,将部分内存数据存储在磁盘上,以减少实际物理内存的占用。
- 但要注意虚拟内存带来的磁盘读写开销和访问延迟,因此应权衡利弊,避免过度依赖虚拟内存。
使用 free -h 简单查看内存当前使用情况
Mem :物理内存
Swap:虚拟内存
total :总内存大小
used :已使用的内存大小
free :剩余空闲内存大小
shared:共享使用内存大小
buff/cache:已使用的缓存大小
available :实际可用的内存(空闲内存 + 缓存 + 缓冲区)
buffers(缓冲区): 被系统分配用于缓冲I/O操作的内存的大小
cached(缓存) : 被系统分配用于缓存文件系统数据的内存的大小,包括文件缓存和页缓存
使用 top 动态的查看内存使用情况
total :总内存大小
used :已使用的内存大小
free :剩余空闲内存大小
shared:共享使用内存大小
buff/cache:已使用的缓存大小
avail Mem :可用的物理内存大小(不包括虚拟内存)
VIRT(虚拟内存):进程当前使用的虚拟内存大小,即进程所需的虚拟地址空间的总大小。它包括进程使用的物理内存、共享库、映射文件等。
RES(常驻内存):实际驻留在物理内存中的内存量。
SHR(共享内存):进程当前使用的共享内存大小,即进程所使用的可与其他进程共享的内存大小。共享内存通常属于共享库或映射文件,多个进程可以共享同一块内存区域。
%MEM(内存占用百分比):进程当前使用的物理内存占系统总内存的百分比(RES / total)。
使用 sar -r 动态监控内存使用情况
kbmemfree:空闲物理内存(单位KB)
kbmemused:正在使用的物理内存(单位KB)
%memused :物理内存使用率
kbbuffers:缓冲区正在使用的内存(单位KB)
kbcached :缓存的文件大小(单位KB)
kbcommit :保证当前系统所需内存,为了确保不溢出而需要的内存(RAM + SWAP)
%commit :实际可用内存占比
使用 sar -B 动态监控内存分页
pgpgin/s :每秒从磁盘或swap置换到内存的字节数(单位 KB)
pgpgout/s:每秒从内存置换到磁盘或swap的字节数(单位 KB)
fault/s :系统每秒产生的缺页数(major + minor)
majflt/s :每秒产生的主缺页数
pgfree/s :每秒被放入空闲队列的页个数
pgscank/s:每秒被kswapd扫描的页个数
pgscand/s:每秒被直接扫描的页个数
pgsteal/s:每秒从cache中被清除来满足内存需要的页个数
%vmeff :清除页占总扫描页的百分比
1. Mem 中 free 字段
当 free 字段显示的内存不足时,表示可用的空闲内存快用完了。如果缓存(buff/cache)较多,系统会将已缓存的部分数据释放,将内存分配给申请资源的进程。当然了,这种操作也会带来相应的开销 (例如:CPU开销、I/O开销、响应时间延迟)。这些开销通常是短暂的,在大多数情况下不会对系统性能产生明显影响。但是在测试性能时,尽量先清理多余的缓存。
2. Mem 中 available 字段
当 available 字段接近于 total 字段时,表示系统中的大部分内存都已经被使用,可供分配的空闲内存较少。这种情况将会导致以下问题:
性能下降:系统中的内存不足可能导致频繁的页面交换(swap),加重对磁盘I/O的依赖,从而导致显著的性能下降。
进程受限:由于可供分配的内存较少,系统可能无法同时运行更多的进程或启动新的任务。
常见的解决方法
优化内存使用:审查当前正在运行的进程,查找可能的内存泄漏、内存占用较高的进程,优化其内存使用方式,释放未使用的内存。
增加物理内存:如果可能,增加系统的物理内存容量,以提供更多的可用内存供系统使用。
调整内存管理参数:根据系统的需求和特定的工作负载,可以调整内核参数来优化内存管理策略,例如修改swappiness值、虚拟内存参数等。
减少负载:尝试减少并发运行的进程数量,优化系统资源的利用,以减轻对内存的需求。
3. Swap 中 free 字段
如果 Swap 中的 free 字段频繁减少,这可能意味着:
物理内存不足:系统的物理内存容量可能不足以满足所有进程的需求,导致频繁地将数据页转移到交换空间中。
高内存压力:系统当前的工作负载可能较大,导致系统对内存的需求超过了物理内存的限制。这可能是由于运行大型应用程序、多个并发任务、内存泄漏或内存使用不合理等原因导致的。
使用交换空间来扩展可用内存会引入额外的开销和性能影响,包括:
IO负载:交换空间的访问速度相对较慢,因此频繁的页面交换操作可能会增加磁盘IO负载,导致延迟增加。
性能下降:在交换空间中存储的数据页需要在需要时重新加载到物理内存中。这会引起较高的访问延迟,从而导致系统性能下降。
系统响应变慢:当系统对内存的需求超过了可用的物理内存时,操作系统可能会进行频繁的页面调度和交换,导致系统响应变慢,并可能导致应用程序和进程的假死或崩溃。
内存泄漏是指在程序中动态分配的内存空间在不再需要时未被释放的情况,导致内存资源无法再被其他程序使用。
内存泄漏的表现
内存占用持续增加:内存泄漏通常导致程序的内存占用量持续增长,即使在程序逻辑上不再需要这些内存空间。这会导致系统的可用内存逐渐减少,可能导致内存不足的问题。
系统性能下降:随着内存占用的增加,系统可能出现性能下降的情况。因为内存泄漏会导致系统频繁进行页面交换(swapping)以及过多的内存碎片(fragmentation),从而增加了磁盘I/O操作次数,导致系统响应变慢、运行缓慢或出现延迟。
缓慢的程序执行:内存泄漏可能导致程序执行速度变慢,因为内存占用过高会导致频繁的页面调度和交换,以及增加了内存访问延迟。
进程崩溃或异常终止:如果内存泄漏导致大量内存资源被耗尽,操作系统可能无法分配足够的内存给进程,进而导致进程崩溃或异常终止。这是一种极端情况,通常发生于长时间运行、内存泄漏严重的程序或者资源消耗巨大的场景。
在启动程序时,我们可以通过 top 命令去做简单的判断
- VIRT 表示程序的虚拟空间映射大小,一般就是程序可用的大小
- RES表示当前进程实际使用的内存大小
我们可以通过这两个信息简单的判断:当程序在执行某个业务时可能需要大量的内存,那么执行时RES会持续增加。当然了,如果我们设定的内存大小不超过 n 的话,那么使用到 n 后将会停止增加;如果我们并没有设定的话,仍由业务继续使用,则会超过 n。判断的办法:
- 设定大小不超过 n,实际使用 >n ,则可能内存有问题.
- 设定大小可以超过 n,业务运行时也超过 n,业务运行结束后自动释放且小于 n,则非问题。