-
2019-10-27 13:39:19
一、定义
内存在计算机中的作用很大,电脑中所有运行的程序都需要经过内存来执行,如果执行的程序很大或很多,就会导致内存消耗殆尽。为了解决这个问题,WINDOWS运用了虚拟内存技术,即拿出一部分硬盘空间来充当内存使用,这部分空间即称为虚拟内存,虚拟内存在硬盘上的存在形式就是 PAGEFILE.SYS这个页面文件。
【百度百科】虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。目前,大多数操作系统都使用了虚拟内存,如Windows家族的“虚拟内存”;Linux的“交换空间”等。
二、虚拟内存的工作方式
虚拟存储器是由硬件和操作系统自动实现存储信息调度和管理的。它的工作过程包括6个步骤: [3]
- 中央处理器访问主存的逻辑地址分解成组号a和组内地址b,并对组号a进行地址变换,即将逻辑组号a作为索引,查地址变换表,以确定该组信息是否存放在主存内。
- 如该组号已在主存内,则转而执行④;如果该组号不在主存内,则检查主存中是否有空闲区,如果没有,便将某个暂时不用的组调出送往辅存,以便将这组信息调入主存。
- 从辅存读出所要的组,并送到主存空闲区,然后将那个空闲的物理组号a和逻辑组号a登录在地址变换表中。
- 从地址变换表读出与逻辑组号a对应的物理组号a。
- 从物理组号a和组内字节地址b得到物理地址。
- 根据物理地址从主存中存取必要的信息。
三、虚拟内存的调度方式
调度方式有分页式、段式、段页式3种。页式调度是将逻辑和物理地址空间都分成固定大小的页。主存按页顺序编号,而每个独立编址的程序空间有自己的页号顺序,通过调度辅存中程序的各页可以离散装入主存中不同的页面位置,并可据表一一对应检索。页式调度的优点是页内零头小,页表对程序员来说是透明的,地址变换快,调入操作简单;缺点是各页不是程序的独立模块,不便于实现程序和数据的保护。段式调度是按程序的逻辑结构划分地址空间,段的长度是随意的,并且允许伸长,它的优点是消除了内存零头,易于实现存储保护,便于程序动态装配;缺点是调入操作复杂。将这两种方法结合起来便构成段页式调度。在段页式调度中把物理空间分成页,程序按模块分段,每个段再分成与物理空间页同样小的页面。段页式调度综合了段式和页式的优点。其缺点是增加了硬件成本,软件也较复杂。大型通用计算机系统多数采用段页式调度。
1.页式调度
在页式虚拟存储系统中,虚拟空间被分成大小相等的页,称为逻辑页或虚页。主存空间也被分成同样大小的页,称为物理页或实页。相应地,虚拟地址分为两个字段:高位字段为虚页号,低位字段为页内地址。实存地址也分为两个字段:高位字段为实页号,低位字段为页内地址。同时,页的大小都取2的整数幂个字。
通过页表可以把虚拟地址转换成物理地址。每个程序设置一张页表,在页表中,对应每一个虚页号都有一个条目,条目内容至少包含该虚页所在的主存页面地址(实页号),用它作为实存地址的高位字段;实页号与虚拟地址的页内地址相拼接,就产生完整的实存地址,据此访问主存。
2.段式调度
页面是主存物理空间中划分出来的等长的固定区域。分页方式的优点是页长固定,因而便于构造页表、易于管理,且不存在外碎片。但分页方式的缺点是页长与程序的逻辑大小不相关。例如,某个时刻一个子程序可能有一部分在主存中,另一部分则在辅存中。这不利于编程时的独立性,并给换入/换出处理、存储保护和存储共享等操作造成麻烦。
另一种划分可寻址的存储空间的方法称为分段。段是按照程序的自然分界划分的、长度可以动态改变的区域。通常,程序员把子程序、操作数和常数等不同类型的数据划分到不同的段中,并且每个程序可以有多个相同类型的段。
在段式虚拟存储系统中,虚拟地址由段号和段内地址组成,虚拟地址到实存地址的变换通过段表来实现。每个程序设置一个段表,段表的每一个表项对应一个段,每个表项至少包括三个字段:有效位(指明该段是否已经调入主存)、段起址(该段在实存中的首地址)和段长(记录该段的实际长度)。
3.段页式调度
段页式虚拟存储器是段式虚拟存储器和页式虚拟存储器的结合。
首先,实存被等分成页。在段页式虚拟存储器中,把程序按逻辑结构分段以后,再把每段按照实存的页的大小分页,程序按页进行调入和调出操作,但它又可按段实现共享和保护。因此,它可以兼有页式和段式系统的优点。它的缺点是在地址映像过程中需要多次查表,虚拟地址转换成物理地址是通过一个段表和一组页表来进行定位的。段表中的每个表目对应一个段,每个表目有一个指向该段的页表的起始地址(页号)及该段的控制保护信页表指明该段各页在主存中的位置以及是否已装入、已修改等标志。
四、虚拟内存的调度方式
虚拟存储器地址变换基本上有3种形虚拟存储器工作过程式:全联想变换、直接变换和组联想变换。任何逻辑空间页面能够变换到物理空间任何页面位置的方式称为全联想变换。每个逻辑空间页面只能变换到物理空间一个特定页面的方式称为直接变换。组联想变换是指各组之间是直接变换,而组内各页间则是全联想变换。替换规则用来确定替换主存中哪一部分,以便腾空部分主存,存放来自辅存要调入的那部分内容。常见的替换算法有4种。
- 随机算法:用软件或硬件随机数产生器确定替换的页面。
- 先进先出:先调入主存的页面先替换。
- 近期最少使用算法(LRU,Least Recently Used):替换最长时间不用的页面。
- 最优算法:替换最长时间以后才使用的页面。这是理想化的算法,只能作为衡量其他各种算法优劣的标准。
虚拟存储器的效率是系统性能评价的重要内容,它与主存容量、页面大小、命中率,程序局部性和替换算法等因素有关。
五、虚拟内存的作用
虚拟内存提供了三个重要的能力: 缓存,内存管理,内存保护
- 将主存视为一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据
- 为每个进程提供了一致的地址空间,简化内存管理
- 保护了每个进程的地址空间不被其他进程破坏
六、总结
- 调度问题:决定哪些程序和数据应被调入主存。
- 地址映射问题:在访问主存时把虚地址变为主存物理地址(这一过程称为内地址变换);在访问辅存时把虚地址变成辅存的物理地址(这一过程称为外地址变换),以便换页。此外还要解决主存分配、存储保护与程序再定位等问题。
- 替换问题:决定哪些程序和数据应被调出主存。
- 更新问题:确保主存与辅存的一致性。
在操作系统的控制下,硬件和系统软件为用户解决了上述问题,从而使应用程序的编程大大简化。
我的微信公众号:架构真经(id:gentoo666),分享Java干货,高并发编程,热门技术教程,微服务及分布式技术,架构设计,区块链技术,人工智能,大数据,Java面试题,以及前沿热门资讯等。每日更新哦!
参考文章
更多相关内容 -
内存管理:程序是如何被优雅的装载到内存中的
2021-11-04 09:26:35我们知道我们写的代码最终是要从磁盘被加载到内存中的,然后再被CPU执行,不知道你有没有想过,为什么一些大型游戏大到10几G,却可以在只有8G内存的电脑上运行?甚至在玩游戏期间,我们还可以聊微信、听音乐...,...内存作为计算机中一项比较重要的资源,它的主要作用就是解决CPU和磁盘之间速度的鸿沟,但是由于内存条是需要插入到主板上的,因此对于一台计算机来说,由于物理限制,它的内存不可能无限大的。我们知道我们写的代码最终是要从磁盘被加载到内存中的,然后再被CPU执行,不知道你有没有想过,为什么一些大型游戏大到10几G,却可以在只有8G内存的电脑上运行?甚至在玩游戏期间,我们还可以聊微信、听音乐...,这么多进程看着同时在运行,它们在内存中是如何被管理的?带着这些疑问我们来看看计算系统内存管理那些事。
内存的交换技术
如果我们的内存可以无限大,那么我们担忧的问题就不会存在,但是实际情况是往往我们的机器上会同时运行多个进程,这些进程小到需要几十兆内存,大到可能需要上百兆内存,当许许多多这些进程想要同时加载到内存的时候是不可能的,但是从我们用户的角度来看,似乎这些进程确实都在运行呀,这是怎么回事?
这就引入要说的交换技术了,从字面的意思来看,我想你应该猜到了,它会把某个内存中的进程交换出去。当我们的进程空闲的时候,其他的进程又需要被运行,然而很不幸,此时没有足够的内存空间了,这时候怎么办呢?似乎刚刚那个空闲的进程有种占着茅坑不拉屎的感觉,于是可以把这个空闲的进程从内存中交换到磁盘上去,这时候就会空出多余的空间来让这个新的进程运行,当这个换出去的空闲进程又需要被运行的时候,那么它就会被再次交换进内存中。通过这种技术,可以让有限的内存空间运行更多的进程,进程之间不停来回交换,看着好像都可以运行。
如图所示,一开始进程A被换入内存中,所幸还剩余的内存空间比较多,然后进程B也被换入内存中,但是剩余的空间比较少了,这时候进程C想要被换入到内存中,但是发现空间不够了,这时候会把已经运行一段时间的进程A换到磁盘中去,然后调入进程C。
内存碎片
通过这种交换技术,交替的换入和换出进程可以达到小内存可以运行更多的进程,但是这似乎也产生了一些问题,不知道你发现了没有,在进程C换入进来之后,在进程B和进程C之间有段较小的内存空间,并且进程B之上也有段较小的内存空间,说实话,这些小空间可能永远没法装载对应大小的程序,那么它们就浪费了,在某些情况下,可能会产生更多这种内存碎片。
如果想要节约内存,那么就得用到内存紧凑的技术了,即把所有的进程都向下移动,这样所有的碎片就会连接在一起变成一段更大的连续内存空间了。
但是这个移动的开销基本和当前内存中的活跃进程成正比,据统计,一台16G内存的计算机可以每8ns复制8个字节,它紧凑全部的内存大概需要16s,所以通常不会进行紧凑这个操作,因为它耗费的CPU时间还是比较大的。
动态增长
其实上面说的进程装载算是比较理想的了,正常来说,一个进程被创建或者被换入的时候,它占用多大的空间就分配多大的内存,但是如果我们的进程需要的空间是动态增长的,那就麻烦了,比如我们的程序在运行期间的for循环可能会利用到某个临时变量来存放目标数据(例如以下变量a,随着程序的运行是会越来越大的):
var a []int64 for i:= 0;i <= 1000000;i++{ if i%2 == 0{ a = append(a,i) //a是不断增大的 } }
当需要增长的时候:
- 如果进程的邻居是空闲区那还好,可以把该空闲区分配给进程
- 如果进程的邻居是另一个进程,那么解决的办法只能把增长的进程移动到一个更大的空闲内存中,但是万一没有更大的内存空间,那么就要触发换出,把一个或者多个进程换出去来提供更多的内存空间,很明显这个开销不小。
为了解决进程空间动态增长的问题,我们可以提前多给一些空间,比如进程本身需要10M,我们多给2M,这样如果进程发生增长的时候,可以利用这2M空间,当然前提是这2M空间够用,如果不够用还是得触发同样的移动、换出逻辑。
空闲的内存如何管理
前面我们说到内存的交换技术,交换技术的目的是腾出空闲内存来,那么我们是如何知道一块内存是被使用了,还是空闲的?因此需要一套机制来区分出空闲内存和已使用内存,一般操作系统对内存管理的方式有两种:位图法和链表法。
位图法
先说位图法,没错,位图法采用比特位的方式来管理我们的内存,每块内存都有位置,我们用一个比特位来表示:
- 如果某块内存被使用了,那么比特位为1
- 如果某块内存是空闲的,那么比特位为0
这里的某块内存具体是多大得看操作系统是如何管理的,它可能是一个字节、几个字节甚至几千个字节,但是这些不是重点,重点是我们要知道内存被这样分割了。
位图法的优点就是清晰明确,某个内存块的状态可以通过位图快速的知道,因为它的时间复杂度是O(1),当然它的缺点也很明显,就是需要占用太多的空间,尤其是管理的内存块越小的时候。更糟糕的是,进程分配的空间不一定是内存块的整数倍,那么最后一个内存块中一定是有浪费的。
如图,进程A和进程B都占用的最后一个内存块的一部分,那么对于最后一个内存块,它的另一部分一定是浪费的。
链表法
相比位图法,链表法对空间的利用更加合理,我相信你应该已经猜到了,链表法简单理解就是把使用的和空闲的内存用链表的方式连接起来,那么对于每个链表的元素节点来说,他应该具备以下特点:
- 应该知道每个节点是空闲的还是被使用的
- 每个节点都应该知道当前节点的内存的开始地址和结束地址
针对这些特点,最终内存对应的链表节点大概是这样的:
p代表这个节点对应的内存空间是被使用的,H代表这个节点对应的内存空间是空闲的,start代表这块内存空间的开始地址,length代表的是这块内存的长度,最后还有指向邻居节点的pre和next指针。
因此对于一个进程来说,它与邻居的组合有四种:
- 它的前后节点都不是空闲的
- 它的前一个节点是空闲的,它的后一个节点也不是空闲的
- 它的前一个节点不是空闲的,它的后一个节点是空闲的
- 它的前后节点都是空闲的
当一个内存节点被换出或者说进程结束后,那么它对应的内存就是空闲的,此时如果它的邻居也是空闲的,就会发生合并,即两块空闲的内存块合并成一个大的空闲内存块。
ok,通过链表的方式把我们的内存给管理起来了,接下来就是当创建一个进程或者从磁盘换入一个进程的时候,如何从链表中找到一块合适的内存空间?
首次适应算法
其实想要找到空闲内存空间最简单的办法就是顺着链表找到第一个满足需要内存大小的节点,如果找到的第一个空闲内存块和我们需要的内存空间是一样大小的,那么就直接利用,但是这太理想了,现实情况大部分可能是找到的第一个目标内存块要比我们的需要的内存空间要大一些,这时候呢,会把这个空闲内存空间分成两块,一块正好使用,一块继续充当空闲内存块。
一个需要3M内存的进程,会把4M的空间拆分成3M和1M。
下次适配算法
和首次适应算法很相似,在找到目标内存块后,会记录下位置,这样下次需要再次查找内存块的时候,会从这个位置开始找,而不用从链表的头节点开始寻找,这个算法存在的问题就是,如果标记的位置之前有合适的内存块,那么就会被跳过。
一个需要2M内存的进程,在5这个位置找到了合适的空间,下次如果需要这1M的内存会从5这个位置开始,然后会在7这个位置找到合适的空间,但是会跳过1这个位置。
最佳适配算法
相比首次适应算法,最佳适配算法的区别就是:不是找到第一个合适的内存块就停止,而是会继续向后找,并且每次都可能要检索到链表的尾部,因为它要找到最合适那个内存块,什么是最合适的内存块呢?如果刚好大小一致,则一定是最合适的,如果没有大小一致的,那么能容得下进程的那个最小的内存块就是最合适的,可以看出最佳适配算法的平均检索时间相对是要慢的,同时可能会造成很多小的碎片。
假设现在进程需要2M的内存,那么最佳适配算法会在检索到3号位置(3M)后,继续向后检索,最终会选择5号位置的空闲内存块。
最差适配算法
我们知道最佳适配算法中最佳的意思是找到一个最贴近真实大小的空闲内存块,但是这会造成很多细小的碎片,这些细小的碎片一般情况下,如果没有进行内存紧凑,那么大概率是浪费的,为了避免这种情况,就出现了这个最差适配算法,这个算法它和最佳适配算法是反着来的,它每次尝试分配最大的可用空闲区,因为这样的话,理论上剩余的空闲区也是比较大的,内存碎片不会那么小,还能得到重复利用。
一个需要1.5M的进程,在最差适配算法情况下,不会选择3号(2M)内存空闲块,而是会选择更大的5号(3M)内存空闲块。
快速适配算法
上面的几种算法都有一个共同的特点:空闲内存块和已使用内存块是共用的一个链表,这会有什么问题呢?正常来说,我要查找一个空闲块,我并不需要检索已经被使用的内存块,所以如果能把已使用的和未使用的分开,然后用两个链表分别维护,那么上面的算法无论哪种,速度都将得到提升,并且节点也不需要P和M来标记状态了。但是分开也有缺点,如果进程终止或者被换出,那么对应的内存块需要从已使用的链表中删掉然后加入到未使用的链表中,这个开销是要稍微大点的。当然对于未使用的链表如果是排序的,那么首次适应算法和最佳适应算法是一样快的。
快速适配算法就是利用了这个特点,这个算法会为那些常用大小的空闲块维护单独的链表,比如有4K的空闲链表、8K的空闲链表...,如果要分配一个7K的内存空间,那么既可以选择两个4K的,也可以选择一个8K的。
它的优点很明显,在查找一个指定大小的空闲区会很快速,但是一个进程终止或被换出时,会寻找它的相邻块查看是否可以合并,这个过程相对较慢,如果不合并的话,那么同样也会产生很多的小空闲区,它们可能无法被利用,造成浪费。
虚拟内存:小内存运行大程序
可能你看到小内存运行大程序比较诧异,因为上面不是说到了吗?只要把空闲的进程换出去,把需要运行的进程再换进来不就行了吗?内存交换技术似乎解决了,这里需要注意的是,首先内存交换技术在空间不够的情况下需要把进程换出到磁盘上,然后从磁盘上换入新进程,看到磁盘你可能明白了,很慢。其次,你发现没,换入换出的是整个进程,我们知道进程也是由一块一块代码组成的,也就是许许多多的机器指令,对于内存交换技术来说,一个进程下的所有指令要么全部进内存,要么全部不进内存。看到这里你可能觉得这不是正常吗?好的,别急,我们接着往下看。
后来出现了更牛逼的技术:虚拟内存。它的基本思想就是,每个程序拥有自己的地址空间,尤其注意后面的自己的地址空间,然后这个空间可以被分割成多个块,每一个块我们称之为页(page)或者叫页面,对于这些页来说,它们的地址是连续的,同时它们的地址是虚拟的,并不是真正的物理内存地址,那怎么办?程序运行需要读到真正的物理内存地址,别跟我玩虚的,这就需要一套映射机制,然后MMU出现了,MMU全称叫做:Memory Managment Unit,即内存管理单元,正常来说,CPU读某个内存地址数据的时候,会把对应的地址发到内存总线上,但是在虚拟内存的情况下,直接发到内存总线上肯定是找不到对应的内存地址的,这时候CPU会把虚拟地址告诉MMU,让MMU帮我们找到对应的内存地址,没错,MMU就是一个地址转换的中转站。
程序地址分页的好处是:
- 对于程序来说,不需要像内存交换那样把所有的指令都加载到内存中才能运行,可以单独运行某一页的指令
- 当进程的某一页不在内存中的时候,CPU会在这个页加载到内存的过程中去执行其他的进程。
当然虚拟内存会分页,那么对应的物理内存其实也会分页,只不过物理内存对应的单元我们叫页框。页面和页框通常是一样大的。我们来看个例子,假设此时页面和页框的大小都是4K,那么对于64K的虚拟地址空间可以得到64/4=16个虚拟页面,而对于32K的物理地址空间可以得到32/4=8个页框,很明显此时的页框是不够的,总有些虚拟页面找不到对应的页框。
我们先来看看虚拟地址为20500对应物理地址如何被找到的:
- 首先虚拟地址20500对应5号页面(20480-24575)
- 5号页面的起始地址20480向后查找20个字节,就是虚拟地址的位置
- 5号页面对应3号物理页框
- 3号物理页框的起始地址是12288,12288+20=12308,即12308就是我们实际的目标物理地址。
但是对于虚拟地址而言,图中还有红色的区域,上面我们也说到了,总有些虚拟地址没有对应的页框,也就是这部分虚拟地址是没有对应的物理地址,当程序访问到一个未被映射的虚拟地址(红色区域)的时候,那么就会发生缺页中断,然后操作系统会找到一个最近很少使用的页框把它的内容换到磁盘上去,再把刚刚发生缺页中断的页面从磁盘读到刚刚回收的页框中去,最后修改虚拟地址到页框的映射,然后重启引起中断的指令。
最后可以发现分页机制使我们的程序更加细腻了,运行的粒度是页而不是整个进程,大大提高了效率。
页表
上面说到虚拟内存到物理内存有个映射,这个映射我们知道是MMU做的,但是它是如何实现的?最简单的办法就是需要有一张类似hash表的结构来查看,比如页面1对应的页框是10,那么就记录成
hash[1]=10
,但是这仅仅是定位到了页框,具体的位置还没定位到,也就是类似偏移量的数据没有。不猜了,我们直接来看看MMU是如何做到的,以一个16位的虚拟地址,并且页面和页框都是4K的情况来说,MMU会把前4位当作是索引,也就是定位到页框的序号,后12位作为偏移量,这里为什么是12位,很巧妙,因为2^12=4K,正好给每个页框里的数据上了个标号。因此我们只需要根据前4位找到对应的页框即可,然后偏移量就是后12位。找页框就是去我们即将要说的页表里去找,页表除了有页面对应的页框后,还有个标志位来表示对应的页面是否有映射到对应的页框,缺页中断就是根据这个标志位来的。可以看出页表非常关键,不仅仅要知道页框、以及是否缺页,其实页表还有保护位、修改位、访问位和高速缓存禁止位。
- 保护位:指的是一个页允许什么类型的访问,常见的是用三个比特位分别表示读、写、执行。
- 修改位:有时候也称为脏位,由硬件自动设置,当一个页被修改后,也就是和磁盘的数据不一致了,那么这个位就会被标记为1,下次在页框置换的时候,需要把脏页刷回磁盘,如果这个页的标记为0,说明没有被修改,那么不需要刷回磁盘,直接把数据丢弃就行了。
- 访问位:当一个页面不论是发生读还是发生写,该页面的访问位都会设置成1,表示正在被访问,它的作用就是在发生缺页中断时,根据这个标志位优先在那些没有被访问的页面中选择淘汰其中的一个或者多个页框。
- 高速缓存禁止位:对于那些映射到设备寄存器而不是常规内存的页面而言,这个特性很重要,加入操作系统正在紧张的循环等待某个IO设备对它刚发出的指令做出响应,保证这个设备读的不是被高速缓存的副本非常重要。
TLB快表加速访问
通过页表我们可以很好的实现虚拟地址到物理地址的转换,然而现代计算机至少是32位的虚拟地址,以4K为一页来说,那么对于32位的虚拟地址,它的页表项就有2^20=1048576个,无论是页表本身的大小还是检索速度,这个数字其实算是有点大了。如果是64位虚拟的地址,按照这种方式的话,页表项将大到超乎想象,更何况最重要的是每个进程都会有一个这样的页表。
我们知道如果每次都要在庞大的页表里面检索页框的话,效率一定不是很高。而且计算机的设计者们观察到这样一种现象:大多数程序总是对少量的页进行多次访问,如果能为这些经常被访问的页单独建立一个查询页表,那么速度就会大大提升,这就是快表,快表只会包含少量的页表项,通常不会超过256个,当我们要查找一个虚拟地址的时候。首先会在快表中查找,如果能找到那么就可以直接返回对应的页框,如果找不到才会去页表中查找,然后从快表中淘汰一个表项,用新找到的页替代它。
总体来说,TLB类似一个体积更小的页表缓存,它存放的都是最近被访问的页,而不是所有的页。
多级页表
TLB虽然一定程度上可以解决转换速度的问题,但是没有解决页表本身占用太大空间的问题。其实我们可以想想,大部分程序会使用到所有的页面吗?其实不会。一个进程在内存中的地址空间一般分为程序段、数据段和堆栈段,堆栈段在内存的结构上是从高地址向低地址增长的,其他两个是从低地址向高地址增长的。
可以发现中间部分是空的,也就是这部分地址是用不到的,那我们完全不需要把中间没有被使用的内存地址也引入页表呀,这就是多级页表的思想。以32位地址为例,后12位是偏移量,前20位可以拆成两个10位,我们暂且叫做顶级页表和二级页表,每10位可以表示2^10=1024个表项,因此它的结构大致如下:
对于顶级页表来说,中间灰色的部分就是没有被使用的内存空间。顶级页表就像我们身份证号前面几个数字,可以定位到我们是哪个城市或者县的,二级页表就像身份证中间的数字,可以定位到我们是哪个街道或者哪个村的,最后的偏移量就像我们的门牌号和姓名,通过这样的分段可以大大减少空间,我们来看个简单的例子:
如果我们不拆出顶级页表和二级页表,那么所需要的页表项就是2^20个,如果我们拆分,那么就是1个顶级页表+2^10个二级页表,两者的存储差距明显可以看出拆分后更加节省空间,这就是多级页表的好处。
当然我们的二级也可以拆成三级、四级甚至更多级,级数越多灵活性越大,但是级数越多,检索越慢,这一点是需要注意的。
最后
为了便于大家理解,本文画了20张图,肝了将近7000多字,创作不易,各位的三连就是对作者最大的支持,也是作者最大的创作动力。
微信搜一搜【假装懂编程】,加入我们,与作者共同学习,共同进步。
往期精彩:
-
深入理解Java虚拟机-Java内存区域与内存溢出异常
2020-01-03 21:42:24Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来。 文章目录概述运行时数据区域程序计数器(线程私有)Java虚拟机栈(线程私有)局部变量表操作数栈动态链接...本博客主要参考周志明老师的《深入理解Java虚拟机》第二版
读书是一种跟大神的交流。阅读《深入理解Java虚拟机》受益匪浅,对Java虚拟机有初步的认识。这里写博客主要出于以下三个目的:一方面是记录,方便日后阅读;一方面是加深对内容的理解;一方面是分享给大家,希望对大家有帮助。
《深入理解Java虚拟机》全书总结如下:
序号 内容 链接地址 1 深入理解Java虚拟机-走近Java https://blog.csdn.net/ThinkWon/article/details/103804387 2 深入理解Java虚拟机-Java内存区域与内存溢出异常 https://blog.csdn.net/ThinkWon/article/details/103827387 3 深入理解Java虚拟机-垃圾回收器与内存分配策略 https://blog.csdn.net/ThinkWon/article/details/103831676 4 深入理解Java虚拟机-虚拟机执行子系统 https://blog.csdn.net/ThinkWon/article/details/103835168 5 深入理解Java虚拟机-程序编译与代码优化 https://blog.csdn.net/ThinkWon/article/details/103835883 6 深入理解Java虚拟机-高效并发 https://blog.csdn.net/ThinkWon/article/details/103836167 文章目录
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来。
概述
对于从事C、C++程序开发的开发人员来说,在内存管理领域,他们既是拥有最高权力的“皇帝”又是从事最基础工作的“劳动人民”——既拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任。
对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏和内存溢出问题,由虚拟机管理内存这一切看起来都很美好。不过,也正是因为Java程序员把内存控制的权力交给了Java虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会成为一项异常艰难的工作。运行时数据区域
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
Execution engine(执行引擎):执行classes中的指令。
Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域:
程序计数器(线程私有)
程序计数器是一块较小的内存区域,可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。「属于线程私有的内存区域」
附加:
- 当前线程所执行的字节码行号指示器。
- 每个线程都有一个自己的
PC
计数器。 - 线程私有的,生命周期与线程相同,随
JVM
启动而生,JVM
关闭而死。 - 线程执行
Java
方法时,记录其正在执行的虚拟机字节码指令地址。 - 线程执行
Native
方法时,计数器记录为空(Undefined
)。 - 唯一在
Java
虚拟机规范中没有规定任何OutOfMemoryError
情况区域。
Java虚拟机栈(线程私有)
线程私有内存空间,它的生命周期和线程相同。线程执行期间,每个方法被执行时,都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法从被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。「属于线程私有的内存区域」
注意:下面的内容为附加内容,对Java虚拟机栈进行详细说明,感兴趣的小伙伴可以有针对性的阅读
下面依次解释栈帧里的四种组成元素的具体结构和功能:
局部变量表
局部变量表局部变量表是 Java 虚拟机栈的一部分,是一组变量值的存储空间,用于存储方法参数和局部变量。 在
Class
文件的方法表的Code
属性的max_locals
指定了该方法所需局部变量表的最大容量。局部变量表在编译期间分配内存空间,可以存放编译期的各种变量类型:
- 基本数据类型 :
boolean
,byte
,char
,short
,int
,float
,long
,double
等8
种; - 对象引用类型 :
reference
,指向对象起始地址的引用指针;不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置 - 返回地址类型 :
returnAddress
,返回地址的类型。指向了一条字节码指令的地址
变量槽(
Variable Slot
):变量槽是局部变量表的最小单位,规定大小为
32
位。对于64
位的long
和double
变量而言,虚拟机会为其分配两个连续的Slot
空间。操作数栈
操作数栈(
Operand Stack
)也常称为操作栈,是一个后入先出栈。在Class
文件的Code
属性的max_stacks
指定了执行过程中最大的栈深度。Java
虚拟机的解释执行引擎被称为基于栈的执行引擎 ,其中所指的栈就是指-操作数栈。- 和局部变量表一样,操作数栈也是一个以
32
字长为单位的数组。 - 虚拟机在操作数栈中可存储的数据类型:
int
、long
、float
、double
、reference
和returnType
等类型 (对于byte
、short
以及char
类型的值在压入到操作数栈之前,也会被转换为int
)。 - 和局部变量表不同的是,它不是通过索引来访问,而是通过标准的栈操作 — 压栈和出栈来访问。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。
虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。
begin iload_0 // push the int in local variable 0 onto the stack iload_1 // push the int in local variable 1 onto the stack iadd // pop two ints, add them, push result istore_2 // pop int, store into local variable 2 end
在这个字节码序列里,前两个指令
iload_0
和iload_1
将存储在局部变量表中索引为0
和1
的整数压入操作数栈中,其后iadd
指令从操作数栈中弹出那两个整数相加,再将结果压入操作数栈。第四条指令istore_2
则从操作数栈中弹出结果,并把它存储到局部变量表索引为2
的位置。下图详细表述了这个过程中局部变量表和操作数栈的状态变化(图中没有使用的局部变量表和操作数栈区域以空白表示)。
动态链接
每个栈帧都包含一个指向运行时常量池中所属的方法引用,持有这个引用是为了支持方法调用过程中的动态链接。
Class
文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用:- 静态解析:一部分会在类加载阶段或第一次使用的时候转化为直接引用(如
final
、static
域等),称为静态解析, - 动态解析:另一部分将在每一次的运行期间转化为直接引用,称为动态链接。
方法返回地址
当一个方法开始执行以后,只有两种方法可以退出当前方法:
- 正常返回:当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(
Normal Method Invocation Completion
),一般来说,调用者的PC
计数器可以作为返回地址。 - 异常返回:当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(
Abrupt Method Invocation Completion
),返回地址要通过异常处理器表来确定。
当一个方法返回时,可能依次进行以下
3
个操作:- 恢复上层方法的局部变量表和操作数栈。
- 把返回值压入调用者栈帧的操作数栈。
- 将
PC
计数器的值指向下一条方法指令位置。
小结
注意:在Java虚拟机规范中,对这个区域规定了两种异常。
其一:如果当前线程请求的栈深度大于虚拟机栈所允许的深度,将会抛出
StackOverflowError
异常(在虚拟机栈不允许动态扩展的情况下);其二:如果扩展时无法申请到足够的内存空间,就会抛出
OutOfMemoryError
异常。本地方法栈(线程私有)
本地方法栈和
Java
虚拟机栈发挥的作用非常相似,主要区别是Java
虚拟机栈执行的是Java
方法服务,而本地方法栈执行Native
方法服务(通常用C编写)。有些虚拟机发行版本(譬如
Sun HotSpot
虚拟机)直接将本地方法栈和Java
虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError
和OutOfMemoryError
异常。Java堆(全局共享)
对大多数应用而言,Java 堆是虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一作用就是存放对象实例,几乎所有的对象实例都是在这里分配的(不绝对,在虚拟机的优化策略下,也会存在栈上分配、标量替换的情况,后面的章节会详细介绍)。
Java 堆是 GC 回收的主要区域,因此很多时候也被称为 GC 堆。
从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以在Java堆被划分成两个不同的区域:新生代 (
Young Generation
) 、老年代 (Old Generation
) 。新生代 (Young
) 又被划分为三个区域:一个Eden
区和两个Survivor
区 -From Survivor
区和To Survivor
区。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然时对象实例,记你一步划分的目的是为了使JVM
能够更好的管理堆内存中的对象,包括内存的分配以及回收。简要归纳:新的对象分配是首先放在年轻代 (
Young Generation
) 的Eden
区,Survivor
区作为Eden
区和Old
区的缓冲,在Survivor
区的对象经历若干次收集仍然存活的,就会被转移到老年代Old
中。从内存回收的角度看,线程共享的 Java 堆可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。「属于线程共享的内存区域」
方法区(全局共享)
方法区和
Java
堆一样,为多个线程共享,它用于存储类信息、常量、静态常量和即时编译后的代码等数据。Non-Heap(非堆)「属于线程共享的内存区域」运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(Constant Pool Table),用于存放编译期生成的各种字面常量和符号引用,这部分内容会在类加载后进入方法区的运行时常量池。
下面信息为附加信息
- HotSpot虚拟机中,将方法区称为“永久代”,本质上两者并不等价,仅仅是因为HotSpot虚拟机把GC分代收集扩展至方法区。
- JDK 7的HotSpot中,已经将原本存放于永久代中的字符串常量池移出。
- 根据虚拟机规范的规定,当方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常。当常量池无法再申请到内存时也会抛出OutOfMemoryError异常。
- JDK 8的HotSpot中,已经将永久代废除,用元数据实现了方法区。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数。
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。Java 中的 NIO 可以使用 Native 函数直接分配堆外内存,通常直接内存的速度会优于Java堆内存,然后通过一个存储在 Java 堆中的 DiectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景显著提高性能,对于读写频繁、性能要求高的场景,可以考虑使用直接内存,因为避免了在 Java 堆和 Native 堆中来回复制数据。直接内存不受 Java 堆大小的限制。
HotSpot虚拟机对象探秘
对象的创建
说到对象的创建,首先让我们看看
Java
中提供的几种对象创建方式:Header 解释 使用new关键字 调用了构造函数 使用Class的newInstance方法 调用了构造函数 使用Constructor类的newInstance方法 调用了构造函数 使用clone方法 没有调用构造函数 使用反序列化 没有调用构造函数 下面是对象创建的主要流程:
虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载。类加载通过后,接下来分配内存。若Java堆中内存是绝对规整的,使用“指针碰撞“方式分配内存;如果不是规整的,就从空闲列表中分配,叫做”空闲列表“方式。划分内存时还需要考虑一个问题-并发,也有两种方式: CAS同步处理,或者本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。然后内存空间初始化操作,接着是做一些必要的对象设置(元信息、哈希码…),最后执行
<init>
方法。下面内容是对象创建的详细过程
对象的创建通常是通过new关键字创建一个对象的,当虚拟机接收到一个new指令时,它会做如下的操作。
1.判断对象对应的类是否加载、链接、初始化
虚拟机接收到一条new指令时,首先会去检查这个指定的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被类加载器加载、链接和初始化过。如果没有则先执行相应的类加载过程。
2.为对象分配内存
类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:
- 指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
- 空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。
选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
3.处理并发安全问题
对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:
- 对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的原子性);
- 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁。通过-XX:+/-UserTLAB参数来设定虚拟机是否使用TLAB。
4.初始化分配到的内存空间
内存分配完后,虚拟机要将分配到的内存空间初始化为零值(不包括对象头)。如果使用了 TLAB,这一步会提前到 TLAB 分配时进行。这一步保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用。
5.设置对象的对象头
接下来设置对象头(Object Header)信息,包括对象的所属类、对象的HashCode和对象的GC分代年龄等数据存储在对象的对象头中。
6.执行init方法进行初始化
执行init方法,初始化对象的成员变量、调用类的构造方法,这样一个对象就被创建了出来。
对象的内存布局
HotSpot
虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header
)、实例数据(Instance Data
)和对齐填充(Padding
)。对象头
在
HotSpot
虚拟机中,对象头有两部分信息组成:运行时数据 和 类型指针,如果是数组对象,还有一个保存数组长度的空间。-
Mark Word(运行时数据):用于存储对象自身运行时的数据,如哈希码(hashCode)、GC分带年龄、线程持有的锁、偏向线程ID 等信息。在32位系统占4字节,在64位系统中占8字节;
HotSpot虚拟机对象头Mark Word在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示:
存储内容 标志位 状态 对象哈希码、对象分代年龄 01 未锁定 指向锁记录的指针 00 轻量级锁定 指向重量级锁的指针 10 膨胀(重量级锁定) 空,不需要记录信息 11 GC标记 偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向 - Class Pointer(类型指针):用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在32位系统占4字节,在64位系统中占8字节;
- Length:如果是数组对象,还有一个保存数组长度的空间,占4个字节;
实例数据
实例数据 是对象真正存储的有效信息,无论是从父类继承下来的还是该类自身的,都需要记录下来,而这部分的存储顺序受虚拟机的分配策略和定义的顺序的影响。
默认分配策略:
long/double -> int/float -> short/char -> byte/boolean -> reference
如果设置了
-XX:FieldsAllocationStyle=0
(默认是1
),那么引用类型数据就会优先分配存储空间:reference -> long/double -> int/float -> short/char -> byte/boolean
结论:
分配策略总是按照字节大小由大到小的顺序排列,相同字节大小的放在一起。
对齐填充
无特殊含义,不是必须存在的,仅作为占位符。
HotSpot
虚拟机要求每个对象的起始地址必须是8
字节的整数倍,也就是对象的大小必须是8
字节的整数倍。而对象头部分正好是8
字节的倍数(32
位为1
倍,64
位为2
倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。对象的访问定位
Java
程序需要通过JVM
栈上的引用访问堆中的具体对象。对象的访问方式取决于JVM
虚拟机的实现。目前主流的访问方式有 句柄 和 直接指针 两种方式。指针: 指向对象,代表一个对象在内存中的起始地址。
句柄: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。
句柄访问
Java
堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息,具体构造如下图所示:
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。直接指针
如果使用直接指针访问,引用 中存储的直接就是对象地址,那么
Java
堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。
优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java
中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。实战:OutOfMemoryError异常
内存异常是我们工作当中经常会遇到问题,但如果仅仅会通过加大内存参数来解决问题显然是不够的,应该通过一定的手段定位问题,到底是因为参数问题,还是程序问题(无限创建,内存泄露)。定位问题后才能采取合适的解决方案,而不是一内存溢出就查找相关参数加大。
概念
内存泄露:代码中的某个对象本应该被虚拟机回收,但因为拥有GCRoot引用而没有被回收。
内存溢出:虚拟机由于堆中拥有太多不可回收对象没有回收,导致无法继续创建新对象。
在分析问题之前先给大家讲一讲排查内存溢出问题的方法,内存溢出时JVM虚拟机会退出,那么我们怎么知道JVM运行时的各种信息呢,Dump机制会帮助我们,可以通过加上VM参数-XX:+HeapDumpOnOutOfMemoryError让虚拟机在出现内存溢出异常时生成dump文件,然后通过外部工具(VisualVM)来具体分析异常的原因。
除了程序计数器外,
Java
虚拟机的其他运行时区域都有可能发生OutOfMemoryError
的异常,下面分别给出验证:Java堆溢出
Java堆用来存储对象,因此只要不断创建对象,并保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清楚这些对象,那么当对象数量达到最大堆容量时就会产生 OOM。
/** * java堆内存溢出测试 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError */ public class HeapOOM { static class OOMObject{} public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while (true) { list.add(new OOMObject()); } } }
运行结果:
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid7164.hprof … Heap dump file created [27880921 bytes in 0.193 secs] Exception in thread “main” java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:2245) at java.util.Arrays.copyOf(Arrays.java:2219) at java.util.ArrayList.grow(ArrayList.java:242) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208) at java.util.ArrayList.add(ArrayList.java:440) at com.jvm.oom.HeapOOM.main(HeapOOM.java:17)
堆内存 OOM 是经常会出现的问题,异常信息会进一步提示 Java heap space
虚拟机栈和本地方法栈溢出
在 HotSpot 虚拟机中不区分虚拟机栈和本地方法栈,栈容量只由 -Xss 参数设定。关于虚拟机栈和本地方法栈,在 Java 虚拟机规范中描述了两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常。
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。
/** * 虚拟机栈和本地方法栈内存溢出测试,抛出stackoverflow exception * VM ARGS: -Xss128k 减少栈内存容量 */ public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak () { stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length = " + oom.stackLength); throw e; } } }
运行结果:
stack length = 11420 Exception in thread “main” java.lang.StackOverflowError at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12) at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13) at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
以上代码在单线程环境下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配时,抛出的都是 StackOverflowError 异常。
如果测试环境是多线程环境,通过不断建立线程的方式可以产生内存溢出异常,代码如下所示。但是这样产生的 OOM 与栈空间是否足够大不存在任何联系,在这种情况下,为每个线程的栈分配的内存足够大,反而越容易产生OOM 异常。这点不难理解,每个线程分配到的栈容量越大,可以建立的线程数就变少,建立多线程时就越容易把剩下的内存耗尽。这点在开发多线程的应用时要特别注意。如果建立过多线程导致内存溢出,在不能减少线程数或更换64位虚拟机的情况下,只能通过减少最大堆和减少栈容量来换取更多的线程。
/** * JVM 虚拟机栈内存溢出测试, 注意在windows平台运行时可能会导致操作系统假死 * VM Args: -Xss2M -XX:+HeapDumpOnOutOfMemoryError */ public class JVMStackOOM { private void dontStop() { while (true) {} } public void stackLeakByThread() { while (true) { Thread thread = new Thread(new Runnable() { @Override public void run() { dontStop(); } }); thread.start(); } } public static void main(String[] args) { JVMStackOOM oom = new JVMStackOOM(); oom.stackLeakByThread(); } }
方法区和运行时常量池溢出
方法区用于存放Class的相关信息,对这个区域的测试,基本思路是运行时产生大量的类去填满方法区,直到溢出。使用CGLib实现。
方法区溢出也是一种常见的内存溢出异常,在经常生成大量Class的应用中,需要特别注意类的回收情况,这类场景除了使用了CGLib字节码增强和动态语言外,常见的还有JSP文件的应用(JSP第一次运行时要编译为Java类)、基于OSGI的应用等。
/** * 测试JVM方法区内存溢出 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M */ public class MethodAreaOOM { public static void main(String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); } } static class OOMObject{} }
本机直接内存溢出
DirectMemory 容量可通过 -XX:MaxDirectMemorySize 指定,如不指定,则默认与Java堆最大值一样。测试代码使用了 Unsafe 实例进行内存分配。
由 DirectMemory 导致的内存溢出,一个明显的特征是在Heap Dump 文件中不会看见明显的异常,如果发现 OOM 之后 Dump 文件很小,而程序直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。
/** * 测试本地直接内存溢出 * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M */ public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws Exception { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB); } } }
本章小结
通过本章的学习,我们明白了虚拟机中的内存是如何划分的,哪部分区域、什么样的代码和操作可能导致内存溢出异常。虽然Java有垃圾收集机制,但内存溢出异常离我们仍然并不遥远,本章只是讲解了各个区域出现内存溢出异常的原因。
-
linux内存相关命令汇总
2021-10-09 20:13:12内存管理 查看内存剩余free: e0005055@ibudev20:~$ free total used free shared buff/cache available Mem: 32791720 19499516 935896 2552 12356308 12824920 Swap: 2097148 2048 2095100 其中,本文依据宋宝华《Linux内核铁三角之内存管理》课程顺序整理。
感谢ppipp1109提供课程笔记:https://ppipp.blog.csdn.net/article/details/108565269内存整体信息
查看内存剩余free:
e0005055@ibudev20:~$ free total used free shared buff/cache available Mem: 32791720 19499516 935896 2552 12356308 12824920 Swap: 2097148 2048 2095100
其中,total ≈ used+available
查看buddy信息buddyinfo:
~$ sudo cat /proc/buddyinfo Node 0, zone DMA 1 1 1 0 2 1 1 0 1 1 3 Node 0, zone DMA32 7437 3964 2908 225 83 19 8 2 0 0 0 Node 0, zone Normal 64044 11133 27161 593 116 3 0 0 0 0 0
从buddy可以看到mem区域和对应的使用情况。64系统因为寻址空间大,就不存在高端内存了。
查看slab信息slabinfo:
~$ sudo cat /proc/slabinfo slabinfo - version: 2.1 # name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail> uvm_tools_event_tracker_t 0 0 1128 29 8 : tunables 0 0 0 : slabdata 0 0 0 uvm_range_group_range_t 0 0 96 42 1 : tunables 0 0 0 : slabdata 0 0 0 uvm_va_block_context_t 0 0 1472 22 8 : tunables 0 0 0 : slabdata 0 0 0 uvm_va_block_gpu_state_t 0 0 432 37 4 : tunables 0 0 0 : slabdata 0 0 0 uvm_va_block_t 0 0 784 20 4 : tunables 0 0 0 : slabdata 0 0 0 uvm_va_range_t 0 0 1912 17 8 : tunables 0 0 0 : slabdata 0 0 0 btrfs_delayed_node 0 0 312 26 2 : tunables 0 0 0 : slabdata 0 0 0 btrfs_ordered_extent 0 0 416 39 4 : tunables 0 0 0 : slabdata 0 0 0 btrfs_inode 0 0 1168 28 8 : tunables 0 0 0 : slabdata 0 0 0 ufs_inode_cache 0 0 808 20 4 : tunables 0 0 0 : slabdata 0 0 0 qnx4_inode_cache 0 0 680 24 4 : tunables 0 0 0 : slabdata 0 0 0
查看zone信息zoneinfo:
:~$ sudo cat /proc/zoneinfo Node 0, zone DMA ... Node 0, zone Normal pages free 201343 min 15777 low 23415 high 31053 spanned 7790592 present 7790592 managed 7640422 protection: (0, 0, 0, 0, 0) nr_free_pages 201343 nr_zone_inactive_anon 65772 nr_zone_active_anon 56559 ... cpu: 7 count: 332 high: 378 batch: 63 vm stats threshold: 72 node_unreclaimable: 0 start_pfn: 1048576 ...
查看总体内存信息meminfo:
~$ cat /proc/meminfo MemTotal: 32791720 kB MemFree: 930456 kB MemAvailable: 12815656 kB Buffers: 2036752 kB Cached: 6727356 kB SwapCached: 244 kB Active: 5784032 kB Inactive: 3470456 kB Active(anon): 225796 kB Inactive(anon): 263096 kB ... Hugepagesize: 2048 kB Hugetlb: 0 kB DirectMap4k: 23929072 kB DirectMap2M: 9529344 kB DirectMap1G: 0 kB
虚拟内存信息
查看vmalloc映射信息vmallocinfo:
~$ sudo cat /proc/vmallocinfo 0xffffbc3d00000000-0xffffbc3d00005000 20480 irq_init_percpu_irqstack+0xcf/0x100 vmap 0xffffbc3d00005000-0xffffbc3d00007000 8192 acpi_os_map_iomem+0x17c/0x1b0 phys=0x000000008dbfe000 ioremap ... 0xffffbc3d0002c000-0xffffbc3d0002e000 8192 gen_pool_add_owner+0x42/0xb0 pages=1 vmalloc N0=1 0xffffbc3d0002e000-0xffffbc3d00030000 8192 bpf_prog_alloc_no_stats+0x4c/0xf0 pages=1 vmalloc N0=1 0xffffbc3d00030000-0xffffbc3d00035000 20480 _do_fork+0x76/0x370 pages=4 vmalloc N0=4 ... 0xffffbc3d00065000-0xffffbc3d00068000 12288 pcpu_mem_zalloc+0x48/0x70 pages=2 vmalloc N0=2 0xffffbc3d00068000-0xffffbc3d0006c000 16384 n_tty_open+0x19/0xa0 pages=3 vmalloc N0=3
查看虚拟内存统计信息vmstat:
很多参数在内核编译时开启VM_EVENT_COUNTERS,因为这部分仅仅用于调试和统计。
$ cat /proc/vmstat
参考:
https://www.kernel.org/doc/Documentation/vm/transhuge.txt实时虚拟内存命令vmstat:
vmstat是Virtual Meomory Statistics(虚拟内存统计)的缩写,可对操作系统的虚拟内存、进程、CPU活动进行监控。
是对系统的整体情况进行统计,无法进行某个进程的分析。$ vmstat 5 5 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 1 0 0 26016976 972420 5206800 0 0 3 9 30 4 1 0 99 0 0 0 0 0 26016716 972428 5206836 0 0 0 4 275 428 0 0 99 0 0 0 0 0 26017284 972428 5206836 0 0 0 0 324 513 0 0 99 0 0 0 0 0 26016992 972436 5206828 0 0 0 2 299 493 0 0 99 0 0 0 0 0 26016512 972436 5206836 0 0 0 0 265 440 0 0 99 0 0
参考:https://www.cnblogs.com/ftl1012/p/vmstat.html
查看进程的vm分布pmap
pmap
pmap [pid]
$ pmap 13713 13713: ./a.out 0000000000400000 4K r-x-- a.out 0000000000600000 4K r---- a.out 0000000000601000 4K rw--- a.out 0000000002315000 132K rw--- [ anon ] 00007f7e2ee73000 1948K r-x-- libc-2.27.so 00007f7e2f05a000 2048K ----- libc-2.27.so 00007f7e2f25a000 16K r---- libc-2.27.so 00007f7e2f25e000 8K rw--- libc-2.27.so 00007f7e2f260000 16K rw--- [ anon ] 00007f7e2f264000 164K r-x-- ld-2.27.so 00007f7e2f46b000 8K rw--- [ anon ] 00007f7e2f48d000 4K r---- ld-2.27.so 00007f7e2f48e000 4K rw--- ld-2.27.so 00007f7e2f48f000 4K rw--- [ anon ] 00007ffe47bad000 132K rw--- [ stack ] 00007ffe47bf8000 12K r---- [ anon ] 00007ffe47bfb000 4K r-x-- [ anon ] ffffffffff600000 4K --x-- [ anon ] total 4516K
循环显示最后一行:
while true; do pmap -d 2173 | tail -1; sleep 2; done
pmap可以显示swap和匿名页等更多信息,使用pmap --help参看细节.
$ pmap -p 25991 -XX 25991: /home/e0005055/wk/test/mm/a.out Address Perm Offset Device Inode Size KernelPageSize MMUPageSize Rss Pss Shared_Clean Shared_Dirty Private_Clean Private_Dirty Referenced Anonymous LazyFree AnonHugePages ShmemPmdMapped FilePmdMapped Shared_Hugetlb Private_Hugetlb Swap SwapPss Locked THPeligible VmFlagsMapping 00400000 r-xp 00000000 103:01 17050969 4 4 4 4 4 0 0 4 0 4 0 0 0 0 0 0 0 0 0 0 0 rd ex mr mw me dw sd /home/e0005055/wk/test/mm/a.out 00600000 r--p 00000000 103:01 17050969 4 4 4 4 4 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 rd mr mw me dw ac sd /home/e0005055/wk/test/mm/a.out 00601000 rw-p 00001000 103:01 17050969 4 4 4 4 4 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me dw ac sd /home/e0005055/wk/test/mm/a.out 01c1e000 rw-p 00000000 00:00 0 132 4 4 4 4 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me ac sd [heap] 7f5ec0f32000 r-xp 00000000 103:01 49681351 1948 4 4 1208 11 1208 0 0 0 1208 0 0 0 0 0 0 0 0 0 0 0 rd ex mr mw me sd /lib/x86_64-linux-gnu/libc-2.27.so 7f5ec1119000 ---p 001e7000 103:01 49681351 2048 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 mr mw me sd /lib/x86_64-linux-gnu/libc-2.27.so 7f5ec1319000 r--p 001e7000 103:01 49681351 16 4 4 16 16 0 0 0 16 16 16 0 0 0 0 0 0 0 0 0 0 rd mr mw me ac sd /lib/x86_64-linux-gnu/libc-2.27.so 7f5ec131d000 rw-p 001eb000 103:01 49681351 8 4 4 8 8 0 0 0 8 8 8 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me ac sd /lib/x86_64-linux-gnu/libc-2.27.so 7f5ec131f000 rw-p 00000000 00:00 0 16 4 4 12 12 0 0 0 12 12 12 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me ac sd 7f5ec1323000 r-xp 00000000 103:01 49681347 164 4 4 164 1 164 0 0 0 164 0 0 0 0 0 0 0 0 0 0 0 rd ex mr mw me dw sd /lib/x86_64-linux-gnu/ld-2.27.so 7f5ec152a000 rw-p 00000000 00:00 0 8 4 4 8 8 0 0 0 8 8 8 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me ac sd 7f5ec154c000 r--p 00029000 103:01 49681347 4 4 4 4 4 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 rd mr mw me dw ac sd /lib/x86_64-linux-gnu/ld-2.27.so 7f5ec154d000 rw-p 0002a000 103:01 49681347 4 4 4 4 4 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me dw ac sd /lib/x86_64-linux-gnu/ld-2.27.so 7f5ec154e000 rw-p 00000000 00:00 0 4 4 4 4 4 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me ac sd 7ffd2b423000 rw-p 00000000 00:00 0 132 4 4 16 16 0 0 0 16 16 16 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me gd ac [stack] 7ffd2b583000 r--p 00000000 00:00 0 12 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 rd mr pf io de dd sd [vvar] 7ffd2b586000 r-xp 00000000 00:00 0 4 4 4 4 0 4 0 0 0 4 0 0 0 0 0 0 0 0 0 0 0 rd ex mr mw me de sd [vdso] ffffffffff600000 --xp 00000000 00:00 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ex [vsyscall] ==== ============== =========== ==== === ============ ============ ============= ============= ========== ========= ======== ============= ============== ============= ============== =============== ==== ======= ====== =========== 4516 72 72 1464 100 1376 0 4 84 1464 84 0 0 0 0 0 0 0 0 0 0 KB
cat /proc/pid/maps
$ cat /proc/25991/maps 00400000-00401000 r-xp 00000000 103:01 17050969 /home/e0005055/wk/test/mm/a.out 00600000-00601000 r--p 00000000 103:01 17050969 /home/e0005055/wk/test/mm/a.out 00601000-00602000 rw-p 00001000 103:01 17050969 /home/e0005055/wk/test/mm/a.out 01c1e000-01c3f000 rw-p 00000000 00:00 0 [heap] 7f5ec0f32000-7f5ec1119000 r-xp 00000000 103:01 49681351 /lib/x86_64-linux-gnu/libc-2.27.so 7f5ec1119000-7f5ec1319000 ---p 001e7000 103:01 49681351 /lib/x86_64-linux-gnu/libc-2.27.so 7f5ec1319000-7f5ec131d000 r--p 001e7000 103:01 49681351 /lib/x86_64-linux-gnu/libc-2.27.so 7f5ec131d000-7f5ec131f000 rw-p 001eb000 103:01 49681351 /lib/x86_64-linux-gnu/libc-2.27.so 7f5ec131f000-7f5ec1323000 rw-p 00000000 00:00 0 7f5ec1323000-7f5ec134c000 r-xp 00000000 103:01 49681347 /lib/x86_64-linux-gnu/ld-2.27.so 7f5ec152a000-7f5ec152c000 rw-p 00000000 00:00 0 7f5ec154c000-7f5ec154d000 r--p 00029000 103:01 49681347 /lib/x86_64-linux-gnu/ld-2.27.so 7f5ec154d000-7f5ec154e000 rw-p 0002a000 103:01 49681347 /lib/x86_64-linux-gnu/ld-2.27.so 7f5ec154e000-7f5ec154f000 rw-p 00000000 00:00 0 7ffd2b423000-7ffd2b444000 rw-p 00000000 00:00 0 [stack] 7ffd2b583000-7ffd2b586000 r--p 00000000 00:00 0 [vvar] 7ffd2b586000-7ffd2b587000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
cat /proc/pid/smaps
详细描述每一个段的信息,和pmap命令差不多。
$ cat /proc/25991/smaps 00400000-00401000 r-xp 00000000 103:01 17050969 /home/e0005055/wk/test/mm/a.out Size: 4 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Rss: 4 kB Pss: 4 kB Shared_Clean: 0 kB ... THPeligible: 0 VmFlags: rd ex mr mw me dw sd 00600000-00601000 r--p 00000000 103:01 17050969 /home/e0005055/wk/test/mm/a.out Size: 4 kB KernelPageSize: 4 kB ... ...
查看系统整体进程占用smem
smem是通过/proc/PID/smaps分析系统内存
$ smem PID User Command Swap USS PSS RSS 25991 e0005055 /home/e0005055/wk/test/mm/a 0 88 100 1464 17626 e0005055 script test.sh_tmp.log 0 172 212 2812 4398 e0005055 /usr/bin/dbus-daemon --conf 0 424 514 3840 5274 e0005055 /usr/lib/dconf/dconf-servic 0 688 766 5148 4296 e0005055 /usr/lib/x86_64-linux-gnu/x 0 680 769 5272 4384 e0005055 /usr/lib/at-spi2-core/at-sp 0 760 859 6280 ...
android 工具procmem/procrank
procrank是通过/proc/kpagemap分析,暂未实践。
overcommit与OOM
内存超量使用overcommit_memory
overcommit为超量使用,申请内存大小超过当前已申请(并非已分配)内存和swap的总和,仍然允许申请(并非分配),称为超量使用。
在meminfo中两个值:~$ cat /proc/meminfo ... CommitLimit: 18493008 kB Committed_AS: 3567204 kB
CommitLimit为overcommit的阈值,申请的内存总数超过CommitLimit的话就算是overcommit。
Committed_AS 表示所有进程已经申请的内存总大小,如果超过CommitLimit就是已经发生overcommit。
CommitLimit既不是物理内存的大小,也不是free memory的大小,它是通过内核参数vm.overcommit_ratio或vm.overcommit_kbytes间接设置的,公式如下:
【CommitLimit = (Physical RAM * vm.overcommit_ratio / 100) + Swap】
overcommit_ratio是设定的超量使用比例,默认为50,可以通过overcommit_ratio查看:~$ cat /proc/sys/vm/overcommit_ratio 50
超量使用可以通过以下方式查看:
~$ cat /proc/sys/vm/overcommit_memory 0
其中,0为默认值,允许超量使用,但是不能超过系统总内存。1为无限制超量使用,2为关闭超量使用。
sudo sh -c ‘echo 1 \> /proc/sys/vm/overconmit_memory’
其它配置方式:
- 永久生效:
$ vim /etc/sysctl.conf vm.overcommit_memory=1 $ sysctl -p
- 临时修改:
$ sysctl vm.overcommit_memory=1
或者:
$ sysctl -w vm.overcommit_memory=1
参考:
https://access.redhat.com/solutions/665023
http://linuxperf.com/?p=102OOM
内存耗尽时,会根据当前进程的oom_score值来决定某一进程结束:
oom_score会加上oom_score_adj这个值,oom_score_adj的取值范围是-1000~1000,$ cat /proc/898/oom_score 0 $ cat /proc/898/oom_score_adj -999
oom_adj是调整进程优先级,序号越靠后,优先级越低,oom_score越大。范围【-17,16】默认为0,系统级进程为负值。
oom_adj是一个旧的接口参数,其功能类似oom_score_adj,保留时为了兼容,其实最后也是算oom_score_adj。
$ cat /proc/898/oom_adj -16
OOM测试时,需要关闭swap分区,关闭overcommit:
sudo swapoff -a sudo sh -c ‘echo 1 \> /proc/sys/vm/overconmit_memory’ git grep overcommit_memory
普通权限可以将oom_adj往后调整,但需要管理员权限才能往前调整:
$ cat /proc/18092/oom_adj 0 $ echo 13 > /proc/18092/oom_adj $ cat /proc/18092/oom_adj 12 $ echo 3 > /proc/18092/oom_adj -bash: echo: write error: Permission denied $ sudo echo 3 > /proc/18092/oom_adj $ cat /proc/18092/oom_adj 2
/proc/sys/vm/panic_on_oom设定oom发生时,采取的策略。
当该参数等于0的时候,启动OOM killer回收内存。当该参数等于2的时候,强制kernel panic宕机。$ cat /proc/sys/vm/panic_on_oom 0
/proc/sys/vm/oom_dump_tasks设定是否在oom时打印所有进程的内存信息,设置非0时会打印。
$ cat /proc/sys/vm/oom_dump_tasks 1
参考:https://blog.csdn.net/u011677209/article/details/52769225
内存泄漏检测工具
内存泄露通过长时间观察内存状态,占用持续上升。
valgrind
除内存泄露之外,可以检测多线程、缓存、堆栈等相关问题。
AddressSanitizer
Address Sanitizer是基于LLVM的的工具,已经内置在GCC4.8以上版本。使用时大概多出2倍的运行开销,属于比较快速的工具。
Address Sanitizer替换了malloc和free的实现。根据内存分配和释放操作,将不可访问区域标记为”off-limits“,当访问到这些标记内存区域时,报告异常。
Address Sanitizer可以用来检测如下内存使用错误:内存释放后又被使用;
内存重复释放;
释放未申请的内存;
使用栈内存作为函数返回值;
使用了超出作用域的栈内存;
内存越界访问;
需要在程序编译时使用 -fsanitize=address、-fsanitize=leaks,-fno-omit-frame-pointer用于额外的堆栈信息。之后再运行,出现问题时会报错。静态代码检测工具cppcheck
cppcheck是一个C/C++静态检查工具。协助侦测代码级别的问题,比如数组越界、内存申请未释放、文件打开未关闭等。
大部分IDE都有用于静态检测的插件,比如SourceInsight\Eclipse\VS Code , 可以在互联网搜索相关信息。内核泄露检测kmemleak
有时候内存泄露并不是应用造成的,而是依赖的对应内核模组产生的。kmemleak是Linux内核自带的检测工具,不过使用该功能需要在内核编译时需要打开kmemleaks选项,重新编译内核。
GNU C库中的内存检测工具mtrace
mtrace是GNU扩展函数,mtrace为内存分配函数(malloc, realloc, memalign, free)安装hook函数。
使用mtrace需要在代码里添加mtrace函数重新编译运行,在运行停止之后会打印出尚未释放的内存信息,然后用mtrace分析log,找到对应的函数。之后查看代码,分析其调用信息修改。内存分析一般步骤
工程调试内存泄漏问题一般步骤:
-
meminfo, free 多点采样
使用多点采样,确认是否有内存泄漏。 -
定位程序
通过smem等方式,检查用户空间,找到可疑的应用程序 -
检查内核空间
通过slab等信息判断内核是否存在泄露,通过kmemleak分析内核泄露信息。 -
通过buddy、slab、meminfo等系统信息来辅助分析。
page cache
缓存和回收
free 命令中,buff是指文件IO时的缓存,cache是上层文件系统的缓存。
不带pagecached的IO为direct IO。 可以在用户态根据业务特点做cached,在某些场合上会在应用层来建立对应的访问逻辑。
类似于CPU跳过cache,直接访问mem;匿名页和文件背景页
- 文件背景页:
- 在内存的缓存中,具有文件属性的缓存称为file_page,可以回收。比如代码段,映射文件等。
- 匿名页:
堆栈变量类的数据为anon page,匿名页,不可回收。
swap
在内核配置CONFIG_SWAP,支持匿名页swap。不配置,普通文件swap依然支持;
SWAP分区,对应windows的虚拟内存文件pagefile.system。页面回收和LRU
局部性原理:最近活跃的就是将来活跃的,最近不活跃的,以后也不活跃。
包括时间局部性,空间局部性。
LRU:Least Recently Used最近最少使用,是一种常用的页面置换算法。问题
swap匿名页可以理解,是将数据交换到磁盘上的swap区域。
但是普通文件swap,究竟交换的是什么?比如一个so文件,swap到swap分区,和直接与库文件原先的位置来加载有啥区别?
答案:普通文件不需要swap,文件页本身就是可以回收的,不需要放到匿名页。swap只是针对匿名页,原本不可回收的、没有任何文件背景的数据。嵌入式设备和zRAM:
嵌入式设备,一般不用swap,不使能swap,因为:
1.嵌入式磁盘速度很慢;
2.FLASH读写寿命有限;
zRAM的功能就是将RAM划分一部分区域,将RAM中数据压缩放入该区域来swap。这就实现了用CPU算力来换取空间。#内核使能swap echo $((48*1024*1024)) > /sys/block/zram0/disksize 打开swap分区: swapon –p 10 /dev/zram0 cat /proc/swaps
swapoff –a 不能关掉文件背景页面。
脏页写回
脏页回写是由pdfulsh执行的。其运行周期dirty_writeback_centisecs,单位厘秒:
$ cat /proc/sys/vm/dirty_writeback_centisecs 500
脏页回写有两个机制:
超过时间周期触发回收
- dirty_expire_centisecs
既定时间周期执行/proc/sys/vm/dirty_expire_centisecs 周期
单位厘秒(默认值3000厘秒:3000*10毫秒,3000/100秒)
$ cat /proc/sys/vm/dirty_expire_centisecs 3000
超过空间比值触发回收
- dirty_background_ratio
脏页所占比例到达既定值/proc/sys/vm/dirty_background_ratio
脏数据所占内存 / (MemFree + Cached - Mapped ) > dirty_background_ratio,默认10%
$ cat /proc/sys/vm/dirty_background_ratio 10
当dirty_expire_centisecss设置较小,刷新频率就会增加,这样就会使得脏数据所占总内存的比例不会达到dirty_background_ratio,从而使得dirty_background_ratio参数没有什么作用。
相反,如果dirty_background_ratio参数设置很小同时dirty_expire_centisecs设置较大,dirty_expire_centisecss可能也用不上。- dirty_ratio
当系统大量触发IO,硬件IO速度达不到缓存的速度,导致脏页比例超过该值时,所有IO将被停止,直到当前数据回写完成。默认为20%。
$ cat /proc/sys/vm/dirty_ratio 20
假如某个进程在不停写数据,当写入大小触发dirty_background_ratio_10%时,脏页开始写入磁盘,写入数据大小触发dirty_ratio_20%时(磁盘IO速度远比写内存慢),如果继续写,会被内核阻塞,等脏页部分被写入磁盘,释放pagecached后,进程才能继续写入内存;
参考:
https://www.cnblogs.com/ywcz060/p/5589926.html
https://blog.csdn.net/u010039418/article/details/107500892swappiness空间回收
内存回收,是通过kswap进行的,涉及三个水位值,min\low\high:
:~$ sudo cat /proc/zoneinfo Node 0, zone DMA ... Node 0, zone Normal pages free 201343 min 15777 low 23415 high 31053 spanned 7790592
当内存水位低于low时开始回收,直到水位达到high停止。当水位低于min时,直接阻塞应用,在进程上下文直接回收。
默认值60,最大值100.Swappiness越大,越优先回收匿名页;
即使Swappiness设置为0,优先回收文件背景页,以使达到最低水位;假使达不到,还是会回收匿名页;cgroup中使用内存
./swapoff –a echo 1 \> /proc/sys/vm/overcommit_memory // cd /sys/fs/cgroup/memory mkdir A cd A sudo echo \$(100\*1024\*1024) \> memory.limit_in_bytes //限制最大内存100M //a.out放到A cgroup执行; sudo cgexec –g memory:A ./a.out
swap主要是解决内存不足的问题。
目前, Hadoop集群,kubernetes等都会关闭swap。首先是swap交换引起IO和内存的性能问题。另外,开启swap后通过cgroups设置的内存上限就会失效。
另外,对于cgroup而言,设置swappiness=0,该group的swap回收会被关闭;但是全局的swap还是可以回收;Q&A
Q: swap只针对匿名页,不会对文件页进行交换。比如lib.so等,没有意义。
A:是的,文件页是直接回收,匿名页才会被交换到swap分区。 -
超硬核!十万字c++题,让你秒杀老师和面试官(上)
2021-04-21 10:46:15浅复制 —-只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做“(浅复制)浅拷贝”,换句话说,浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来... -
深入理解Java虚拟机-垃圾回收器与内存分配策略
2020-01-04 13:08:32Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来。 文章目录概述对象已死吗引用计数法可达性分析算法再谈引用生存还是死亡回收方法区垃圾收集算法标记-清除... -
【Android 内存优化】dumpsys meminfo PID 查看单进程内存信息详解
2022-04-04 22:13:13【Android 内存优化】dumpsys meminfo PID 查看单进程内存信息详解 -
玩转Redis-删除了两百万key,为什么内存依旧未释放?
2020-10-08 23:39:19删除了两百万key,为什么内存依旧未释放? 如何查看Redis内存数据 内存为何不释放 什么是内存碎片 Redis的内存碎片是如何形成的 如何释放内存 生产环境整理理内存碎片的注意事项 -
JVM内存模型
2020-07-06 15:54:20JVM内存模型 JVM的内存模型也就是JVM中的内存布局,不要与java的内存模型(与多线程相关)混淆。 下图是jdk8 jvm内存模型图: 程序计数器 程序计数器是当前线程所执行的字节码的行号指示器。 JVM支持多个线程同时... -
Java虚拟机中对象内存的分配情况
2019-02-20 17:50:16本文主要是介绍下对象在虚拟机中的内存布局分配情况。 JVM的内存对象介绍[创建和访问] 对象的内存布局 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头,实例数据和对齐填充。 对象头 ... -
内存的分类与结构
2017-08-11 17:19:42不同类型的内存传输类型各有差异,在传输率、工作频率、工作方式等方面均会有所不同。市场中主要存在的内存类型有SDRAM、DDR SDRAM (简称为DDR)、 RDRAM三种。EDO是Extended Data Out(扩展数据输出)的简称,它... -
java 命令: jmap 命令使用 ( 查看内存使用、设置 )
2018-11-26 16:59:56主要用于打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。 jmap命令可以获得运行中的jvm的堆的快照,从而可以离线分析堆,以检查内存泄漏,检查一些严重影响性能的大对象的创建,... -
深入理解操作系统[8]:内存管理
2018-11-16 17:54:34内存的连续分配方式对换4. 基本分页式5. 基本分段方式 1 存储系统的层次结构 寄存器:寄存器访问速度最快,完全能与 CPU 协调工作。寄存器的长度一般以字(word)为单位。 主存:CPU的控制部件只能从主存储器中取得... -
超硬核十万字!全网最全 数据结构 代码,随便秒杀老师/面试官,我说的
2021-04-11 01:11:23采用这种方法时,可实现对结点的随机存取,即每一个结点对应一个序号,由该序号可以直接计算出来结点的存储地址。但顺序存储方法的主要缺点是不便于修改,对结点的插入、删除运算时,可能要移动一系列的结点。 优点... -
pandas.read_csv() 详解与如何合适的读取行序号与列名
2021-05-20 18:34:22如果列或者索引不能被看作日期序列,称为不可解析的值,则将其按照原数据类型返回。 For non-standard datetime parsing, use pd.to_datetime after pd.read_csv. To parse an index or column with a mixture of ... -
JVM内存结构,JAVA类的加载机制,GC算法,垃圾收集器
2018-08-20 14:01:27类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class... -
内存溢出和内存泄漏的区别
2015-07-11 15:55:31内存溢出 out of memory,是指程序在...内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。 memory leak会 -
Linux内核内存管理
2018-04-07 23:39:471、内存模型概览通常CPU可见的地址是有限制的,32位的CPU最多可见4GB的物理空间,64位的CPU可见的空间会更大。32位的系统一般都需要考虑通过“动态映射”方法拓展物理内存的可见性问题。通常程序访问的地址都是虚拟... -
Elasticsearch学习-关于倒排索引、DocValues、FieldData和全局序号
2018-09-12 21:59:39在为每个段加载FieldData后,ES会构建一个称为Global Ordinals(全局序号)的数据结构来构建一个由分片内的所有段中的唯一term组成的列表。默认的,Global Ordinals是延迟构建的。如果这个field的基数非常高,那么... -
超硬核!操作系统学霸笔记,考试复习面试全靠它
2021-03-22 18:43:49活动态和静止态最本质的区别为活动态在内存中,静止态暂时调出内存,进入外存 (3由执行态可以直接变为静止就绪态,即时间片用完,直接调离内存 (4)静止态(外存)必须通过激活变为非静止态(调入内存)才能够参与... -
缓存/内存
2020-04-09 14:19:10随机存取存储器(Random Access Memory, RAM),也称主存(或内存),与CPU直接交换数据的内部存储器,可随时读写,速度很快.主存(Main Memory)计算机内最主要的存储器,用于加载各种程序与数据以供CP... -
内存知识大全
2018-06-08 22:48:22在主机中,内存所存储的数据或程序有些是永久的,有些是暂时的,所以内存就有不同形式的功能与作用,而且存储数据的多少也关系着内存的容量大 小,传送数据的快慢也关系着内存的速度,这些都跟内存的种类与功能有关... -
操作系统总结之内存管理(除虚拟内存管理)
2018-07-02 23:14:351 存储器的层次结构 ...辅存和可移动存储介质 属于设备管理(因此会涉及中断,设备驱动程序和物理设备的运行),存储的信息被长期保存 1.1 主存储器和寄存器 主存储器 用于保存进程运行时的程序和数据,... -
为什么32位cpu只支持4G内存?
2019-04-10 10:08:25理解基本概念 首先内存是cpu处理数据的临时存储站,cpu每次解析的数据(指令)都是内存传来的,数据...在微机的内存中,每个基本单位都被赋予一个惟一的序号,这个序号称为地址,而内存的基本单位是Byte(这一点对后面... -
Linux 0.01 内存管理
2020-08-15 11:01:26这段代码的思想是将主内存区分为 4K 为一段来管理,mem_map 的一个 entry 项代表一个 4K 的页,当其值为 0 时,表示该页没有被占用,大于 0 表示被占用,大于 1 表示页面被共享。MAP_NR 求给定地址 addr 所在页面在... -
超硬核!数据结构学霸笔记,考试面试吹牛就靠它
2021-03-26 11:11:21使用这种方式时,时间复杂度可被称为是渐近的,它考察当输入值大小趋近无穷时的情况。 注意:文中提到:不包括这个函数的低阶项和首项系数。什么意思呢?就是说10n,100n,哪怕1000000000n,还是算做O(N),而低阶项是... -
内存溢出(out of memory)和内存泄露(memory leak)的区别和检测工具方法
2019-01-21 15:02:35内存溢出 out of memory,...内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。 memory leak会最终会导... -
[RFC]RISC-V内存一致性模型
2020-08-21 00:10:23[RFC]RISC-V内存一致性模型 https://gitee.com/laokz/OS-kernel-test/blob/master/memorder/riscv.md -
GDB观察栈的内存布局
2017-03-29 12:38:43进程的内存布局如下图所示,栈是其中一块向下(低地址处)增长的内存。 栈的英文是stack,堆的英文是heap,很多人把stact翻译成堆栈,是不对的。 栈由栈帧组成。当一个函数调用时,栈会为这个函数分配... -
C++内存泄露和内存管理
2016-05-24 22:26:12一直没有找到系统的讲解C++内存管理的文章,所以结合自己的工作经验,以及网友的一些总结,分析了内存泄露检测的方法,一般原则,最后还补充了内存溢出