精华内容
下载资源
问答
  • 内存 - 工作集:私人工作集中的内存数量与进程正在使用且可以由其他进程共享的内存数量总和。 内存 - 峰值工作集:进程所使用的工作内存的最大数量。 内存 - 工作集增量:进程所使用的工作内存更改量。 ...
    • 内存 - 工作集:私人工作集中的内存数量与进程正在使用且可以由其他进程共享的内存数量的总和。 
    • 内存 - 峰值工作集:进程所使用的工作集内存的最大数量。 
    • 内存 - 工作集增量:进程所使用的工作集内存中的更改量。 
    • 内存 - 私人工作集:工作集的子集,它专门描述了某个进程正在使用且无法与其他进程共享的内存数量。 
    • 内存 - 提交大小:为某进程使用而保留的虚拟内存的数量。 
    • 内存 - 页面缓冲池:由内核或驱动程序代表进程分配的可分页内核内存的数量。可分页内存是可写入其他存储媒体(例如硬盘)的内存。 
    • 内存 - 非页面缓冲池:由内核或驱动程序代表进程分配的不可分页的内核内存的数量。不可分页的内存是不能写入其他存储媒体的内存。

    转载于:https://www.cnblogs.com/xidongs/p/3809014.html

    展开全文
  • 在很多脚本语言中,您不必担心内存如何管理的,这并不能使得内存管理的重要性有一点点降低。对实际编程来说,理解您的内存管理器的能力与局限性至关重要。在大部分系统语言中,比如 C 和 C++,您必须进行内存...
  • 前两篇文章中,讲CLR的内存管理,都提到了垃圾回收器(GC)作用。那么GC具体怎么在虚拟内存中进行存储空间监控和释放呢?下面就介绍一下这个过程: 什么是GC? 正如其名,Garbage Collector,垃圾收集器,...

    前两篇文章中,讲CLR的内存管理,都提到了垃圾回收器(GC)的作用。那么GC具体是怎么在虚拟内存中进行存储空间的监控和释放的呢?下面就介绍一下这个过程:

    什么是GC?

    正如其名,Garbage Collector,垃圾收集器,也就是说,GC的主要任务就是垃圾的清理。以应用程序的root为基础,遍历应用程序在Heap上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是已经死亡的哪些仍需要被使用。已经不再被应用程序的root或者别的对象所引用的对象就是已经死亡的对象,即所谓的垃圾,需要被回收。这就是GC工作的原理。为了实现这个原理,GC有多种算法。比较常见的算法有Reference CountingMark SweepCopy Collection等等。目前主流的虚拟系统.net CLRJava VMRotor都是采用的Mark Sweep算法。本文以.net为基础,这里只对Mark Sweep算法进行讲述。

    Mark Sweep算法

    在程序运行的过程中,不断的把Heap的分配空间给对象,当Heap的空间被占用到不足 以为下一个对象分配的时候Mark Sweep算法被激活,将垃圾内存进行回收并将其返回到free list中。Mark Sweep就像它的名字一样在运行的过程中分为两个阶段,Mark阶段和Sweep阶段。Mark阶段的任务是从root出发,利用相互的引用关系遍历整个Heap,将被root和其它对象所引用的对象标记起来。没有被标记的对象就是垃圾。之后是Sweep阶段,这个阶段的任务就是回收所有的垃圾。

    Mark Sweep算法虽然速度比Reference Counting要快,并且可以避免循环引用造成的内存泄漏。但是也有不少缺点,它需要遍历Heap中所有的对象(存活的对象在Mark阶段遍历,死亡的对象在Sweep阶段遍历)所以速度也不是十分理想。而且对垃圾进行回收以后会造成大量的内存碎片。为了解决这两个问题,Mark Sweep算法得到了改进。首先是在算法中加入了Compact阶段,即先标记存活的对象,再移动这些对象使之在内存中连续,最后更新和对象相关的地址和free list。这就是Mark Compact算法,它解决了内存碎片的问题。而为了提高速度,Generation(代)的概念被引入了。

    Generation(代):

    Generation的设计是基于以下几个原则:

    1,对象越年轻则它的生命周期越短

    2,对象越老则它的生命周期越长

    3,年轻的对象和其它对象的关系比较强,被访问的频率也比较高

    4,Heap(托管堆)一部分的回收压缩比对整个Heap的回收压缩要快

    Generation的概念就是对Heap中的对象进行分代(分成几块,每一块中的对象生存期不同)管理。当对象刚被分配时位于Generation 0中,当Generation 0的空间将被耗尽时,Mark Compact算法被启动。经过几次GC后如果这个对象仍然存活则会将其移动到Generation 1中。同理,如果经过几次GC后这对象还是存活的,则会被移动到Generation 2中,直到被移动到最高级中最后被回收或者是同程序一同死亡。采用Generation的最大好处就在于每次GC不用对整个Heap都进行处理,而是每次处理一小块。对于Generation 0中的对象,因为它们死亡的可能性最大,所以对它们GC的次数可以安排多一些,而其它相对死亡的可能性小一些的对象所在的Generation可以少安排几次GC。这样做就使得GC的速度得到了一定程度的提高。这样就产生了几个有待讨论的问题,首先是应该设置几个Generation,每个Generation应该设置成多大,然后是对每个对象升级时它应该是已被GC了多少次而仍然存活。关于.net CLR对这个问题的处理,在本文的最后将给出一个例子对其进行测试。

    相关的数据结构:

    .net GC相关的数据结构有三个Managed HeapFinalization QueueFreachable Queue

    Managed Heap(托管堆)

    Managed Heap是一个设计简单而优化的堆,它与传统的C-runtime的堆不太一样。它的简单管理方法是为了提高对堆的管理速度,同时也是基于一个简单的(也是不可能的)假设。对Managed Heap的管理假设内存是无穷无尽的。在Managed Heap上有一个称为NextObjPtr的指针,这个指针用于指示堆上最后一个对象的地址。当有一个新的对象要分配到这个堆上时,所要做的仅仅是将NextObjPtr的值加上新对象的大小形成新的NextObjPtr。这只是一个简单的相加,当NextObjPtr的值超出了Managed Heap边界的时候说明堆已经满了,GC将被启动。

    Finalization QueueFreachable Queue

    这两个队列和.net对象所提供的Finalize方法有关。这两个队列并不用于存储真正的对象,而是存储一组指向对象的指针。当程序中使用了new操作符在Managed Heap上分配空间时,GC会对其进行分析,如果该对象含有Finalize方法则在Finalization Queue中添加一个指向该对象的指针。在GC被启动以后,经过Mark阶段分辨出哪些是垃圾。再在垃圾中搜索,如果发现垃圾中有被Finalization Queue中的指针所指向的对象,则将这个对象从垃圾中分离出来,并将指向它的指针移动到Freachable Queue中。这个过程被称为是对象的复生(Resurrection),本来死去的对象就这样被救活了。为什么要救活它呢?因为这个对象的Finalize方法还没有被执行,所以不能让它死去。Freachable Queue平时不做什么事,但是一旦里面被添加了指针之后,它就会去触发所指对象的Finalize方法执行,之后将这个指针从队列中剔除,这是对象就可以安静的死去了。.net frameworkSystem.GC类提供了控制Finalize的两个方法,ReRegisterForFinalizeSuppressFinalize。前者是请求系统完成对象的Finalize方法,后者是请求系统不要完成对象的Finalize方法。ReRegisterForFinalize方法其实就是将指向对象的指针重新添加到Finalization Queue中。这就出现了一个很有趣的现象,因为在Finalization Queue中的对象可以复生,如果在对象的Finalize方法中调用ReRegisterForFinalize方法,这样就形成了一个在堆上永远不会死去的对象,像凤凰涅槃一样每次死的时候都可以复生(GC直接控制方法中有一个KeepAlive()方法,估计就是运用的这个机制保证对象的长久驻留,有兴趣的朋友可以验证一下~~)。

    虽然.net提倡让GC自动去管理内存的清除工作,但是还是提供了一些直接控制GC的方法,请看下面的示例:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace Demo
    {
        class GCTest
        {
            static void Main()
            {
                Console.WriteLine(string.Format("GC MaxGengerations: {0}", GC.MaxGeneration));
                Generation gen = new Generation();
                gen.ConsoleGeneration();
    
                for (int i = 0; i <= GC.MaxGeneration; i++)
                {
                    GC.Collect();
                    gen.ConsoleGeneration();
                }
    
                gen = null;
    
                for (int i = 0; i <= GC.MaxGeneration; i++)
                {
                    GC.Collect(i);
                    GC.WaitForPendingFinalizers();
                }
    
                    Console.ReadLine();
            }
        }
    
        public class Generation
        {
            public void ConsoleGeneration()
            {
                Console.WriteLine(GC.GetGeneration(this));
            }
        }
    }
    

    首先我们声明了一个类Generation用于验证GC的按Generation(代)处理的机制。通过GC.GetGeneration(this)我们可以得到当前实例的Generation值。通过GC.MaxGeneration我们可以得到GC中Generation的最大值。接下来的循环中,由于我们没有取消对实例的引用,所以每次的Collect()操作都会把gen的Generation上移一次。然后,我们取消对gen对实例的引用,再次循环进行Collect()操作,注意我们这里使用了GC.WaitForPendingFinalizers()方法,MSDN对其的解释是:“挂起当前线程,直到处理终结器队列的线程清空该队列为止”。也就是为了防止上面提到的复生(Resurrection过程造成对象没有被删除。

    上面示例的运行结果如下:

    当然,有关GC直接控制的方法绝不止于这些,详细的介绍,请参考:http://msdn.microsoft.com/zh-cn/library/vstudio/baa1f39s.aspx

    转载于:https://www.cnblogs.com/yonghuisoft/archive/2013/01/20/2868786.html

    展开全文
  • 前言:这篇文章的主要内容由翻译而来,原文链接。但是大体内容与原文不尽相同,删除了一些内容,同时新增部分内容。由于本文大部分内容翻译而来,若有理解不当之处还望谅解并指出,我会尽快进行修改。(内心:如果...

    前言:这篇文章的主要内容由翻译而来,原文链接。但是大体内容与原文不尽相同,删除了一些内容,同时新增部分内容。由于本文大部分内容是翻译而来,若有理解不当之处还望谅解并指出,我会尽快进行修改。(内心:如果有什么不对的地方还希望大家指出,反正我也不会改 。玩笑话玩笑话 别当真!)

    概述

    在一些语言中,开发人员需要手动的使用原生语句来显示的分配和释放内存。但是在许多高级语言中,这些过程都会被自动的执行。在JavaScript中,变量(对象,字符串,等等)创建的时候为其分配内存,当不再被使用的时候会“自动地”释放这些内存,这个过程被称为垃圾回收。这个看似“自动的”释放资源的本质是一个混乱的来源,给JavaScript(和其他高等级语言)开发者可以不去关心内存管理的错误印象。这是一个很大的错误

    内存泄漏

    内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

    内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。

    内存生命周期

    无论使用哪一种编程语言,内存的生命周期几乎总是一模一样的 分配内存、使用内存、释放内存。 在这里我们主要讨论内存的回收。

    引用计数垃圾回收

    这是最简单的垃圾回收算法。一个对象在没有被其他的引用指向的时候就被认为“可回收的”。

    对JS引用类型不熟悉的请先百度引用类型,理解了值类型(基本类型)和引用类型之后才能理解下面的代码

    var obj1 = {
      obj2: {
        x: 1
      }
    };
    //2个对象被创建。 obj2被obj1引用,并且作为obj1的属性存在。这里并没有可以被回收的。
    //obj1和obj2都指向了{obj2: {x: 1}}这个对象,这个示例中用`原来的对象`来表示这个对象。
    
    var obj3 = obj1;  //obj3也引用了obj1指向的对象。
    obj1 = 1; // obj1不引用原来的对象了。此时原来的对象只有obj3在引用。
    
    var obj4 = obj3.obj2; //obj4引用了obj3对象的obj2属性,
    //此时obj2对象有2个引用,一个是作为obj3的一个属性,一个是作为obj4变量。
    
    obj3 = 1;
    // 咦,obj1原来对象只有obj3在引用,现在obj3也没用在引用了。
    // obj1原来的对象就沦为了一只单身狗,于是乎抓狗大队就来带走了它。(好吧、其实内存就可以被回收了)。
    // 然而  obj2对象依然有人爱(被obj4引用)。所以obj2的内存就不会被垃圾回收。
    
    obj4 = null;
    // obj2内心在呐喊:小姐姐不要离开我 QOQ。现在obj2也没有被引用了,引用计数就是0
    也就是可以被回收了。
    复制代码

    简而言之~,如果内存有人爱,那就不会被回收。如果是单身狗的话,[手动滑稽]。

    循环引用会造成麻烦

    引用计数在涉及循环引用的时候有一个缺陷。在下面的例子中,创建了2个对象,并且相互引用,这样创建了一个循环。因此他们实际上是无用的,可以被释放。然而引用计数算法考虑到2个对象中的每一个至少被引用了一次,因此都不可以被回收。

    function f() {
      var o1 = {};
      var o2 = {};
      o1.p = o2;
      o2.p = o1;
    }
    
    f();
    复制代码

    单身狗心里千万头草泥马在奔腾(我特么也会自己牵自己手啊,我也会假装情侣拍照啊)

    标记清除算法

    别以为你假装不是单身狗就拿你没办法了,这个算法确定了对象是否可以被达到。 这个算法包含了以下步骤:

    1. 从‘根’上生成一个列表(通常是以全局变量为根)。在JS中window对象可以作为一个'根'
    2. 所有的'根'都被标记为活跃的,所有的子变量也被递归检查。能够从'根'上到达的都不会被认为成垃圾。
    3. 没有被标记为活跃的就被认为成垃圾。这些内存就会被释放。

    上图就是标记清除的动作。

    在之前的例子中,虽然两个变量相互引用,但在函数执行完之后,这个两个变量都没有被window对象上的任何对象所引用。因此,他们会被认为不可到达的。

    4种常见的JS内存泄漏

    1:全局变量 JavaScript用一个有趣的方式管理未被声明的变量:对未声明的变量的引用在全局对象里创建一个新的变量。在浏览器的情况下,这个全局对象是window。换句话说:

    function foo(arg) {
      bar = 'some text';
    }
    //等同于
    function foo(arg) {
      window.bar = 'some text';
    }
    
    复制代码

    如果bar被假定只在foo函数的作用域里引用,但是你忘记了使用var去声明它,一个意外的全局变量就被声明了。 在这个例子里,泄漏一个简单的字符并不会造成很大的伤害,但是它确实有可能变得更糟。 有时有会通过this来创建意外的全局变量。

    为了防止这些问题发生,可以在你的JaveScript文件开头使用'use strict';。这个可以使用一种严格的模式解析JavaScript来阻止意外的全局变量。

    如果有时全局变量被用于暂时储存大量的数据或者涉及到的信息,那么在使用完之后应该指定为null或者重新分配

    2:被遗忘的定时器或者回调 还是来个栗子吧,定时器可能会产生对不再需要的DOM节点或者数据的引用。

    var serverData = loadData();
    setInterval(function() {
        var renderer = document.getElementById('renderer');
        if(renderer) {
            renderer.innerHTML = JSON.stringify(serverData);
        }
    }, 5000); //每五秒会执行一次
    复制代码

    renderer对象在将来有可能被移除,让interval没有存在的意义。然而当处理器interval仍然起作用时,renderer并不能被回收(interval在对象被移除时需要被停止),如果interval不能被回收,它的依赖也不可能被回收。这就意味着serverData,大概保存了大量的数据,也不可能被回收。 如今,大部分的浏览器都能而且会在对象变得不可到达的时候回收观察处理器,甚至监听器没有被明确的移除掉。在对象被处理之前,最好也要显式地删除这些观察者。

    var element = document.getElementById('launch-button');
    var counter = 0;
    
    function onClick(event) {
       counter++;
       element.innerHtml = 'text ' + counter;
    }
    
    element.addEventListener('click', onClick);
    // 做一些其他的事情
    
    element.removeEventListener('click', onClick);
    element.parentNode.removeChild(element);
    复制代码

    如今,现在的浏览器(包括IE和Edge)使用现代的垃圾回收算法,可以立即发现并处理这些循环引用。换句话说,在一个节点删除之前也不是必须要调用removeEventListener。 框架和插件例如jQuqery在处理节点(当使用具体的api的时候)之前会移除监听器。这个是插件内部的处理可以确保不会产生内存泄漏,甚至运行在有问题的浏览器上(哈哈哈 说的就是IE6)。

    3: 闭包 闭包是javascript开发的一个关键方面,一个内部函数使用了外部(封闭)函数的变量。由于JavaScript运行的细节,它可能以下面的方式造成内存泄漏:

    var theThing = null;
    
    var replaceThing = function () {
      var originalThing = theThing;
      var unused = function () {
        if (originalThing) console.log('hi')  //引用了originalThing
      };
      
      theThing = {
        longStr: new Array(1000000).jojin('*'),
        someMethod: function (){
          console.log('message');  
        }
      };
    };
    
    setInterval(replaceThing,1000);
    复制代码

    这些代码做了一件事情,每次relaceThing被调用,theThing获得一个包含大量数据和新的闭包(someMethod)的对象。同时,变量unused引用了originalThingtheThing是上一次函数被调用时产生的)。已经有点困惑了吧?最重要的事情是一旦为同一父域中的作用域产生闭包,则该作用域是共享的。

    在这个案例中,someMethodunused共享闭包作用域,unused引用了originalThing,这阻止了originalThing的回收,尽管unused不会被使用,但是someMethod依然可以通过theThing来访问replaceThing作用域外的变量(例如某些全局的)。

    4:来自DOM的引用 在你要重复的操作DOM节点的时候,存储DOM节点是十分有用的。但是在你需要移除DOM节点的时候,需要确保移除DOM tree和代码中储存的引用。

    var element = {
      image: document.getElementById('image'),
      button: document.getElementById('button')
    };
    
    //Do some stuff
    
    document.body.removeChild(document.getElementById('image'));
    //这个时候  虽然从dom tree中移除了id为image的节点,但是还保留了一个对该节点的引用。于是image仍然不能被回收。
    复制代码

    当涉及到DOM树内部或子节点时,需要考虑额外的考虑因素。例如,你在JavaScript中保持对某个表格的特定单元格的引用。有一天你决定从DOM中移除表格但是保留了对单元格的引用。你也许会认为除了单元格其他的都会被回收。实际并不是这样的:单元格是表格的一个子节点,子节点保持了对父节点的引用。确切的说,JS代码中对单元格的引用造成了整个表格被留在内存中了,所以在移除有被引用的节点时候要移除其子节点。

    总结

    1. 小心使用全局变量,尽量不要使用全局变量来存储大量数据,如果是暂时使用,要在使用完成之后手动指定为null或者重新分配
    2. 如果使用了定时器,在无用的时候要记得清除。如果为DOM节点绑定了事件监听器,在移除节点时要先注销事件监听器。
    3. 小心闭包的使用。如果掌握不好,至少在使用大量数据的时候仔细考量。在使用递归的时候也要非常小心(例如用canvas做小游戏)。
    4. 在移除DOM节点的时候要确保在代码中没有对节点的引用,这样才能完全的移除节点。在移除父节点之前要先移除子节点。
    展开全文
  • 内存管理操作系统最核心功能之一。内存主要用来存储系统和应用程序指令、数据、缓存等内存映射我们通常所说内存容量指的是物理内存。物理内存也称为主存,大多数计算机用主存都动态随机访问内...

    目录

    内存映射

    进程如何访问内存?

    进程的用户态和内核态

    什么是内存映射?

    什么是缺页异常

    MMU 如何管理内存

    多级页表和大页

    虚拟内存空间分布

    内存分配与回收

    如何查看内存使用情况

    问题:如何统计所有进程的物理内存使用量?


    内存管理也是操作系统最核心的功能之一。内存主要用来存储系统和应用程序的指令、数据、缓存等

    内存映射

    我们通常所说的内存容量指的是物理内存。物理内存也称为主存,大多数计算机用的主存都是动态随机访问内存(DRAM)。只有内核才可以直接访问物理内存。

    进程如何访问内存?

    Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。进程访问内存其实访问的虚拟内存。

    虚拟地址空间的内部又被分为内核空间用户空间

    不同字长(也就是单个CPU指令可以处理数据的最大长度)的处理器,地址空间的范围也不同。比如最常见的 32 位和 64 位系统

    ac1eba2bbcd5b1ff2744270ca1eb2a0b.png
    • 32位系统的内核空间占用 1G,位于最高处,剩下的3G是用户空间。
    • 64位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的

    进程的用户态和内核态

    进程在用户态时,只能访问用户空间内存;只有进入内核态后,才可以访问内核空间内存
    虽然每个进程的地址空间
    都包含了内核空间,但这些内核空间,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。

    每个进程都有一个这么大的地址空间,那么所有进程的虚拟内存加起来,自然要比实际的物理内存大得多。所以,并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。

    什么是内存映射?

    其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系。

    20f5a552747bbc1425ab9c69623db710.png

    页表实际上存储在 CPU 的内存管理单元 MMU中,这样,正常情况下,处理器就可以直接通过硬件,找出要访问的内存。

    什么是缺页异常

    当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。

    TLB(Translation Lookaside Buffer,转译后备缓冲器)会影响 CPU 的内存访问性能,TLB 其实就是 MMU 中页表的高速缓存。由于进程的虚拟地址空间是独立的,而 TLB 的访问速度又比 MMU 快得多,所以,通过减少进程的上下文切换,减少TLB的刷新次数,就可以提高TLB 缓存的使用率,进而提高CPU的内存访问性能。

    CPU首先得到的是虚拟地址,必须将虚拟地址转换为物理地址才能进行数据访问。这个过程先查TLB,若TLB命中,则得到了物理地址,进行步骤2;否则,继续查找主存中的页表。若仍不命中,则引发缺页中断进行调页。调页过程又可能引发驻留集页替换和页回写过程。 2.得到物理地址之后,首先访问cache,若命中,CPU直接从cache中读取相应物理地址中的内容;若不命中,则访问主存读取数据,并且更新cache,此过程又可能引发cache的行替换和回写过程。

    MMU全称就是内存管理单元,管理地址映射关系(也就是页表)。但MMU的性能跟CPU比还是不够快,所以又有了TLB。TLB实际上是MMU的一部分,把页表缓存起来以提升性能。

    MMU 如何管理内存

    MMU 并不以字节为单位来管理内存,而是规定了一个内存映射的最小单位,也就是页,通常是 4 KB大小。这样,每一次内存映射,都需要关联 4 KB 或者 4KB 整数倍的内存空间。

    多级页表和大页

    页的大小只有4 KB ,导致的另一个问题就是,整个页表会变得非常大。比方说,仅 32 位系统就需要 100 多万个页表项(4GB/4KB),才可以实现整个地址空间的映射。为了解决页表项过多的问题,Linux 提供了两种机制,也就是多级页表和大页(HugePage)

    • 多级页表

    把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移。由于虚拟内存空间通常只用了很少一部分,那么,多级页表就只保存这些使用中的区块,这样就可以大大地减少页表的项数。

    Linux 用的正是四级页表来管理内存页,如下图所示,虚拟地址被分为5个部分,前4个表项用于选择页,而最后一个索引表示页内偏移。

    d76ff86b82431d71aa4491e90c4af18c.png
    • 大页

    比普通页更大的内存块,常见的大小有 2MB 和 1GB。大页通常用在使用大量内存的进程上,比如 Oracle、DPDK 等。GP是否可以也使用大页呢?


    虚拟内存空间分布

    在页表的映射下,进程就可以通过虚拟地址来访问物理内存了。那么具体到一个Linux 进程中,这些内存又是怎么使用的呢?

    3.1 虚拟内存空间的分布情况

    以32位系统为例

    52239c2159fe3a1a69300804042fafde.png


    1. 只读段,包括
    代码和常量等。
    2. 数据段,包括
    全局变量等。
    3. 堆,包括
    动态分配的内存,从低地址开始向上增长
    4. 文件映射段,包括动态库、共享内存等,从
    高地址开始向下增长
    5. 栈,包括
    局部变量函数调用的上下文等。栈的大小是固定的,一般是 8 MB。

    堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc()或者 mmap() ,就可以分别在文件映射段动态分配内存。64位系统的内存分布也类似,只不过内存空间要大得多。

    内存分配与回收

    malloc() 是 C 标准库提供的内存分配函数,对应到系统调用上,有两种实现方式,即 brk() 和mmap()。对小块内存(小于128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。而大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块
    空闲内存分配出去。

    这两种方式,自然各有优缺点。

    • brk() 方式的缓存,可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片
    • mmap() 方式分配的内存,会在释放时直接归还系统,所以每次 mmap 都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是malloc 只对大块内存使用 mmap 的原因。如果太小的内存也使用mmap分配内核的管理负担会更大。

    来个例子理解一下

    servers]# strace cat /dev/null execve("/bin/cat", ["cat", "/dev/null"], [/* 29 vars */]) = 0brk(0)                                  = 0x1e0d000mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f42ef607000access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)open("/etc/ld.so.cache", O_RDONLY)      = 3fstat(3, {st_mode=S_IFREG|0644, st_size=77682, ...}) = 0mmap(NULL, 77682, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f42ef5f4000close(3)                                = 0open("/lib64/libc.so.6", O_RDONLY)      = 3read(3, "177ELF21130000000030>01000p3563412323000"..., 832) = 832fstat(3, {st_mode=S_IFREG|0755, st_size=1926760, ...}) = 0mmap(0x339ae00000, 3750152, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x339ae00000mprotect(0x339af8a000, 2097152, PROT_NONE) = 0mmap(0x339b18a000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x18a000) = 0x339b18a000mmap(0x339b18f000, 18696, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x339b18f000close(3)                                = 0mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f42ef5f3000mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f42ef5f2000mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f42ef5f1000arch_prctl(ARCH_SET_FS, 0x7f42ef5f2700) = 0mprotect(0x339b18a000, 16384, PROT_READ) = 0mprotect(0x339a81f000, 4096, PROT_READ) = 0munmap(0x7f42ef5f4000, 77682)           = 0brk(0)                                  = 0x1e0d000brk(0x1e2e000)                          = 0x1e2e000open("/usr/lib/locale/locale-archive", O_RDONLY) = 3fstat(3, {st_mode=S_IFREG|0644, st_size=99158576, ...}) = 0mmap(NULL, 99158576, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f42e9760000close(3)                                = 0fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 4), ...}) = 0open("/dev/null", O_RDONLY)             = 3fstat(3, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0read(3, "", 32768)                      = 0close(3)                                = 0close(1)                                = 0close(2)                                = 0exit_group(0)                           = ?

    当这两种调用发生后,其实并没有真正分配内存。这些内存,都只在首次访问时才分配,也就是通过缺页异常进入内核中,再由内核来分配内存。

    Linux 使用伙伴系统来管理内存分配。前面我们提到过,这些内存在MMU中以页为单位进行管理,伙伴系统也一样,以页为单位来管理内存,并且会通过相邻页的合并,减少内存碎片化(比如brk方式造成的内存碎片)

    比页(4KB)还小的内存如何分配?

    如果为它们也分配单独的页,那就太浪费内存了

    所以

    • 用户空间,malloc 通过 brk() 分配的内存,在释放时并不立即归还系统,而是缓存起来重复利用
    • 内核空间,Linux 则通过 slab 分配器来管理小内存。你可以把slab 看成构建在伙伴系统上的一个缓存,主要作用就是分配并释放内核中的小对象。(malloc本身不会使用slab只有内核中使用kmalloc才回通过slab分配内存) 因此需要注意对于内核空间才会用slab 分配内存

    对内存来说,如果只分配而不释放,就会造成内存泄漏,甚至会耗尽系统内存。所以,在应用程序用完内存后,还需要调用 free() 或 unmap() ,来释放这些不用的内存。

    内存紧张时触发内存回收机制

    • 回收缓存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面;
    • 回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中;

    Swap 其实就是把一块磁盘空间当成内存来用。它可以把进程暂时不用的数据存储到磁盘中(这个过程称为换出),当进程访问这些内存时,再从磁盘读取这些数据到内存中(这个过程称为换入)。
    Swap 把系统的可用内存变大了。不过要注意,通常
    只在内存不足时,才会发生 Swap 交换。并且由于磁盘读写的速度远比内存慢,Swap 会导致严重的内存性能问题

    • 杀死进程,内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程

    OOM killer是内核的一种保护机制。它监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分:

    • 一个进程消耗的内存越大,oom_score 就越大;
    • 一个进程运行占用的 CPU 越多,oom_score 就越小。

    这样,进程的 oom_score 越大,代表消耗的内存越多,也就越容易被 OOM 杀死,从而可以更好保护系统。
    管理员可以通过 /proc 文件系统,手动设置进程的 oom_adj ,从而调整进程的 oom_score。oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中
    -17 表示禁止 OOM

    echo -16 > /proc/$(pidof watch)/oom_adj

    如何查看内存使用情况

    free

    第一列,total 是总内存大小;
    第二列,used 是已使用内存的大小,包含了共享内存;
    第三列,free 是未使用内存的大小;
    第四列,shared 是共享内存的大小;
    第五列,buff/cache 是缓存和缓冲区的大小;

    top

    主要列含义:
    VIRT 是进程虚拟内存的大小,只要是
    进程申请过的内存,即便还没有真正分配物理内存,也会计算在内。
    RES 是常驻内存的大小,也就是进程
    实际使用的物理内存大小,但不包括 Swap 和共享内存
    SHR 是共享内存的大小,比如与其他进程共同使用的
    共享内存、加载的动态链接库以及程序的代码段等。
    %MEM 是进程使用物理内存占系统总内存的百分比。


    第一,虚拟内存通常并不会全部分配物理内存。
    第二,共享内存 SHR 并不一定是共享的,比方说,程序的代码段、非共享的动态链接库,也都算在 SHR 里。当然,SHR 也包括了
    进程间真正共享的内存。所以在计算多个进程的内存使用时,不要把所有进程的 SHR 直接相加得出结果。

    问题:如何统计所有进程的物理内存使用量?

    The “proportional set size” (PSS) of a process is the count of pagesit has in memory, where each page is divided by the number ofprocesses sharing it. So if a process has 1000 pages all to itself, and1000 shared with one other process, its PSS will be 1500.

    每个进程的 PSS ,是指把共享内存平分到各个进程后,再加上进程本身的非共享内存大小的和。就像文档中的这个例子,一个进程的非共享内存为 1000 页,它和另一个进程的共享进程也是 1000 页,那么它的 PSS=1000/2+1000=1500 页。这样,你就可以直接累加 PSS ,不用担心共享内存重复计算的问题了。

    grep Pss /proc/[1-9]*/smaps | awk '{total+=$2}; END {printf "%d kB", total }'
    展开全文
  • 1、内存管理基础知识(指令工作原理、地址转化、程序运行过程)思维导图什么是内存?指令的工作原理装入模块三种实现绝对装入可重定位装入动态重定位程序运行过程链接三种方式 思维导图 什么是内存? 内存...
  • 内存管理(13)zone初始化1中说到free_area_init_node函数,接下来主要做的工作如下:1.计算有效页框数,总页框数2.初始化内存节点管理数据结构pgdat2.初始化zone管理数据结构其中包含空闲链表3.初始化zone下的管理的...
  • 内存管理操作系统最核心功能之一。内存主要用来存储系统和应用程序指令、数据、缓存等内存映射我们通常所说内存容量指的是物理内存。物理内存也称为主存,大多数计算机用主存都动态随机访问内...
  • JAVA堆内存管理是影响性能主要因素之一。 堆内存溢出JAVA项目非常常见故障,在解决该问题之前,必须先了解下JAVA堆内存怎么工作的。 先看下JAVA堆内存如何划分,如图: JVM内存划分为堆内存和非堆内存...
  • JVM:自动内存管理之Java内存区域与内存溢出

    千次阅读 多人点赞 2021-01-07 12:03:22
    本博客主要参考周志明老师《深入理解Java...程序计数器一块较小的内存空间,可以看作当前线程所执行字节码行号指示器,在Java虚拟机概念模型里,字节码解释器工作时就是通过改变这个计数器值来选取下.
  • Java内存模型的主要目标定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。 这里所讲的主内存工作内存与Java内存区域中的Java堆、栈、方法区等并不是同一层次的...
  • C++基础-内存管理

    2020-02-16 16:33:11
    本文主要是堆、栈和RAII,属于内存管理的范畴吧。说一下自己的心得体会。 堆和栈的概念 在早期学生时期写代码几乎没有内存管理的概念的,什么时候该用new/malloc(堆),什么时候用局部变量(栈),完全看心情的...
  • OC中的内存管理主要由三种方式ARC(自动内存计数),手动内存计数,内存池; 1)ARC:这种方式和java类似,在你程序执行中,始终有一个高人在你背后准确帮你收拾垃圾,你不用考虑它什么时候开始工作,怎么样...
  • 六、内存模型6.1、内存模型与运行时数据区Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同数据区域。Java内存模型的主要目标定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从...
  • JVM调优一个系统而又复杂过程,由于Java虚拟机自动管理内存,在大多数情况下,我们基本上不用去调整JVM内存分配,因为一些初始化参数已经可以保证应用服务正常稳定地工作。但是当有性能问题时候该怎么去调优,...
  • 内存管理与垃圾回收

    2017-02-27 15:12:31
    首先我们需要理解,内存是什么。简单来讲,内存存储了计算机运行过程需要全部数据,也就是计算机正在使用全部数据。我们需要合理使用内存,防止内存被大量无用数据占用,同时也要防止访问和修改与当前程序...
  • JVM 如何高效进行内存管理? 为什么需要有元空间,它又涉及什么问题? 带着这 3 个问题,我们开始今天学习,关于内存划分知识我希望在本课时你能够理解就可以,不需要死记硬背,因为在后面课时我们会经常...
  • 操作系统(内存管理)

    热门讨论 2009-09-20 12:55:25
    在很多脚本语言中,您不必担心内存如何管理的,这并不能使得内存管理的重要性有一点点降低。对实际编程来说,理解您的内存管理器的能力与局限性至关重要。在大部分系统语言中,比如 C 和 C++,您必须进行内存管理...
  • #Memcached那些事 本文不是为了介绍Memcached是什么,而是讨论在使用Memcached时候你必须知道一些事情。以便于方便排查和更好使用Memcached。本文主要围绕两个方面来讨论这个话题:Memcached使用和监控。 ##...
  • 1.1 为什么研究内存管理 1.1.1 程序:数据 + 算法 写程序为了解决某个问题,生活中的问题最终被计算机抽象为控制或运算 CPU中的主要构件就是运算器和控制器,本质上一堆组合逻辑电路,表现为机器指令集 一个...
  • 第四章 存储器管理 作业 为什么要配置层次式存储器? ...使用多级存储系统, 主要是为了解决CPU速度与存储器速度不匹配问题, 就CPU与内存而言, 内存的速度远慢于CPU, 在CPU与内存间利用Cache
  • Java语言,我想大家都不陌生了,本文主要介绍java程序语言在代码的内存管理方面用法,让我们一起来看。   本文通过几个方面,来介绍Java代码的内存管理。 有代码,GC根本就回收不了,直接系统挂...
  • 当然这目前 linux 现状,那么为什么linux会发展成这个样子,这当然程序员对程序要求决定, 为了满足这些要求,提出了 进程地址空间抽象(1) , 其中 硬件上增加了新模块MMU(2), 软件上根据MMU使用手册(3)...
  • Java内存模型的主要目标定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。 这里所讲的主内存工作内存与Java内存区域中的Java堆、栈、方法区等并不是同一层次的...
  • 前记这段时间,工作当中杂事比较多,有点像充当产品经理,去给算法业务同学去安利我们最近完成一些东西,感觉自己几乎没啥提升,希望这样日子快点过去,公众号也落下了,主要原因最近事比较多,又加上还迷上...
  • 什么是linux内核?包括哪些模块?

    千次阅读 2020-02-04 22:06:54
    计算机硬件含有外围设备、处理器、内存、硬盘和...内存管理主要完成如何合理有效地管理整个系统物理内存,同时快速响应内核各个子系统对内存分配请求。Linux内存管理支持虚拟内存,而多余出这部分内存就是...
  • 由于JVM运行程序实体线程,而每个线程创建时都会为其创建一个工作内存(有些地方称之为栈空间)用于存储线程私有数据,线程与主内存变量操作必须通过工作内存间接完成,主要过程讲变量从主内存拷贝到每...
  • 以下关于Java异常说法不正确的( )。更多相关问题737-300/400APU启动前的...在辅导员队伍建设的过程中,提出了“三化”包括()以下属于OTN网的主要优点有()简述他律道德与自律道德的主要区别。为什么脱硫剂从塔的...
  • 5、 在存储管理中分页与分段的主要区别是什么?分页与分段两种方法中,哪个更易于实现共享,为什么? 6、 在分页式存储管理中,什么叫快表,说明其工作原理和过程,画出具有快表的地址变换机构。 7、 系统抖动 8、 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 698
精华内容 279
关键字:

内存管理的主要工作是什么