精华内容
下载资源
问答
  • 深入理解Java虚拟机-Java内存区域与内存溢出异常

    万次阅读 多人点赞 2020-01-03 21:42:24
    Java与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 虚拟机所管理的内存被划分为如下几个区域:

    在这里插入图片描述

    程序计数器(线程私有)

    程序计数器是一块较小的内存区域,可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。「属于线程私有的内存区域」

    附加:

    1. 当前线程所执行的字节码行号指示器
    2. 每个线程都有一个自己的PC计数器。
    3. 线程私有的,生命周期与线程相同,随JVM启动而生,JVM关闭而死。
    4. 线程执行Java方法时,记录其正在执行的虚拟机字节码指令地址
    5. 线程执行Native方法时,计数器记录为(Undefined)。
    6. 唯一在Java虚拟机规范中没有规定任何OutOfMemoryError情况区域。

    Java虚拟机栈(线程私有)

    线程私有内存空间,它的生命周期和线程相同。线程执行期间,每个方法被执行时,都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法从被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。「属于线程私有的内存区域」

    注意:下面的内容为附加内容,对Java虚拟机栈进行详细说明,感兴趣的小伙伴可以有针对性的阅读

    下面依次解释栈帧里的四种组成元素的具体结构和功能:

    局部变量表

    局部变量表局部变量表是 Java 虚拟机栈的一部分,是一组变量值的存储空间,用于存储方法参数局部变量。 在 Class 文件的方法表的 Code 属性的 max_locals 指定了该方法所需局部变量表的最大容量

    局部变量表在编译期间分配内存空间,可以存放编译期的各种变量类型:

    1. 基本数据类型boolean, byte, char, short, int, float, long, double8种;
    2. 对象引用类型reference,指向对象起始地址引用指针;不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置
    3. 返回地址类型returnAddress,返回地址的类型。指向了一条字节码指令的地址

    变量槽(Variable Slot):

    变量槽局部变量表最小单位,规定大小为32位。对于64位的longdouble变量而言,虚拟机会为其分配两个连续Slot空间。

    操作数栈

    操作数栈Operand Stack)也常称为操作栈,是一个后入先出栈。在 Class 文件的 Code 属性的 max_stacks 指定了执行过程中最大的栈深度。Java虚拟机的解释执行引擎被称为基于栈的执行引擎 ,其中所指的就是指-操作数栈

    1. 局部变量表一样,操作数栈也是一个以32字长为单位的数组。
    2. 虚拟机在操作数栈中可存储的数据类型intlongfloatdoublereferencereturnType等类型 (对于byteshort以及char类型的值在压入到操作数栈之前,也会被转换为int)。
    3. 局部变量表不同的是,它不是通过索引来访问,而是通过标准的栈操作压栈出栈来访问。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。

    虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈

    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_0iload_1 将存储在局部变量表中索引为01的整数压入操作数栈中,其后iadd指令从操作数栈中弹出那两个整数相加,再将结果压入操作数栈。第四条指令istore_2则从操作数栈中弹出结果,并把它存储到局部变量表索引为2的位置。

    下图详细表述了这个过程中局部变量表操作数栈的状态变化(图中没有使用的局部变量表操作数栈区域以空白表示)。

    在这里插入图片描述

    动态链接

    每个栈帧都包含一个指向运行时常量池中所属的方法引用,持有这个引用是为了支持方法调用过程中的动态链接

    Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用:

    1. 静态解析:一部分会在类加载阶段或第一次使用的时候转化为直接引用(如finalstatic域等),称为静态解析
    2. 动态解析:另一部分将在每一次的运行期间转化为直接引用,称为动态链接
    方法返回地址

    当一个方法开始执行以后,只有两种方法可以退出当前方法:

    1. 正常返回:当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。
    2. 异常返回:当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。

    当一个方法返回时,可能依次进行以下3个操作:

    1. 恢复上层方法局部变量表操作数栈
    2. 返回值压入调用者栈帧操作数栈
    3. PC计数器的值指向下一条方法指令位置。
    小结

    注意:在Java虚拟机规范中,对这个区域规定了两种异常。

    其一:如果当前线程请求的栈深度大于虚拟机栈所允许的深度,将会抛出 StackOverflowError 异常(在虚拟机栈不允许动态扩展的情况下);

    其二:如果扩展时无法申请到足够的内存空间,就会抛出 OutOfMemoryError 异常。

    本地方法栈(线程私有)

    本地方法栈Java虚拟机栈发挥的作用非常相似,主要区别是Java虚拟机栈执行的是Java方法服务,而本地方法栈执行Native方法服务(通常用C编写)。

    有些虚拟机发行版本(譬如Sun HotSpot虚拟机)直接将本地方法栈Java虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会抛出StackOverflowErrorOutOfMemoryError异常。

    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有垃圾收集机制,但内存溢出异常离我们仍然并不遥远,本章只是讲解了各个区域出现内存溢出异常的原因。

    展开全文
  • 为什么32位cpu只支持4G内存

    千次阅读 2019-04-10 10:08:25
    理解基本概念 首先内存是cpu处理数据的临时存储站,cpu每次解析的数据(指令)都是内存传来的,数据...在微机的内存中,每个基本单位都被赋予一个惟一的序号,这个序号称为地址,而内存基本单位是Byte(这一点对后面...

    理解基本概念

    首先内存是cpu处理数据的临时存储站,cpu每次解析的数据(指令)都是内存传来的,数据流向:硬盘->内存->L3->L2->L1->L0(用作计算的CPU内核),L1-L3是一般操作系统都支持的多级缓存,目的是解决CPU和内存的速度不匹配问题。

    在微机的内存中,每个基本单位都被赋予一个惟一的序号,这个序号称为地址,而内存的基本单位是Byte(这一点对后面计算4GB这个值很关键)。
    CPU里有三根线:控制线,地址线,数据线,地址线传递数据的地址,再根据地址通过数据线去取数据。

    换算过程

    32位cpu的意思就是cpu每次解析数据是32bits,也就是4B,那么支持的地址就是4B长度的,也就是支持的最大内存是2^32 (个地址)*1Byte(内存基本数据单位)=4GB,地址空间范围为0~2^32-1 (-1是因为全部位数都为1时归0)=2^32个地址
    因此32位cpu最高寻址空间支持4GB的数据(不考虑使用PAE——物理地址扩展技术)。

    但是地址长度不代表真正能支持这么大的寻址空间,决定寻址空间的还在于地址线,地址线是用来传输地址信息用的,因此地址线能传多长的数据(考虑CPU,这个长度小于等于CPU处理数据位数),才决定寻址空间也就是支持的内存大小。

    展开全文
  • 内存管理

    千次阅读 2010-11-20 23:25:00
    内存管理 内存

    作者:CppExplore 网址:http://www.cppblog.com/CppExplore/
    服务器设计人员在一段时间的摸索后,都会发现:服务器性能的关键在于内存。从收包到解析,到消息内存的申请,到session结构内存的申请都要小心处理,尽量减少内存数据copy,减少内存动态申请,减少内存检索。为达到这个目的,不同的地方有不同的方法,比如常见的包解析,使用缓冲区偏移以及长度来标识包内字段信息;内存使用量固定的系统,系统启动就申请好所有需要的内存,初始化好,等待使用的时候直接使用;基于license控制的系统,根据license的数量,一次性申请固定数量内存等......。本文不再总结这些特性方案,重点说下常见的通用的内存池缓存技术。
        内存池可有效降低动态申请内存的次数,减少与内核态的交互,提升系统性能,减少内存碎片,增加内存空间使用率,避免内存泄漏的可能性,这么多的优点,没有理由不在系统中使用该技术。
    为了给内存池技术寻找基石,先从低层的内存管理看起。
    硬件层略掉不谈,可回顾《操作系统》。
    一、linux内存管理策略
        linux低层采用三层结构,实际使用中可以方便映射到两层或者三层结构,以适用不同的硬件结构。最下层的申请内存函数get_free_page。之上有三种类型的内存分配函数
    (1)kmalloc类型。内核进程使用,基于slab技术,用于管理小于内存页的内存申请。思想出发点和应用层面的内存缓冲池同出一辙。但它针对内核结构,特别处理,应用场景固定,不考虑释放。不再深入探讨。
    (2)vmalloc类型。内核进程使用。用于申请不连续内存。
    (3)brk/mmap类型。用户进程使用。malloc/free实现的基础。
    有关详细内容,推荐http://www.kerneltravel.net/journal/v/mem.htmhttp://www.kerneltravel.net上有不少内核相关知识。
    二、malloc系统的内存管理策略
        malloc系统有自己的内存池管理策略,malloc的时候,检测池中是否有足够内存,有则直接分配,无则从内存中调用brk/mmap函数分配,一般小于等于128k(可设置)的内存,使用brk函数,此时堆向上(有人有的硬件或系统向下)增长,大于128k的内存使用mmap函数申请,此时堆的位置任意,无固定增长方向。free的时候,检测标记是否是mmap申请,是则调用unmmap归还给操作系统,非则检测堆顶是否有大于128k的空间,有则通过brk归还给操作系统,无则标记未使用,仍在glibc的管理下。glibc为申请的内存存储多余的结构用于管理,因此即使是malloc(0),也会申请出内存(一般16字节,依赖于malloc的实现方式),在应用程序层面,malloc(0)申请出的内存大小是0,因为malloc返回的时候在实际的内存地址上加了16个字节偏移,而c99标准则规定malloc(0)的返回行为未定义。除了内存块头域,malloc系统还有红黑树结构保存内存块信息,不同的实现又有不同的分配策略。频繁直接调用malloc,会增加内存碎片,增加和内核态交互的可能性,降低系统性能。linux下的glibc多为Doug Lea实现,有兴趣的可以去baidu、google。
    三、应用层面的内存池管理
        跳过malloc,直接基于brk/mmap实现内存池,原理上是可行的,但实际中这种实现要追逐内核函数的升级,增加了维护成本,另增加了移植性的困难,据说squid的内存池是基于brk的,本人尚未阅读squid源码(了解磁盘缓存的最佳代码,以后再详细阅读),不敢妄言。本文后面的讨论的内存池都是基于malloc(或者new)实现。我们可以将内存池的实现分两个类别来讨论。
    1、不定长内存池。典型的实现有apr_pool、obstack。优点是不需要为不同的数据类型创建不同的内存池,缺点是造成分配出的内存不能回收到池中。这是由于这种方案以session为粒度,以业务处理的层次性为设计基础。
    (1)apr_pool。apr全称Apache portable Run-time libraries,Apache可移植运行库。可以从http://www.apache.org/网站上下载到。apache以高性能、稳定性著称,它所有模块的内存申请都由内存池模块apr_pool实现。有关apr_pool结构、实现的原理,http://blog.csdn.net/tingya/(apache源码分析类别中的apache内存池实现内幕系列)已经有了详细的讲解,结合自己下载的源码,已经足够了。本人并不推荐去看这个blog和去看详细的代码数据结构以及逻辑。明白apr_pool实现的原理,知道如何使用就足够了。深入细节只能是浪费脑细胞,当然完全凭个人兴趣爱好了。
        这里举例说下简单的使用:
    #include "apr_pools.h"
    #include <stdio.h>
    #include <new>

    int main()
    {
        apr_pool_t *root;
        apr_pool_initialize();//初始化全局分配子(allocator),并为它设置mutext,以用于多线程环境,初始化全局池,指定全局分配

    子的owner是全局池
        apr_pool_create(&root,NULL);//创建根池(默认父池是全局池),根池生命期为进程生存期。分配子默认为全局分配子
        {
            apr_pool_t *child;
            apr_pool_create(&child,root);//创建子池,指定父池为root。分配子默认为父池分配子
            void *pBuff=apr_palloc(child,sizeof(int));//从子池分配内存
            int *pInt=new (pBuff)  int(5);//随便举例下基于已分配内存后,面向对象构造函数的调用。
            printf("pInt=%d/n",*pInt);
            {
                apr_pool_t *grandson;
                apr_pool_create(&grandson,root);
                void *pBuff2=apr_palloc(grandson,sizeof(int));
                int *pInt2=new (pBuff2)  int(15);
                printf("pInt2=%d/n",*pInt2);   

                apr_pool_destroy(grandson);
            }
            apr_pool_destroy(child);//释放子池,将内存归还给分配子
        }
        apr_pool_destroy(root);//释放父池,
        apr_pool_terminate();//释放全局池,释放全局allocator,将内存归还给系统
        return 1;
    }

        apr_pool中主要有3个对象,allocator、pool、block。pool从allocator申请内存,pool销毁的时候把内存归还allocator,allocator销毁的时候把内存归还给系统,allocator有一个owner成员,是一个pool对象,allocator的owner销毁的时候,allocator被销毁。在apr_pool中并无block这个单词出现,这里大家可以把从pool从申请的内存称为block,使用apr_palloc申请block,block只能被申请,没有释放函数,只能等pool销毁的时候才能把内存归还给allocator,用于allocator以后的pool再次申请。
        我给的例子中并没有出现创建allocator的函数,而是使用的默认全局allocator。apr_pool提供了一系列函数操作allocator,可以自己调用这些函数:
    apr_allocator_create
    apr_allocator_destroy
    apr_allocator_alloc
    apr_allocator_free  创建销毁allocator
    apr_allocator_owner_set
    apr_allocator_owner_get  设置获取owner
    apr_allocator_max_free_set 设置pool销毁的时候内存是否直接归还到操作系统的阈值
    apr_allocator_mutex_set
    apr_allocator_mutex_get  设置获取mutex,用于多线程

    另外还有设置清理函数啊等等,不说了。自己去看include里的头文件好了:apr_pool.h和apr_allocator.h两个。源码.c文件里,APR_DECLARE宏声明的函数即是暴露给外部使用的函数。大家也可以仿造Loki(后文将介绍Loki)写个顶层类重载operator new操作子,其中调用apr_palloc,使用到的数据结构继承该类,则自动从pool中申请内存,如要完善的地方很多,自行去研究吧。
        可以看出来apr_pool的一个大缺点就是从池中申请的内存不能归还给内存池,只能等pool销毁的时候才能归还。为了弥补这个缺点,apr_pool的实际使用中,可以申请拥有不同生命周期的内存池(类似与上面的例子程序中不同的大括号代表不同的生命周期,实际中,尽可以把大括号中的内容想象成不同的线程中的......),以便尽可能快的回收不再使用的内存。实际中apache也是这么做的。因此apr_pool比较适合用于内存使用的生命期有明显层次的情况。
        至于担心allocator中的内存一旦申请就再也不归还给操作系统(当然最后进程退出的时候你可以调用销毁allocator归还,实际中网络服务程序都是一直运行的,找不到销毁的时机)的问题,就是杞人忧天了,如果在某一时刻,系统占用的内存达到顶峰,意味着以后还会有这种情况。是否能接受这个解释,就看个人的看法和系统的业务需求了,不能接受,就使用其它的内存池。个人觉得apr_pool还是很不错的,很多服务系统的应用场景都适用。
    (2)obstack。glibc自带的内存池。原理与apr_pool相同。详细使用文档可以参阅
    http://www.gnu.org/software/libc/manual/html_node/Obstacks.html。推荐apr_pool,这个就不再多说了。
    (3)AutoFreeAlloc。许式伟的专栏http://blog.csdn.net/xushiweizh/category/265099.aspx
        这个内存池我不看好。这个也属于一个变长的内存池,内存申请类似与apr_pool的pool/block层面,一次申请大内存作为pool,用于block的申请,同样block不回收,等pool销毁的时候直接归还给操作系统。这个内存池的方案,有apr_pool中block不能回收到pool的缺点,没有pool回收到allocator,以供下次继续使用的优点,不支持多线程。适合于单线程,集中使用内存的场景,意义不是很大。

     

    2、定长内存池。典型的实现有LOKI、BOOST。特点是为不同类型的数据结构分别创建内存池,需要内存的时候从相应的内存池中申请内存,优点是可以在使用完毕立即把内存归还池中,可以更为细粒度的控制内存块。
        与变长的相比,这种类型的内存池更加通用,另一方面对于大量不同的数据类型环境中,会浪费不少内存。但一般系统主要的数据结构都不会很多,并且都是重复申请释放使用,这种情况下,定长内存池的这点小缺点可以忽略了。
    (1)Loki::SmallObject。Andrei Alexandrescu的《Modern C++ Design》第四章节已经进行了详细的描述,尽管和当前的loki版本实现有出入,还是了解Loki::SmallObject的最佳文字讲解,结合最新的loki源码,足够了。这里我再罗唆一下。先举例看下使用:
    #include "loki/SmallObj.h"
    class Small:public  Loki::SmallObject<>//继承SmallObject即可,所有都使用默认策略
    {
    public:
     Small(int data):data_(data){}
    private:
     int data_;
    };
    int main()
    {
     Small *obj=new Small(8);
     delete obj;
    }
    使用valgrind执行可以证实new一个obj和多new几次,申请的内存都是4192。可以看出loki在使用层面非常简单。
        loki的内存池分4层,从低向上依次是chunk、FixedAllocator、SmallObjAllocator、SmallObject。
    1)chunk:每个chunk管理一定数量(最大255,char型保存)的block,每个chunk中block的申请和释放,时间复杂度都是o(1),非常快,实现算法非常精巧,boost::pool中也是采用的相同算法。
        这里简单说下这个算法:首次申请一块连续内存,pdata_指向该内存基址,依据block大小,划分成多个连续的block,每个block开头的第一个字节保存该block的顺序号,第一个是1,第二个是2,依次类推。另有一字节变量firstAvailableBlock_存储上次分配出的block序号,开始是0。
        分配block:返回pdata_ +firstAvailableBlock_*blocksize,同时firstAvailableBlock_赋值为该块的序列号。
        回收block:block指针假设为pblock,该块序列号赋值为firstAvailableBlock_,firstAvailableBlock_赋值为(pblock-pdata_ )/blocksize即可。
    2)FixedAllocator:chunk中的block上限是255,不具有通用性,因此封装了一层,称为FixedAllocator,它保存了一个vector<chunk>,消除了单个chunk中block数目的上限限制。
       FixedAllocator中的block申请:FixedAllocator中保存活动的chunk(上次有空闲空间的chunk),申请block的时候如果活动chunk有空闲快,直接申请,否则扫描vector,时间复杂度o(N),同时更新活动chunk。
       FixedAllocator中的回收block:简单想,给定block回收到FixedAllocator,自然要扫描vector,以确认block属于哪个chunk,以便chunk回收。实际实现的时候,Loki针对应用场景进行了优化,一般使用都是批量使用,回收一般和申请顺序相同或者相反,因此FixedAllocator保存上次回收block的chunk指针,每次回收优先匹配这个chunk,匹配不上则以该chunk为中心,向两侧chunk顺序检测。
       FixedAllocator带来的优点:上文提到的消除了block的上限限制。另一方面,可以以chunk为单位,把内存归还给操作系统。实际实现中防止刚释放的内存立即又被申请,是存在两个空闲chunk的时候才回收一个。这个特点,这里暂时归结为优点吧。实际使用中,回收多余内存个人认为是个缺点,意义并不是很大。
       FixedAllocator带来的缺点:很明显,就是申请回收block的时间复杂度。
    3)SmallObjAllocator:截至到FixedAllocator层面blocksize都是定长。因此封装一层适用于任意长度的内存申请。SmallObjAllocator保存了一个FixedAllocator的数组pool_,存储拥有不同block长度的FixedAllocator。《Modern C++ Design》中描述该数组下标和存储的FixedAllocator的block长度无直接关系,从SmallObjAllocator申请以及回收block的时候二分查找找到对应的FixedAllocator再调用相应FixedAllocator的申请或者回收。当前最新版本的loki,已经抛弃了这种做法。当前SmallObjAllocator的构造函数有3个参数:chunksize,maxblocksize,alignsize。数组元素个数取maxblocksize除以alignsize的向上取整。每个FixedAllocator中实际的blocksize是(下标+1)*alignsize。
         SmallObjAllocator中block申请:依据block和alignsize的商直接取到数组pool_下标,使用相应的FixedAllocator申请。
         SmallObjAllocator中回收block:根据block和alignsize的商直接找到相应的FixedAllocator回收。
         优点:差异化各种长度的对象申请,增强了易用性。
         缺点:《Modern C++ Design》中描述增加扫描的时间复杂度,当前版本的loki浪费内存。这也是进一步封装,屏蔽定长申请的细节,带来的负面效应。
    4)SmallObject。暴露给外部使用的一层。该层面秉承了《Modern C++ Design》开始引入的以设计策略类为最终目的,让用户在编译期选择设计策略,而不是提供框架限制用户的设计。这也是引入模版的一个层面。当前版本SmallObject有6个模版参数,第一个是线程策略,紧接着的三个正好是SmallObjAllocator层面的三个构造参数,下面的一个生存期策略,最后的是锁方式。
        这里说下SmallObjAllocator层面的三个默认参数值,分别是4096,256,4。意味着SmallObjAllocator层面有数组(256+4-1)/4=64个,数组存储的FixedAllocator中的chunksize一般都是4096(当4096<=blocksize*255时候)字节(第一个chunk的申请推迟到首次使用的时候),各FixedAllocator中的chunk的blocksize依次是4、8......256,大于256字节的内存申请交给系统的malooc/new管理,数组中FixedAllocator中单个chunk中的blocknum依次是4096/4=824>255取255、255......4096/256=16。如果这不能满足需求,请调用的时候显式赋值。
        当前loki提供了三种线程策略:
    SingleThreaded  单线程
    ObjectLevelLockable  对象级别,一个对象一个锁
    ClassLevelLockable  类级别,一个类一个锁,该类的所有对象共用该锁


    目前只提供了一种锁机制:Mutex
    它的基类SmallObjectBase复写了new/delete操作子,因此直接继承SmallObject就可以象普通的类一样new/delete,并且从内存池分配内存。
        SmalObject中block申请和释放都从一个全局的SmallObjAllocator单例进行。
    评价:chunk层面限制了上限个数,导致了FixedAllocator层面出现,造成申请回收时间复杂度的提高,而以chunk为单位回收内存,在内存池的使用场景下意义并不是很大。SmallObjAllocator为了差异化变长内存的申请,对FixedAllocator进一步封装,引入了内存的浪费,不如去掉这个层面,直接提供给用户层面定长的接口。另一方面,loki已经进行了不少优化,尽可能让block申请释放的时间复杂度在绝大多数情况下都是O(1),而SmallObjAllocator中内存的浪费可以根据alignsize调整,即便是极端情况下,loki将chunk归还给系统又被申请出来,根据chunk中block的最大值看,也比不使用内存池的情况动态申请释放内存的次数减少了1/255。因此,loki是一个非常不错的小巧的内存池。

     

    (2)boost::pool系列。boost的内存池最低层是simple_segregated_storage,类似于Loki中的chunk,在其中申请释放block(boost中把block称为chunk,晕死,这里还是称其为block)采用了和loki的chunk中同样的算法,不同的是simple_segregated_storage使用void*保存block的块序号,loki中使用char,因此boost中的simple_segregated_storage没有255的上限限制,自然也就不需要再其上再封装一层类似与FixedAllocator的层面。另boost没有屏蔽块的大小,直接提供定长的接口给用户,省掉了SmallObjAllocator层面。因此boost的内存池申请释放block的时间复杂度都是O(1)(object_pool和pool_allocator除外),另避免的小内存的浪费,同时boost不能象loki那样在将block归还给内存池的时候根据chunk的空闲数量释放内存归还给系统,只能显式调用释放内存函数或者等内存池销毁的时候,基本上和内存池生命周期内永不释放没什么区别。
        boost的最低层是simple_segregated_storage,主要算法和loki中的chunk一样,不多说了。这里说下影响上层接口的两类实现:add_block/malloc/free、add_ordered_block/malloc/ordered_free,两种低层实现造成boost上层设计的成功与失败,前者效率高,和loki一样直接增加释放,时间复杂度O(1),后者扫描排序,时间复杂度O(n)。
        boost提供了四种内存池模型供使用:pool、object_pool、singleton_pool、pool_allocator/fast_pool_allocator。
    1)pool
    基本的定长内存池

    #include <boost/pool/pool.hpp>
    typedef struct student_st
    {
       
    char name[10];
       
    int age;
    }
    CStudent;
    int main()
    {
       boost::pool
    <> student_pool(sizeof(CStudent));
       CStudent 
    * const obj=(CStudent *)student_pool.malloc();
       student_pool.free(obj);
       
    return 0;
    }

        pool的模版参数只有一个分配子类型,boost提供了两种default_user_allocator_new_delete/default_user_allocator_malloc_free,指明申请释放内存的时候使用new/delete,还是malloc/free,默认是default_user_allocator_new_delete。构造函数有2个参数:nrequested_size,nnext_size。nrequested_size是block的大小(因为void*保存序号,因此boost内置了block的最小值,nrequested_size过小则取内置值),nnext_size是simple_segregated_storage中内存不足的时候,申请的block数量,默认是32。最全面的实例化pool类似这样:boost::pool<boost::default_user_allocator_malloc_free> student_pool(sizeof(CStudent),255);
        pool提供的函数主要有:

    malloc/free  基于add_block/malloc/free实现,高效
    ordered_malloc/ordered_free 基于add_ordered_block/malloc/ordered_free实现,在pool中无任何意义,切勿使用。
    release_memory/purge_memory 前者释放池中未使用内存,后者释放池中所有内存。另池析构也会释放内存

    2)object_pool

    对象内存池,这是最失败的一个内存池设计。
    #include <boost/pool/object_pool.hpp>

    class A{
    public:
       A():data_(
    0){}
    private:
       
    int data_;
    }
    ;
    int main()
    {
       boost::object_pool
    <A> obj_pool;
       A 
    *const pA=obj_pool.construct();
       obj_pool.destroy(pA);
       
    return 0;
    }

        object_pool继承至pool,有两个模版参数,第一个就是对象类型,第二个是分配子类型,默认同pool是default_user_allocator_new_delete。构造函数参数只有nnext_size,意义以及默认值同pool。最全面的实例化object_pool类似这样:boost::pool<A,boost::default_user_allocator_malloc_free> obj_pool(255);
    object_pool提供的函数主要有(继承至父类的略):

    malloc/free 复写pool的malloc/free,add_ordered_block/malloc/ordered_free实现
    construct/destroy 基于本类的malloc/free实现,额外调用默认构造函数和默认析构函数。
    ~object_pool 单独拿出这个说下,若析构的时候有对象未被destroy,可以检测到,释放内存前对其执行destroy
        为什么boost::object_pool要设计成这样?能调用构造函数和析构函数显然不是boost::object_pool类设计的出发点,因为构造函数只能执行默认构造函数(首次发表错误:可以调用任意的构造函数,参见代码文件:boost/pool/detail/pool_construct.inc和boost/pool/detail/pool_construct_simple.inc,感谢eXile指正),近似于无,它的重点是内存释放时候的清理工作,这个工作默认的析构函数就足够了。apr_pool内存池中就可以注册内存清理函数,在释放内存的时刻执行关闭文件描述符、关闭socket等操作。boost::object_pool也想实现同样的功能,因此设计了destroy这个函数,而同时为了防止用户遗漏掉这个调用,而又在内存池析构的时候进行了检测回收。为了这个目的而又不至于析构object_pool的时间复杂度是O(n平方),boost::object_pool付出了沉重的代价,在每次的destoy都执行排序功能,时间复杂度O(n),最后析构的时间复杂度是O(n),同样为了这个目的,从simple_segregated_storage增加了add_ordered_block/ordered_free,pool增加了ordered_malloc/ordered_free等累赘多余的功能。
        基于上面讨论的原因,boost::object_pool被设计成了现在的样子,成了一个鸡肋类。类的设计者似乎忘记了内存池使用的初衷,忘记了内存池中内存申请释放的频率很高,远远大于内存池对象的析构。如果你依然想使用类似于此的内存清理功能,可以在boost::object_pool上修改,不复写malloc/free即可,重写object_pool的析构,简单释放内存就好,因此析构object_pool前不要忘记调用destroy,这也是使用placement new默认遵守的规则,或者保持以前的析构函数,牺牲析构时的性能。placement new的作用是为已经申请好的内存调用构造函数,使用流程为(1)申请内存buf(2)调用placement new:new(buf)construtor()(3)调用析构destructor()(4)释放内存buf。#include<new>可以使用placement new。
    3)singleton_pool
    pool的加锁版本。
    #include <boost/pool/singleton_pool.hpp>
    typedef struct student_st
    {
       
    char name[10];
       
    int age;
    }
    CStudent;
    typedef struct singleton_pool_tag
    {}singleton_pool_tag;
    int main()
    {
       typedef boost::singleton_pool
    <singleton_pool_tag,sizeof(CStudent)>  global;
       CStudent 
    * const df=(CStudent *)global::malloc();
       global::free(df);
       
    return 0;
    }

        singleton_pool为单例类,是对pool的加锁封装,适用于多线程环境,其中所有函数都是静态类型。它的模版参数有5个,tag:标记而已,无意义;RequestedSize:block的长度;UserAllocator:分配子,默认还是default_user_allocator_new_delete;Mutex:锁机制,默认值最终依赖于系统环境,linux下是pthread_mutex,它是对pthread_mutex_t的封装;NextSize:内存不足的时候,申请的block数量,默认是32。最全面的使用singleton_pool类似这样:typedef boost::singleton_pool<singleton_pool_tag,sizeof(CStudent),default_user_allocator_new_delete,details::pool::default_mutex,200>  global;
        它暴露的函数和pool相同。
    4)pool_allocator/fast_pool_allocator
        stl::allocator的替换方案。两者都是基于singleton_pool实现,实现了stl::allocator要求的接口规范。两者的使用相同,区别在于pool_allocator的实现调用ordered_malloc/ordered_free,fast_pool_allocator的实现调用malloc/free,因此推荐使用后者。

    #include <boost/pool/pool_alloc.hpp>
    #include 
    <vector>
    typedef struct student_st
    {
     
    char name[10];
     
    int age;
    }
    CStudent;

    int main()
    {
      std::vector
    <CStudent *,boost::fast_pool_allocator<CStudent *> > v(8);
      CStudent 
    *pObj=new CStudent();
      v[
    1]=pObj;
      boost::singleton_pool
    <boost::fast_pool_allocator_tag,sizeof(CStudent *)>::purge_memory(); 
      
    return 0;
    }

        fast_pool_allocator的模版参数有四个:类型,分配子,锁类型,内存不足时的申请的block数量,后三者都有默认值,不再说了。它使用的singleton_pool的tag是boost::fast_pool_allocator_tag。
    评价:boost::pool小巧高效,多多使用,多线程环境下使用boost::singleton_pool,不要使用两者的ordered_malloc/ordered_free函数。boost::object_pool不建议使用,可以改造后使用。pool_allocator/fast_pool_allocator推荐使用后者。

     

     

    展开全文
  • WinCE内存管理

    千次阅读 2012-08-08 10:02:16
    内存管理   如果你在写Windows CE 程序中遇到的最重要的问题,那一定是内存问题。一个WinCE 系统可能只有4MB 的RAM,这相对于个人电脑来说是十分少的,因为个人电脑的标准配置已经到了128MB 甚至更多。事实上...

    内存管理

           如果你在写Windows CE 程序中遇到的最重要的问题,那一定是内存问题。一个WinCE 系统可能只有4MB RAM,这相对于个人电脑来说是十分少的,因为个人电脑的标准配置已经到了128MB 甚至更多。事实上,运行WinCE 的机器的内存十分缺乏,以至于有时候有必要在写程序的时候为节约内存而牺牲程序的整体性能。

           幸运的是,尽管WinCE系统的内存很小,但可用来管理内存的函数却十分完善。WinCE实现了Microsoft Windows XPMicrosoft Windows Me中可用到的几乎全部的Win32内存管理APIWinCE支持虚拟内存(virtual memory)分配,本地(local)和分离(separate)的堆(heaps),甚至还有(memory-mapped files)内存映射文件。

           Windows XP一样,WinCE支持一个带有应用程序间内存保护功能的32位平面地址空间,但是WinCE是被设计来应用于不同场合,所以它底层的内存结构不同于Windows XP。这些不同能够影响到你如何设计一个WinCE 应用程序。在这一章中,我将讲述最基础的WinCE内存结构。我也将讲述包括WinCE中可用的内存分配方式中的不同点以及如何使用这些不同的内存类型来最小化你的程序的内存占有率。

    内存基础

           对所有的电脑来说,系统地运行一个WinCE,需要ROM(只读存储器)和RAM(随机存储器)。但不论如何,在WinCE系统中,ROMRAM的使用还是稍微有些不同于个人电脑环境。

    关于RAM

           RAMWinCE 系统中被分为两个区域:第一个是程序的存储区(program memory),也叫做系统堆(system heap)。第二个是对象存储区(object store)。这个对象存储区有点像一个永久的虚拟RAM磁盘。不同于PC上的旧式的虚拟RAM磁盘,对象存储区保留存储的文件甚至当系统被关闭以后。(脚注)这种安排的原因是WinCE 系统,例如Pocket PC代表性地具有一个主电池和一个备用电池。当用户更换主电池的时候,备用电池的工作是提供电源给RAM以便维持文件在对象存储区的存储。当用户按了重启键之后,WinCE核心就开始寻找在关闭系统前建立的对象存储区,如果找到的话就将继续使用它。

           RAM中的另一个区域则用作程序存储区。程序存储区有点像个人电脑中的RAM,它为正在运行的应用程序保存堆和栈的内容。在对象存储区和程序存储区之间的分界线是可以通过移动它来改变的,用户可以在控制面板中找到改变这条分界线的设置。在可用内存降低的(low-memory)条件下,系统将会弹出对话框询问用户是否要将对象存储区RAM划分一些给程序存储区RAM以满足要运行的应用程序的需求。

    关于ROM

           在个人电脑中,ROM是用来存储BIOS(基本输入输出系统)并且只有64128KB。在WinCE系统中,ROM大小可以从4MB32MB并且存放整个操作系统以及和系统捆绑在一起的应用程序。在这种情况下,ROMWinCE系统中就好像一个只读的硬盘。

           在一个WinCE系统中,存储在ROM之上的程序能够以现场执行(Execute in PlaceXIP)的方式运行。换句话说,程序可以直接从ROM中执行而不必先加载到RAM中再执行。这种能力对小型系统来说,使之在两个方面具有巨大的优势。代码直接从ROM中执行意味着程序代码不会占据更有价值的RAM。同样,程序在执行前也不必先复制到RAM中,这样就只需要很少的时间来启动一个应用程序。不在ROM中,但是被包含在对象存储区(译者注:上文将对象存储区比作永久的RAM磁盘,故此处要说明,只有Intel力推的nor flash memroy类型才能以XIP方式执行,ROM其实也是一种nor flash memory类型)或闪存卡(Flash memory storage card)中的程序将不能以现场方式执行,它们将被复制到RAM中再执行。

    关于虚拟内存

           WinCE 实现了系统的虚拟内存管理,在一个虚拟内存系统中,应用程序主要处理这个分离(译者注:物理上可能分离,但系统将它们联系起来),虚拟的地址空间,因此并不涉及到由硬件管理的物理内存。操作系统使用微处理器的内存管理单元来处理虚拟地址和物理地址间的实时转换。

           这种虚拟内存方法的优势能从MS-DOS系统复杂的地址空间看出来。一旦请求的RAM超过最初PC设计的640-KB限制,程序设计者将不得不作出像扩展内存一样的计划以便增加可用内存的数量。OS/2 1.x(译者注:IBM研制的操作系统)和Windows 3.0采用了一种基于段(segment-based)的虚拟内存系统来解决问题。应用程序使用虚拟内存不需要知道实际物理内存的位置,只要有内存可用就行。在这些系统中,虚拟内存以一种段的方式被实现了,即可移动的内存块(译者注:段其实就是内存分块)大小从16字节到64KB64-KB的限制并不是由于段本身原因,而是由于Intel 80286的特性所致,这就是Windows3.xOS/21.x的分段式虚拟内存系统结构。

    分页存储

    Intel 80386支持的段大小已经超过64KB,但是MicrosoftIBM开始设计OS/2 2.0,他们选择了一种不同的虚拟内存系统,随后也被386所支持,这就是分页式虚拟内存系统。在一个分页存储的系统中,最小的可被微处理器管理的单元是页(page)。对于Windows NTOS/2 2.0系统来说,页大小都被设置为386处理器默认的4096字节。当一个应用程序存取一个页的时候,微处理器将转换该页的虚拟内存地址到实际的ROMRAM中的物理页(译者注:这就是实现了地址映射和转换,将虚拟的和实际的存储单元一一对应),这一页同时被标记以便其他程序对该页的访问将被排斥。操作系统决定虚拟内存页是否有效,如果有效,将做一个物理内存页到虚拟页的映射。

    WinCE实现了一个和其他Win32操作系统类似的分页式虚拟内存系统。在WinCE中,一页的大小可以从1024字节到4096字节,基于微处理器的不同而不同。这和Windows XP不同,Windows XP页面尺寸是Intel微处理器所支持的4096字节。对WinCE所支持的CPU类型来说,有486IntelStrong-ARM,和Hitachi SH4 都是是用了4096-byte 的页面。NEC 4100Windows CE 3.0中使用了4-KB的页面尺寸但是在较早期的开放式系统版本中使用了1-KB的页面大小。

    虚拟内存页可以处在三种状态:自由(free),保留(reserved),或被提交(committed),)。自由页就像它的名称一样,自由并且可被分配。保留页是虚拟地址已经被保留,并且不能被操作系统或进程中的其他线程重新分配。保留页不能用在别处,但是它同样不能被当前程序使用,因为它没有被映射到物理内存。要想执行映射,它必须被提交,一个提交页能被应用程序保留,并且直接映射到物理地址。

    所有我刚才讲述的内容对有经验的Win32 程序员们来说是些陈旧的知识。对Windows CE 程序员来说最重要的东西是学习Windows CE 是如何改变这些因素的。当Windows CE 实现了大部分和它的老大哥Win32一样的内存API集的时候,Windows CE下面的基础结构将影响到上面的程序。在分开来看Window CE 应用程序的内存结构之前,让我们先来看看一些提供系统内存全局状态的函数。

    查询系统的内存

           如果一个应用程序知道系统当前的内存状态,它将可以较好地管理可用到的资源。WinCE实现了Win32GetSystemInfoGlobalMemoryStatus函数,GetSystemInfo函数原型如下:

    VOID GetSystemInfo (LPSYSTEM_INFO lpSystemInfo);

    它传递了一个指针给SYSTEM_INFO结构,定义如下

    typedef struct {

        WORD wProcessorArchitecture;

        WORD wReserved;

        DWORD  dwPageSize;

        LPVOID lpMinimumApplicationAddress;

        LPVOID lpMaximumApplicationAddress;

        DWORD  dwActiveProcessorMask;

        DWORD  dwNumberOfProcessors;

        DWORD  dwProcessorType;

        DWORD  dwAllocationGranularity;

        WORD  wProcessorLevel; 

        WORD  wProcessorRevision;

    } SYSTEM_INFO;

        wProcessorArchitecture参数表示系统微处理器的架构。它的值是定义在Winnt.h中,例如PROCESSOR_ARCHITECTURE_INTELWindows CE扩展了这些常数,包括PROCESSOR_ARCHITECTURE_ARMPROCESSOR_ARCHITECTURE_SHx,等等。增加的常数包括像Win32操作系统支持的网络CPUnet CPU)。跳过一些参数,我们看dwProcessorType参数,它来自于特定的微处理器类型。常数有Hitachi SHx架构中的PROCESSOR_HITACHI_SH3PROCESSOR_HITACHI_SH4。最后两个参数,wProcessorLevelwProcessorRevision,指明了CPU类型的特征。wProcessorLevel参数类似于dwProcessorType参数,它一个指定的微处理器系列中被定义了,dwProcessorRevision告诉你模式(model)和芯片的步进级别(stepping level)。

           dwPageSize参数说明了微处理器页面的大小,以字节为单位。知道这个值,将会在你直接处理虚拟内存API的时候带来方便,在此我只作简短说明。lpMinimumApplication­AddresslpMaximumApplicationAddress参数说明了应用程序可用到的最小和最大的虚拟内存地址。dwActiveProcessorMaskdwNumberOfProcessors参数显示被Window XP系统支持的多个处理器数量。因为Windows CE 只支持一个处理器,所以你可以忽略这个参数。dwAllocationGranularity参数说明了一个完整的虚拟内存区域分配的界限。像Windows XPWindows CE 规定虚拟区为64-KB的界限(译者注:作者此处64-KB的意思是即使你只分配一个字节的内存,系统也将会保留64-KB的虚拟地址空间给它,这个值一般是由硬件代码实现的,但是不同硬件可能不同值)。

           第二个方便的检测系统状态的函数如下:

    void GlobalMemoryStatus(LPMEMORYSTATUS lpmst);

    它返回一个MEMORYSTATUS结构,定义为

    typedef struct { 

        DWORD dwLength; 

        DWORD dwMemoryLoad; 

        DWORD dwTotalPhys; 

        DWORD dwAvailPhys; 

        DWORD dwTotalPageFile; 

        DWORD dwAvailPageFile; 

        DWORD dwTotalVirtual; 

        DWORD dwAvailVirtual; 

    } MEMORYSTATUS;

        dwLength参数在调用这个函数之前必须初始化。dwMemoryLoad参数是一个不确定的值;这是一个可用的一般性的参数指示了当前系统的内存使用情况(译者注:该参数是一个近似的百分比值,指明了物理内存的使用情况)。dwTotalPhysdwAvailPhys参数指明了RAM有多少页被分配给了程序存储区RAM,和还有多少可用(译者注:实际上是以字节为单位)。这些值不包括分配对象存储区的RAM

           dwTotalPageFiledwAvailPageFile参数在Windows XP下和Windows Me下指明了当前页面文件(paging file)的状态。因为Windows CE不支持页面文件,所以这些参数总是0dwTotalVirtualdwAvailVirtual参数指明了总共的和可用的可被应用程序存取的虚拟内存页的数量(译者注:参数都是以字节为单位,而不是指页数,dwAvailVirtual是指未保留和未提交的内存)。

           通过GlobalMemoryStatus返回的信息可以验证Windows CE内存结构,通过在有32MBRAMHP iPaq Pocket PC上调用函数,返回值如下:

    dwMemoryLoad       0x18          (24)

    dwTotalPhys        0x011ac000    (18,530,304)

    dwAvailPhys        0x00B66000    (11,952,128)

    dwTotalPageFile    0

    dwAvailPageFile    0

    dwTotalVirtual     0x02000000    (33,554,432)

    dwAvailVirtual     0x01e10000    (31,522,816)

     

        dwTotalPhys参数表明了系统的32MB RAM,分配了18.5MB给程序存储区RAM,其中12MB仍然可用。注意这对应用程序来说,并不是通过这次调用,就知道了另外14MBRAM是分配给对象存储区的。要检测分配给对象存储区的RAM的大小,要使用GetStoreInformation

           dwTotalPageFiledwAvailPageFile参数是0,表明页面文件不被Windows CE所支持。dwTotalVirtual参数十分有趣,因为它显示了Windows CE 强制给程序的32-MB的虚拟内存限制。其间,dwAvailVirtual参数显示了只使用了32MB虚拟内存的一小部分(译者注:即33,554,432-31,522,816=2,031,616)。

    应用程序的地址空间

    尽管和Windows XP的应用程序设计类似,但Windows CE应用程序地址空间有一个巨大的不同影响到应用程序。在Windows CE之下,一个应用程序被限制在虚拟内存空间中它自己的32MB slot(槽)和 32MB slot 1中,slot 1用来加载基于XIPDLL(译者注:Windows CE将虚拟地址空间分为33slot,每个slot 32MB,序号从0-32 ,附插图c-1,c-2)。当系统只有4MB RAM的时候,分配给应用程序32MB的虚拟地址空间看起来是比较合理的,Win32的程序员在使用这个2-GB的虚拟地址空间的时候,必须记住对Windows CE应用程序的虚拟地址空间限制。

    7-1展示了一个应用程序的64-MB虚拟地址空间,其中包括32MB用于XIPDLL空间。

    7-1     Windows CE的内存映射图

     

           要注意的是应用程序是以一个64-KB的内存区域开始从0x10000映射,记得最低的64KB地址空间是由Windows为所有应用程序保留的。文件映象包括代码,静态数据段和资源段。在实际过程中,当应用程序启动时代码页(code pages)不会载入进来,只有在需要该页面被载入的时候,代码才被载入进来。

           只读静态数据段(read-only static data segment)和可读写静态数据区(read/write static data areas)只占很少的页面。这些段都是排列在一起的。如同代码一样,只有当这些数据段被应用程序读或者写的时候才会提交给RAM。应用程序的资源将被载入到一些分离的页面中,这些资源是只读的,并且只有当它们被应用程序获取的时候才会分页进入RAM

           应用程序的栈(stack)被映射到资源段之上。栈的段位置很容易被找到,因为它提交的页就在被保留的区域的尾部。栈的表现是从高地址到低地址增长(译者注:即从高到低填满地址)。如果该应用程序有超过一个线程,那么应用程序的地址空间就会保留超过一个的栈的段。

           紧接着栈的就是本地堆(local heap)。引导程序保留了大量的页,大约有几百K交给heap来使用,但是只提交满足mallocnewLocalAlloc函数调用分配的内存(译者注:这里是说,被分配多少内存才可以提交多少内存,没被分配的不能用作提交)。从本地堆的保留页尾部到non-XIP DLL开始的部分剩余保留页面将被映射为自由的保留空间,如果RAM允许,应用程序可以提交这些保留页。Non-XIP DLLs 就是不能被在ROM中现场执行的DLL将被从上至下载入到32MB的地址空间。Non-XIP DLLs 包括那些被压缩存储在ROM中的DLL。被压缩的ROM 中的文件在被载入到RAM中执行前必须先解压缩。

           被保留给XIP DLLs32MB 应用程序地址空间的较高位置。Windows CE 映射这些XIP DLLs的代码进入这个空间(译者注:即较高空间),而可读写段被映射进较低位置。从Windows CE 4.2开始,在ROM中的纯资源的DLL将被载入到应用程序64MB空间之外的虚拟内存空间。

     

    脚注

    PocketPC这样的移动式系统中,当用户按下关闭按钮时系统将不会被真正的关闭,系统进入一种低功耗的挂起状态。 

     

    内存分配的不同类型

           一个Windows CE 应用程序有许多不同的内存分配方式。在内存食物链的底端是Virtualxxx 函数,它们直接保留,提交和释放(free)虚拟内存页。接下来的是堆(heap API。堆是系统为应用程序保留的内存区域。堆有两种风味:当应用程序启动时自动默认分配的本地堆(local heap),以及能够由程序手动创建的分离堆(separate heap)。在堆API之后是静态数据,数据块是被编译器定义好的或者由程序手动创建的。最后,我们来看栈,这是程序为函数存储变量的区域。

           一个Windows CE不支持的Win32 内存API是全局堆(global heap)。全局堆API包括Global­AllocGlobalFreeGlobalRealloc,将不会出现在Windows CE中(译者注:很奇怪,我在Windows CE 中仍然可以使用这几个API,并且工作正常,好像Microsoft并没有把它们完全去掉)。全局堆只是从Windows 3.xWin16 时期继承而来。在Win32中,全部和本地的堆很类似,全局内存一个独特用法是,为剪贴板的数据分配内存,在Windows CE中已经被本地堆替代并加上了句柄。

           Windows CE中最小化内存使用的关键是选择与内存块使用模型相匹配的恰当的内存分配策略。我将回顾一下这些内存类型然后讲述Windows CE应用程序中的最小化内存使用策略。

    虚拟内存

           虚拟内存是内存类型中最基础的。系统调用虚拟内存API来为其他类型内存分配内存。包括堆和栈。虚拟内存API,包括VirtualAllocVirtualFreeVirtualReSize函数,这些可以直接操作应用程序虚拟内存空间的虚拟内存页面。页面可以保留,提交给物理内存,或使用这些函数释放。

    分配虚拟内存

           分配和保留虚拟内存是同过这个函数完成的:

    LPVOID VirtualAlloc (LPVOID lpAddress, DWORD dwSize,

                         DWORD flAllocationType,

                         DWORD flProtect);

    VirtualAlloc的第一个参数是要分配内存区域的地址。当你使用VirtualAlloc来提交一块以前保留的内存块的时候,lpAddress参数可以用来识别以前保留的内存块。如果这个参数是NULL,系统将会决定分配内存区域的位置,并且围绕64-KB的范围(译者注:就是前面说提及的最小内存分配尺寸)。第二个参数是dwSize,要分配或者保留的区域的大小。这个参数以字节为单位,而不是页,系统会根据这个大小一直分配到下页的边界。

    flAllocationType参数指定了分配的类型,你可以指定或者合并以下标志:MEM_COMMITMEM_AUTO_COMMITMEM_RESERVEMEM_TOP_DOWNMEM_COMMIT标志分配程序使用的内存,MEM_RESERVE保留虚拟地址空间以便以后提交。保留的页不能存取直到调用VirtualAlloc的时候再次指定了MEM_COMMIT标志。第三个标志,MEM_TOP_DOWN,告诉系统从最高可允许的虚拟地址开始映射应用程序。

    The MEM_AUTO_COMMIT标志是唯一一个Windows CE最方便的标志,当这个参数被指定了之后,内存块立即被保留,当其中的页被第一次存取的时候,系统将自动提交该页。这允许你分配大块的虚拟内存而不需要顾及系统和实际RAM分配直到当前页被第一次使用。自动提交内存的缺点是,物理RAM需要退回当页面被第一次访问时可能不可用的页面。在这种情形下,系统将产生一个异常(exception)(译者注:可能会出现因为无法访问而出错)。

           VirtualAlloc可以通过并行多次调用提交一个区域的部分或全部来保留一个大的内存区域。多重调用提交同一块区域不会引起失败。这使得一个应用程序保留内存后可以随意提交将被写的页。当这种方式不在有效的时候,它会释放应用程序通过检测被保留页的状态看它是否在提交调用之前已经被提交。

           flProtect参数指定了被分配区域的访问保护方式。这些不同的标志被总结在下面的列表中:

    PAGE_READONLY

    该区域为只读。如果应用程序试图访问区域中的页的时候,将会被拒绝访问。

    PAGE_READWRITE

    区域可被应用程序读写。

    PAGE_EXECUTE

    区域包含可被系统执行的代码。试图读写该区域的操作将被拒绝。

    PAGE_EXECUTE_READ

    区域包含可执行代码,应用程序可以读该区域。

    PAGE_EXECUTE_READWRITE

    区域包含可执行代码,应用程序可以读写该区域。

    PAGE_GUARD

    区域第一次被访问时进入一个STATUS_GUARD_PAGE异常,这个标志要和其他保护标志合并使用,表明区域被第一次访问的权限。

    PAGE_NOACCESS

    任何访问该区域的操作将被拒绝。

    PAGE_NOCACHE

    RAM中的页映射到该区域时将不会被微处理器缓存(cached)。

    PAGE_GUARDPAGE_NOCHACHE标志可以和其他标志合并使用以进一步指定页的特征。PAGE_GUARD标志指定了一个防护页(guard page),即当一个页被提交时会因第一次被访问而产生一个one-shot异常,接着取得指定的访问权限。PAGE_NOCACHE防止当它映射到虚拟页的时候被微处理器缓存。这个标志方便设备驱动使用直接内存访问方式(DMA)来共享内存块。

    区域和页

           在我继续谈论虚拟内存API之前,我需要说明一个比较细微的差异。虚拟内存在区域内被保留是以64KB为基础的。在区域内的页面能够一页一页地被提交(译者注:前面说到在Windows CE中每页是4096字节或1024字节)。你可以直接提交一页或者几页而不是保留区域的全部页。但是对页或几页来说,直接提交的仍是以64-KB为单位(译者注:可以直到被提交的页数量足够填满64KB才真正提交),因为这个原因,最好保留一块64-KB的虚拟内存,然后提交那些需要的页到区域里。

           因为对每个进程32MB虚拟内存地址空间的限制,这就有了一个最大值 32MB/64KB-1=511,这是虚拟内存在内存溢出前能被保留的最大值。接下来,有个例子,代码段如下:

    #define PAGESIZE 1024   // Assume we're on a 1-KB page machine

    for (i = 0; i < 512; i++) 

    pMem[i] = VirtualAlloc (NULL, PAGESIZE, MEM_RESERVE  MEM_COMMIT,PAGE_READWRITE);

    代码分配512个单页的虚拟内存。甚至你系统还有一半的可用RAMVirtualAlloc也会在完成分配前失败。因为它的运行已经超出了应用程序的虚拟地址空间。发生这种情况是因为每1-KB的块要占用64-KB的空间,接下来应用程序的代码,栈,和本地堆也要映射到同样的32-MB虚拟地址空间,可用的虚拟分配区域通常不超过475个。

         一个比较好的分配512块特殊内存的方法是这样做:

    #define PAGESIZE 1024   // Assume we're on a 1-KB page machine.

    // Reserve a region first.

    pMemBase = VirtualAlloc (NULL, PAGESIZE * 512, MEM_RESERVE,

                             PAGE_NOACCESS);

     

    for (i = 0; i < 512; i++) 

        pMem[i] = VirtualAlloc (pMemBase + (i*PAGESIZE), PAGESIZE, 

                                MEM_COMMIT, PAGE_READWRITE);

         代码首先保留了一块区域,页面将在以后被提交。因为区域已经被先保留了,提交页就不受64-KB限制(译者注:只有保留页最小值受64KB限制),等等,如果你系统中有512KB的可用内存,分配将会成功。

         尽管我刚才给你看的是一个人为的例子(还有比直接分配虚拟内存更好的方法来分配1-KB的内存块),这中内存分配方法验证了一个重要的不同(对于其他Windows系统)。在桌面版本的Windows中,工作中的应用程序有一个完全的2-GB的虚拟地址空间。在Windows CE中,一个程序员必须明白每个应用程序只被保留了较小的32-MB虚拟地址空间。

    释放虚拟内存

           你可以通过调用VirtualFree来取消提交,或释放虚拟内存。从物理RAM页中取消提交或者取消映射,但是保持页被保留的状态。函数原型如下:

        BOOL VirtualFree (LPVOID lpAddress, DWORD dwSize,

                      DWORD dwFreeType);

    lpAddress参数是一个指针,指向要被释放或取消提交的虚拟内存的区域。dwSize参数指明要取消提交区域的大小,以字节为单位。如果区域要被释放,这个值必须是0dwFreeType参数包含了操作类型标志,MEM_DECOMMIT标志指定了区域将被取消提交但是仍被保留,MEM_RELEASE标志说明区域要取消提交并且释放。

    在区域中的所有的页通过VirtualFree被释放必须处在同样的情况下。更确切地说,区域中的全部页要被释放,那这些页要么都是被提交的页,要么都是被保留的页。如果有些页被提交,有些页被保留,那么VirtualFree函数调用就会失败。

    改变和查询权限

           你可以通过调用VirtualProtect来修改最初通过VirtualAlloc指定的虚拟内存区域的访问权限。这个函数只能改变被提交的页的访问权限。函数的原型如下:

    BOOL VirtualProtect (LPVOID lpAddress, DWORD dwSize, 

                         DWORD flNewProtect, PDWORD lpflOldProtect);

    开始的两个参数lpAddressdwSize,指定了函数作用的块的大小。flNewProtect参数包含区域的新的保护标志。这些标志和我前面提到的VirtualAlloc函数使用的一样。lpflOldProtect参数指向一个DWORD,将返回旧的保护标志(译者注:如果此处为NULL或指向一个无效的变量,函数将会失败)。

    当前区域的保护权限可用通过下面的调用查询:

    DWORD VirtualQuery (LPCVOID lpAddress, 

                        PMEMORY_BASIC_INFORMATION lpBuffer,

                        DWORD dwLength);

        lpAddress参数包含区域开始查询的地址。lpBuffer指针指向我很快就要提到的一个PMEMORY_BASIC_INFORMATION结构。第三个参数dwLength,必须包含PMEMORY_BASIC_INFORMATION结构的大小。

           PMEMORY_BASIC_INFORMATION结构被定义如下:

    typedef struct _MEMORY_BASIC_INFORMATION { 

        PVOID BaseAddress; 

        PVOID AllocationBase; 

        DWORD AllocationProtect; 

        DWORD RegionSize; 

        DWORD State; 

        DWORD Protect; 

        DWORD Type; 

    } MEMORY_BASIC_INFORMATION;

    MEMORY_BASIC_INFORMATION结构的第一个字段是BaseAddress,是传递给VirtualQuery函数的一个地址。AllocationBase字段包含使用VirtualAlloc函数分配的区域的基地址,AllocationProtect字段包含区域原来被分配时的保护属性。RegionSize字段包含从传递给VirtualQuery的指针开始到一系列具有相同属性的页为结尾的区域大小(译者注:这里是从基地址开始)。State字段包含区域中页的状态-自由,保留,提交。Protect字段可以包含MEM_PRIVATE标志,指明该区域包含应用程序私有的数据;MEM_MAPPED指明该区域被映射为一个内存映射文件;MEM_IMAGE指明该区域被映射为一个EXEDLL模块。

    理解VirtualQuery最好的方式是看例子,比方说一个应用程序保留了16,384字节(在以页面大小为1-KB的机器中占16页)。系统从地址0xA0000开始保留这16-KB的块。后来应用程序从最初的区域中提交了从第2048字节(2页)开始的9216字节(9页)。图7-2显示了这个假设的情况。

    7-2被保留的区域有9页被提交

           如果一个对VirtualQuery的调用中,lpAddress指向第四页的区域(地址0xA1000),返回值如下:

    BaseAddress          0xA1000

    AllocationBase       0xA0000

    AllocationProtect    PAGE_NOACCESS

    RegionSize           0x1C00    (7,168 bytes or 7 pages)

    State                MEM_COMMIT

    Protect              PAGE_READWRITE

    Type                 MEM_PRIVATE

    BaseAddress字段包含传递给VirtualQuery的地址,值为0xA1000,在最初的区域中是第4096字节。AllocationBase字段包含最初区域的地址。当AllocationProtect设为PAGE_NOACCESS时,指明区域是最初被保留的,而不是直接提交。RegionSize字段包含传递给VirtualQuery的指针0xA1000开始,到被提交的页结束地址0xA2C00的字节数。StateProtect字段包含的标志表明当前的页状态。Type字节表明区域被应用程序分配给自己使用。

           很明显,以页为单位分配内存对应用程序来说效率是很低的。为了优化内存的使用,应用程序需要以字节为单位分配和释放内存,或者至少以每8字节为单位。系统通过堆来实现这种分配方式。使用堆可以免去处理由Windows CE支持的不同微处理器的不同页面大小。一个应用程序可以简单地在堆中分配一块内存,由系统来处理分配需要的页数。

           就像我前面提到的,堆是系统为应用程序保留的虚拟内存区域。系统提供大量的函数来在堆中分配和释放内存块,并且间隔比页要小(译者注:例如每页大小为4KB,而堆分配可以字节为单位)。当内存由应用程序的堆分配时,系统自动分配调整堆大小来满足需要,当堆中的内存块被释放时,系统会查看是否整页被释放,如果是的话,那么该页将被回收。

           不同于Windows XPWindows CE只支持在堆中分配固定(fixed)的块。这简化了内存块在堆中的处理,但是这使得堆在分配和释放一段时间后会产生碎片。当堆里已经清空的时候,仍然会占用大量的虚拟内存页,因为系统不能在堆中内存页没有完全释放的时候回收这些页(译者注:因为堆以字节为单位,一页中可能有的块需要被释放,其他的块不需要,所以整页都不会被释放)。

           当应用程序启动的时候,每个程序都会有一个由系统创建的默认或本地堆。本地堆中的内存块,可以通过LocalAllocLocalFreeLocalRealloc来分配,释放和改变大小。一个应用程序也可以建立分离堆。这些堆和本地堆有着相同的属性,但是是通过一组Heapxxxx函数来管理的。

    本地堆

           在默认情况下,Windows CE最初会保留192,512字节给本地堆,但是只提交被分配的页。如果应用程序在本地堆中分配了超过188KB,系统将会分配更多的空间给本地堆。增加堆大小将需要一个分离的,不连续的保留地址空间作为堆的附加空间。应用程序不应该假设本地堆被包含在一块虚拟地址空间里。因为Windows CE 的堆只支持固定的块,Windows CE执行的只是Win32本地堆函数的子集,提供必要的分配,改变大小,释放固定的本地堆内存块。

    在本地堆中分配内存

           你可以通过一下调用在本地堆中分配一块内存:

    HLOCAL LocalAlloc (UINT uFlags, UINT uBytes);

    调用返回一个HLOCAL,这是本地内存块的句柄,但是由于内存块是固定分配的,所以返回值可以被简单地看作是一个指向块的指针。

    uFlags参数描述了内存块的特征。标志由于Windows CE被限制固定分配操作,只支持以下内存:

    LMEM_FIXED

    在本地堆中分配一个固定内存块,因为本地堆分配已经固定,所以是多余的。

    LMEM_ZEROINIT

    初始化内存内容为0

    LPTR

    合并LMEM_FIXEDLMEM_ZEROINIT标志。

    uBytes参数指定了要分配的内存块的大小,以字节为单位。块大小要补齐,但是只针对后面8字节范围。

    释放本地堆的内存

           你可以通过以下调用释放内存块:

    HLOCAL LocalFree (HLOCAL hMem);

    函数需要本地堆内存句柄,成功会返回NULL。如果调用失败,会返回内存块的句柄。

    改变和查询本地堆内存的大小

           你可以通过调用改变本地堆的分配:

    HLOCAL LocalReAlloc (HLOCAL hMem, UINT uBytes, UINT uFlag);

    hMem参数是一个由LocalAlloc返回的指针(句柄)。uBytes参数是内存块的新大小。uFlag参数包含给新内存块的标志。在Windows CE中,有两个新标志与之相关,LMEM_ZEROINITLMEM_MOVEABLELMEM_ZEROINIT表示调用函数后内存块中新增加的区域被初始化为0LMEM_MOVEABLE标志告诉Windows,当内存块增加后,没有合适的空间容纳内存块时,函数可以立即移动内存块。如果没有这个标志,当你没有合适的空间来满足需要的时候,LocalRealloc将会出现out-of-memory的错误而失败,如果你指定了LMEM_MOVEABLE标志,调用将会返回句柄(实际是指向内存块的指针)。

    内存块的大小可以通过以下调用查询:

    UINT LocalSize (HLOCAL hMem);

    返回内存块最少需要的内存大小。像我前面提到的,Windows CE本地堆自动以8个字节来补齐(译者注:就是分配1字节要占8字节)。

    分离堆

           为了避免本地堆的碎片,并且如果你要分配连续的内存块,较好的办法是建立分离堆,但将花费一定的时间。一个例子就是,文本编辑器为要编辑的文件建立多个分离堆。当文件被打开或者关闭的时候,堆随之建立和销毁。

    Windows CE下的堆和Windows XP下有着同样的API。唯一值得注意的不同是缺少HEAP_GENERATE- _EXCEPTIONS标志。在Windows XP下,该标志表示系统在分配请求不合适的时候产生一个异常。

    建立一个分离堆

           你可以通过以下调用建立一个分离堆。

    HANDLE HeapCreate (DWORD flOptions, DWORD dwInitialSize,

                       DWORD dwMaximumSize);

    Windows CE中,第一个参数flOptions必须为空或包含HEAP_NO_SERIALIZE标志。默认情况下,Windows堆管理程序防止一个进程中的两个线程在同意时间访问堆。这个串行参数防止系统用来跟踪堆中内存块分配的堆指针被破坏。在其他版本的Windows中,当你不需要这种保护时可以使用HEAP_NO_SERIALIZE标志。在Windows CE中,该标志是为了兼容性而提供的,所有的堆访问都是串行的(译者注:串行即非并行,只能依次访问)。

    其他两个参数,dwInitialSizedwMaximumSize,指定了最初的大小和预期的堆最大值。dwMaximumSize的值确定虚拟内存空间保留给堆多少页。如果你想让Windows来决定有多少页可以保留,你可以把这个参数设为0。默认一个堆的大小是188KBdwInitialSize参数决定了有多少这些保留的页将被提交。如果该参数为0,表示堆将一页一页提交。

    在分离堆中分配内存

           你可以通过以下调用分配内存

    LPVOID HeapAlloc (HANDLE hHeap, DWORD dwFlags, DWORD dwBytes);

           注意,返回值是一个指针,而不是和LocalAlloc函数一样的句柄。分离堆总是分配固定的内存块,甚至在Windows XPWindows Me中也是一样。第一个参数是通过HeapCreate调用返回的句柄。dwFlags参数可以是两个自说明的(self-explanatory)标志之一HEAP_NO_SERIALIZE HEAP_ZERO_MEMORY。最后一个参数dwBytes指定了要分配的内存块字节数。大小要和DWORD补齐。

    释放分离堆中的内存

           你可以通过以下调用释放内存块:

    BOOL HeapFree (HANDLE hHeap, DWORD dwFlags, LPVOID lpMem);

    dwFlags参数唯一的标志是HEAP_NO_SERIALIZE,当hHeap包含堆句柄时,lpMem参数指向要释放的内存块。

    改变和查询分离堆中内存的大小:

           你可以通过以下调用改变堆大小。

        LPVOID HeapReAlloc (HANDLE hHeap, DWORD dwFlags, LPVOID lpMem,

                        DWORD dwBytes);

        dwFlags参数包含三种标志的组合:HEAP_NO_SERIALIZEHEAP_REALLOC_IN_PLACE_ONLYHEAP_ZERO_ MEMORY。其中较新的标志是HEAP_REALLOC_IN_PLACE_ONLY,这个参数告诉堆的管理者,找不到要分配的块的空间,重分配操作失败。这个标志方便的地方在于当你有了一些指向内存数据块的指针,并且你不想改变内存块。lpMem参数是一个指向要改变大小的内存块的指针,dwBytes参数是被请求的新内存块的大小。注意,HeapReAllocHEAP_REALLOC_IN_PLACE_ONLY标志提供和LocalReAllocLMEM_MOVEABLE相反的作用。HEAP_REALLOC_IN_PLACE_ONLY防止在分离堆中对内存块默认的移动操作。而LMEM_MOVEABLE允许本地堆中对内存块的默认移动操作。如果HeapReAlloc成功,就返回一个指向内存块的指针,否则就返回NULL。除非你指定内存块不可重新定位,那么当内存块因为堆中空间不足时将不得不重定位,因此造成返回指针的值将与原来不同。

           要决定实际的内存块大小,你可以作以下调用:

    DWORD HeapSize (HANDLE hHeap, DWORD dwFlags, LPCVOID lpMem);

    参数就像你想象的:有堆的句柄,单选标志HEAP_NO_SERIALIZE,和指向内存块的指针。

    销毁一个分离堆

           你可以通过以下调用完全释放一个堆:

    BOOL HeapDestroy (HANDLE hHeap);

    在堆中单个的内存块并不需要在销毁堆前释放。

    最后一个是写DLL时比较有价值的函数:

    HANDLE GetProcessHeap (VOID);

    返回的是调用DLL时进程的本地堆的句柄。这个函数允许一个DLL在调用者进程的本地堆中分配内存。GetProcessHeap返回的句柄可以供其他堆调用使用,HeapDestroy除外。

           栈是Windows CE内存类型中最容易使用的(自行管理)。在Windows CE中的栈像其它操作系统一样,是被引用函数的临时变量存储区。操作系统也用栈来存储函数的返回地址和在异常处理中微处理器寄存器的状态。

           在系统中,Windows CE给每个线程一个分离的栈。默认情况下,系统中每个栈大小最大被限制为58KB。在一个进程中,每个分离的线程可以增加栈的大小直到58-KB的限制。

    这个限制使得要我们要知道Windows CE如何对栈管理。当线程被建立的时候,Windows CE保留一个64-KB的区域给每个线程的栈。栈增加时,提交虚拟内存页是从上至下的。当栈减小时,系统将处于的低内存环境(low-memory),会回收在栈下面未使用但是仍然被提交的页。58KB的限制来源于64-KB的区域减去用来防止栈的上溢和下溢的页面数量。

           当一个应用程序建立一个新的线程时,栈的最大尺寸可以通过建立线程时CreateThread调用来指定。应用程序的主线程的栈大小可以通过应用程序被连接时的连接器开关(linker switch)来指定。同样会有一些页用作防护,但是栈的大小可以指定至1MB。注意,这个指定大小同样会被用作所有分离线程栈的默认栈大小。那就是说,如果你指定主栈为128KB,程序中所有其他的线程栈大小也限制为128KB,除非在用CreateThread建立线程时指定一个不同的大小。

           当你计划如何在应用程序中使用栈的时候,另一个要值得考虑事情的是。当应用程序调用一个需要栈空间的函数时,Windows CE会试图立即提交满足要求的当前栈之下的页面,如果没有物理RAM可用,需要栈空间的线程将会暂时停止。如果请求在短时间内得不到允许,可能产生一个异常。但是如果系统不发生异常的化,Windows CE将会最大限度释放请求的页。我将简短地说明一下低内存环境,但现在你只需要记住在的内存环境中不要尝试使用大量的栈空间。

    静态数据

           CC++应用程序有一个预先定义好的内存块,这是由应用程序被装载时自动分配的。这些块被用来存储静态分配的字符串,缓冲区和全局变量,同时也包括通过静态连接到应用程序的静态库函数中的缓冲区。这些对C程序员来说都不陌生,但是在Windows CE下,这是最后一块可以在RAM之外压缩的空间(译者注:作者的意图是尽可能压缩内存占有率)。

           Windows CE分配给应用程序两块RAM中的内存块存放静态数据,一个是可读写数据(read/write data)和只读数据(read only data)。因为这些区域是基于页分配的,所以你可以在一页的静态数据开始到下一页开始之间找到一些剩余空间。细微调整Windows CE应用程序就是要写满这些剩余的空间。如果你在静态数据区有空间,最好把一个或两个缓冲区放到静态数据区,避免动态分配缓冲区。

           另一个值得考虑的事情是你是否在写一个基于ROM的应用程序。你要把尽可能多的数据移到只读静态数据区。Windows CE不会分配只读的RAM给基于ROM的应用程序。并且,ROM页会直接映射到虚拟地址空间。这实际上就给你了一个无限制的只读空间,而且不会影响到应用程序对RAM的需求。

           确定静态数据区大小的方法是查看连接器产生的映象(map)文件。映象文件主要用于调试(debug)目的来确定函数和数据的位置。但是如果你知道查看什么地方的话,它也可以用来显示静态数据的大小。列表7-1显示了一个由Visual C++产生的示例映象文件的一部分。

    列表7-1。映象文件的顶部显示了应用程序数据段的大小

    memtest

     

    Timestamp is 34ce4088 (Tue Jan 27 12:16:08 1998)

     

    Preferred load address is 00010000

     

    Start         Length     Name                   Class

    0001:00000000 00006100H .text                   CODE

    0002:00000000 00000310H .rdata                  DATA

    0002:00000310 00000014H .xdata                  DATA

    0002:00000324 00000028H .idata$2                DATA

    0002:0000034c 00000014H .idata$3                DATA

    0002:00000360 000000f4H .idata$4                DATA

    0002:00000454 000003eeH .idata$6                DATA

    0002:00000842 00000000H .edata                  DATA

    0003:00000000 000000f4H .idata$5                DATA

    0003:000000f4 00000004H .CRT$XCA                DATA

    0003:000000f8 00000004H .CRT$XCZ                DATA

    0003:000000fc 00000004H .CRT$XIA                DATA

    0003:00000100 00000004H .CRT$XIZ                DATA

    0003:00000104 00000004H .CRT$XPA                DATA

    0003:00000108 00000004H .CRT$XPZ                DATA

    0003:0000010c 00000004H .CRT$XTA                DATA

    0003:00000110 00000004H .CRT$XTZ                DATA

    0003:00000114 000011e8H .data                   DATA

    0003:000012fc 0000108cH .bss                    DATA

    0004:00000000 000003e8H .pdata                  DATA

    0005:00000000 000000f0H .rsrc$01                DATA

    0005:000000f0 00000334H .rsrc$02                DATA

     Address         Publics by Value              Rva+Base     Lib:Object

     

    0001:00000000       _WinMain                   00011000 f   memtest.obj

    0001:0000007c       _InitApp                   0001107c f   memtest.obj

    0001:000000d4       _InitInstance              000110d4 f   memtest.obj

    0001:00000164       _TermInstance              00011164 f   memtest.obj

    0001:00000248       _MainWndProc               00011248 f   memtest.obj

    0001:000002b0       _GetFixedEquiv             000112b0 f   memtest.obj

    0001:00000350       _DoCreateMain              00011350 f   memtest.obj.

     

    在列表7-1中的映象文件指出了EXE文件有五个区。区0001是文本段,包含程序中可执行的代码。区0002包含只读(read-only)静态数据。区0003包含可读写(read/write)静态数据。区0004包含调用其他DLL的固定表。最后,区0005是资源区,包含应用程序的资源,例如菜单和对话框模板。

    让我们来看看.data.bss.rdata行。.data区包含已初始化的可读写数据。如果你这样初始化了一个全局变量:

    static HINST g_hLoadlib = NULL;

    g_loadlib变量将结束在.data段末尾。.bss段包含未初始化的可读写数据。一个缓冲被定义如下:

    static BYTE g_ucItems[256];

    .bss段为结尾。最后一个段.rdata,包含只读数据。你使用const关键字定义的静态数据结束在.rdata段。有一个结构的例子,使我用来作消息查询表的:

    // Message dispatch table for MainWindowProc

    const struct decodeUINT MainMessages[] = {

        WM_CREATE, DoCreateMain,

        WM_SIZE, DoSizeMain,

        WM_COMMAND, DoCommandMain,

        WM_DESTROY, DoDestroyMain,

    };

        .data.bss块被折叠进0003区,如果你将第三区的所有块大小加起来,总共为0x2274,或8820字节。为和下页对齐,读写数据区将占9页,那么就有396字节未使用(译者注:1024*9-8820=396)。因此在这个例子中,把一个或者两个缓冲区放入静态数据区比较合适。只读数据段0002区,包括.rdata,占0x08422114字节,占3页,剩余958字节,几乎是一整页。在这种情况下,移动75字节的常量数据从只读段到可读写段将在应用程序加载时节约一页的RAM

    字符串资源

    有一个经常忘记的只读区域时应用程序的资源段,像我前面在第四章提到的Windows CE的新特性有一个LoadString函数,值得再次重复。如果你调用LoadString时指向缓冲区的指针写0,函数将返回一个指向资源段中字符串的指针。例子如下:

    LPCTSTR pString;

     

    pString  = (LPCTSTR)LoadString (hInst, ID_STRING, NULL, 0)

    返回的字符串是只读的,但是它允许你应用字符串而不需要分配一个缓冲给字符串。这里警告一下,字符串不能以0结尾,除非你在资源编译器命令行中加了-n开关。不管如何,单词必须是先于字符串资源长度(译者注:作者此处意思可能是说长度包含字符串资源的长度)。

    选择适当的内存类型

           现在我们已经看过了不同类型的内存,是时候来考虑最好的使用办法了。对大的内存块来说,直接分配虚拟内存是最好的办法,一个应用程序可以保留很多的地址空间(直到应用程序32MB的限制)但是只能在一个时间提交必须的页。直接分配虚拟内存是最灵活的内存分配方式,它把页间隔(granularity)的负担以及对保留页和提交页都交由我们负担。

           本地堆是很方便的,它不需要创建并且会自动随着需求扩大。但碎片是这里的问题。但是要考虑到Pocket PC的应用程序可能会运行几星期或几个月的时间。在Pocket PC上没有关闭电源的按钮,只有挂起命令。因此,你考虑内存碎片的时候不要假设用户会打开应用程序,改变一个项目,然后关闭它。用户可能打开程序然后让它一直运行以至于程序就像一个快捷方式(quick click away)。

           分离堆的优点是当你不用时可以销毁,把碎片消灭在萌芽状态。有一点不好的就是分离堆需要手动创建和销毁。

           静态数据区是放置一两个缓冲区的好地方,因为页面是已经被分配的。管理静态数据的关键是使静态数据段大小尽可能地接近,但是要超过你目标处理器的页面的大小。当常量数据在只读段中,往往较好的办法是把它移到可读写段中。但当应用程序被烧到ROM中时,你不要这么做。常量数据越多会比较好,因为它不占RAM。只读段方便应用程序从对象存储区启动,因为只读页能通过操作系统丢弃和重载。

           栈用起来比较简单而且到处存在。唯一要考虑的是栈的最大尺寸和在的内存环境下扩大栈的问题。确定你的应用程序在关闭的时候不需要大量栈空间。当程序被关闭时,如果系统挂起你程序中的一个线程,用户可能会丢失数据。这会使顾客不满意。

    低内存环境

           当系统运行在一个低RAM环境中,应用程序将调整并最小化它们的内存使用。Windows CE运行在一个几乎永久的低内存环境中。Pocket PC被特意设计为运行低内存环境。在Pocket PC中的应用程序没有关闭按钮,当系统需要更多内存时,外壳(shell)自动关闭这些程序。正因为如此,Windows CE有许多方法来管理运行在低内存系统中的程序。

    WM_HIBERNATE 消息

           Windows CE第一个最明显的变化时是增加了WM_HIBERNATE消息。Windows CEshell发送消息给最顶层的有WS_OVERLAPPED式样(那就是说,既没有WS_POPUP也没有WS_CHILD式样)和WS_VISIBLE式样的窗口。这些限制将允许大多数程序至少有一个窗口可以接受WM_HIBERNATE消息。有一个例外就是,当应用程序不能真正结束程序而只是简单隐藏所有窗口。这种方式允许应用程序可以快速启动,因为它下次只是显示窗口。但是这就意味着,当用户想关闭它们的时候仍然占据着RAM。这对程序设计来说是正确的,但是不应用在Windows CE中,这种方式会造成程序被隐藏时总处在冬眠(hibernate)模式,因为它们永远接收不到WM_HIBERNATE消息。

           Shell发送WM_HIBERNATE消息给最顶层的窗口在Z轴相反的位置(reverse Z-order)直到内存被释放,使可用内存超过系统预先的限制。当应用程序接收到一个WM_HIBERNATE消息,它会尽可能减少内存占有程度。这包括释放被缓冲(cached)的数据;释放GDI对象,例如字体,位图和画刷;并销毁任何窗口控件。从本质上来说,应用程序将会减少内存到维持它内部状态的最小值。

           如果发送WM_HIBERNATE消息给后台的应用程序不能释放足够的内存以便使系统离开内存被限制的状态。WM_HIBERNATE消息将会发送给前台程序。如果你正在冬眠的程序开始销毁窗口的控件,你必须确保它不是前台的程序,控件消失不会给用户带来兴奋的感觉而是困惑。

    内存限度

           Windows CE监视系统自由的RAM,并对越来越少的RAM作出响应。当很少内存可用时,Windows CE首先发送WM_HIBERNATE消息,接下来会限制可能的内存分配。下面的两个表显示了Explorer shellPocket PC引发的低内存事件的自由内存级别。Windows CE定义了是个内存状态:normallimitedlowcritical。系统的内存状态依赖于整个系统有多少内存可用。这些限制都比4-KB页要高,因为系统具有内存最小分配限制,就像7-17-2的表。

    7-1 Explorer Shell的内存限度

    事件

    自由内存

    1024-Page Size

    自由内存

    4096-Page Size

    注解

    Limited-memory state

    128 KB

    160 KB

    发送 WM_HIBERNATE 消息给in reverse Z-order的应用程序。释放栈空间并回收利用。

    Low-memory state

    64 KB

    96 KB

    限制虚拟内存分配为16 KB。 显示Low-memory对话框。

    Critical-memory state

    16 KB

    48 KB

    限制虚拟内存分配为8KB

    7-2 Pocket PC的内存限度

    事件

    自由内存

    1024-Page Size

    自由内存

    4096-Page Size

    注解

    Hibernate threshold

    200 KB

    224 KB

    发送 WM_HIBERNATE 消息给in reverse Z-order的应用程序。

    Limited-memory state

    128 KB

    160 KB

    开始关闭在 reverse Z-order上的应用程序。释放栈空间并回收利用。

    Low-memory state

    64 KB

    96 KB

    限制虚拟内存分配为16 KB

    Critical-memory state

    16 KB

    48 KB

    限制虚拟内存分配为8 KB

     

           这些内存状态的影响是共享剩余的财富。首先,WM_HIBERNATE消息被发送给应用程序,并请求减少它们的内存占有率,当应用程序被发送了一个WM_HIBERNATE消息后,系统将检测内存级别,确认是否可用内存在限度之上,如果可用内存不足,WM_HIBERNATE消息将被发送给下一个程序。这会持续到所有程序被发送了WM_HIBERNATE消息。

           Exlporer shellPocket PC的低内存策略在这点上有区别。如果Explorer shell运行时,系统会显示OOMout of memory)对话框,并请用户确认是否关闭一个应用程序或把对象存储区的RAM重新划分给程序内存。如果用户选择了其中之一,仍然没有足够的内存,out of memory对话框将会再次出现,这个过程会重复,直到H/PC有足够的在限度之上的内存。

           Pocket PC来说,操作稍微有些不同。Pocket PC shell自动开始关闭最近最少使用的应用程序,而不询问用户。如果关闭除了前台程序和shell之外的所有程序,仍然没有足够内存,系统将会使用其他的技术来从栈开始清理自由的页,并限制虚拟内存分配。

           如果在任何一个系统上,应用程序被请求关闭却没有关闭,系统在8秒钟后将会清理该应用程序。这就是一个应用程序不要分配大量的栈空间的原因。如果应用程序被关闭而导致低内存环境,很可能是栈空间不能分配,应用程序将被挂起。如果发生在系统请求应用程序关闭以后,可能是清除内存以后没有适当的恢复状态。

           lowcritical-memory状态,应用程序被限制了内存分配的大小。在这些情况下,甚至还有可以满足要求的内存剩余情况下,请求分配大过允许限度的虚拟内存将会被拒绝。记住,并不止是虚拟内存分配被限制,堆分配和栈分配也被禁止,要满足分配请求,那么分配时需要虚拟内存在可允许的限制之上。

           我这里要指出,发送WM_HIBERNATE消息和自动关闭应用程序是由系统的shell执行的。在一个OEM自己可以编写shell的嵌入式系统中,实现WM_HIBERNATE消息和其他内存管理技术是OEM厂商的责任。幸运的是,Microsoft Windows CE PlatForm Builder提供了Exlporer shell实现WM_HIBERNATE消息的源码。

           这里不言而喻,应用程序要检查任何内存分配调用的返回代码,但是因为这里还没说,所以我还是要说。检查内存分配调用的返回代码。在Windows CE中比在桌面版本的Windows中可能有更多的机会导致内存分配失败。应用程序必须很好地实现拒绝内存分配。

           Windows CE不支持完全的Win32内存管理API,但是很清楚这里有对WindowsCE设备受限制内存的足够支持。一个极好的学习Win32错综复杂的内存管理API来源是Jeff Richter’s Programming Applications for Microsoft Windows (Microsoft Press, 1999)。当Jeff和我总结上述相同问题的时候,他在内存管理上花了6章篇幅。

           我们已经看过了程序存储区RAM,这是应用程序可用的RAM。现在是时间,在下两个章节,来看看另一部分RAM,对象存储区。对象存储区支持超过一个文件系统,它也支持一般的注册表APIWindows CE特有的数据库API




    Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=552406

    展开全文
  • 内存泄漏和内存增长

    2017-03-29 19:13:21
    内存泄漏和内存增长不是同一个问题。内存增长的现象是运行时内存占用越来越大,这种现象有可能是有意设计的。内存泄漏是指分配的内存没有释放,导致内存不能归还操作系统。现代的操作系统一般都能在进程结束后回收...
  • 第二讲 内存管理

    千次阅读 2015-09-24 20:52:36
    本讲讲述内存管理方法。本讲包含三个方面的内容: 首先物理内存内存管理的基础,要解决如何对其有效管理的问题,...最后内核如何处理以字节为单位内存请求?这是第三部分内存区管理的内容。主要讲述slab分配器。
  • 内存知识梳理 4. 内存管理

    千次阅读 2017-12-14 22:04:04
    首先物理内存内存管理的基础,要解决如何对其有效管理的问题,因此第一部分讲述页框管理。包括页框的概念、节点和管理区的概念、内核如何申请若干个页框、CPU内如何保障快速申请一个页框、如何用伙伴系
  • HBase 内存配置详解

    万次阅读 2018-03-23 11:46:31
    转载来自:http://www.thebigdata.cn/HBase/30378.htmlHBase最佳实践-内存规划线上HBase集群应该如何进行参数配置?这其实是很多HBase初学者在实践环节都可能会遇到的问题,有些人会选择默认配置,有些人会选择其他...
  • 重学C语言内存管理

    千次阅读 多人点赞 2021-05-02 16:02:30
    内存管理简介2. 内存分类3. 栈区(stack)4. 堆区(heap)5. 全局区(静态区)6. 常量区7. malloc、calloc、realloc函数8. strcpy、memcpy、memmove函数9. 实现动态数组10. 内存越界11. 内存泄露(Memory Leak)12. ...
  • 内存知识大全

    千次阅读 2018-06-08 22:48:22
    在主机中,内存所存储的数据或程序有些是永久的,有些是暂时的,所以内存就有不同形式的功能与作用,而且存储数据的多少也关系着内存的容量大 小,传送数据的快慢也关系着内存的速度,这些都跟内存的种类与功能有关...
  • 基于C语言的内存池的设计与实现

    千次阅读 2016-10-08 19:27:55
    介绍:  设计内存池的目标是为了保证服务器长时间高效的运行,通过对... 此次设计内存池的基本目标,需要满足线程安全性(多线程),适量的内存泄露越界检查,运行效率不太低于malloc/free方式,实现对4-128字节范围
  • 深入理解操作系统[8]:内存管理

    千次阅读 2018-11-16 17:54:34
    内存的连续分配方式对换4. 基本分页式5. 基本分段方式 1 存储系统的层次结构 寄存器:寄存器访问速度最快,完全能与 CPU 协调工作。寄存器的长度一般以字(word)为单位。 主存:CPU的控制部件只能从主存储器中取得...
  • 服务器设计系列:内存管理

    千次阅读 2013-11-09 11:10:52
    从收包到解析,到消息内存的申请,到session结构内存的申请都要小心处理,尽量减少内存数据copy,减少内存动态申请,减少内存检索。为达到这个目的,不同的地方有不同的方法,比如常见的包解析,使用缓冲区偏移以及...
  • OS---内存空间管理

    千次阅读 2018-10-28 23:17:02
    为一个用户程序分配一个连续的内存空间 20世纪六、七十年代的OS中,分类: 单一连续分配 固定分区分配 动态分区分配 动态重定位分区分配 其他 (1)单一连续分配 内存分为系统区和用户区两部分: 系统区:仅...
  • VC使用CRT调试功能来检测内存泄漏

    千次阅读 2008-09-07 17:45:00
    信息来源:csdn C/C++ 编程语言的最强大功能之一便是其动态分配和释放内存,但是中国有句古话:“最大的长处也可能成为最大的弱点”,那么 C/C++ 应用程序正好印证了这句话。在 C/C++ 应用程序开发过程中,动态分配...
  • 1. ORACLE内存全面解析

    千次阅读 2013-05-27 20:32:59
    Oracle的内存配置与oracle性能息息相关。而且关于内存的错误(如4030、4031错误)都是十分令人头疼的问题。可以说,关于内存的配置,是最影响Oracle性能的配置。内存还直接影响到其他两个重要资源的消耗:CPU和IO。 ...
  • 内存泄漏检测

    2013-03-25 16:36:38
    C/C++ 编程语言的最强大功能之一便是其动态分配和释放内存,但是中国有句古话:“最大的长处也可能成为最大的弱点”,那么 C/C++ 应用程序正好印证了这句话。在 C/C++ 应用程序开发过程中,动态分配的内存处理不当是...
  • Golang内存管理机制

    千次阅读 2018-04-27 15:34:12
    golang是一门自带垃圾回收的语言,它的内存分配器和tmalloc(thread-caching malloc)很像,大多数情况下是不需要用户自己管理内存的。最近了解了一下golang内存管理,写出来分享一下,不正确的地方请大佬们指出。那么...
  • C 实现内存

    千次阅读 2012-09-25 16:41:14
    介绍:  设计内存池的目标是为了保证服务器长时间高效的运行,... 此次设计内存池的基本目标,需要满足线程安全性(多线程),适量的内存泄露越界检查,运行效率不太低于malloc/free方式,实现对4-128字节范围内的内
  • Oracle 内存分析

    万次阅读 2012-04-10 17:52:01
    Oracle的内存配置与oracle性能息息相关。而且关于内存的错误(如4030、4031错误)都是十分令人头疼的问题。可以说,关于内存的配置,是最影响Oracle性能的配置。内存还直接影响到其他两个重要资源的消耗:CPU和IO。 ...
  • Redis基本使用

    千次阅读 2016-04-07 11:07:23
    Redis是一个开源的使用C语言编写、开源、支持网络、可基于内存亦可持久化的日志型、高性能的Key-Value数据库,并提供多种语言的API。 它通常被称为 数据结构服务器 ,因为值(value)可以是 字符串(String)、哈希...
  • 如何使用内存快照

    千次阅读 2017-12-29 15:52:47
    C/C++ 编程语言的最强大功能之一便是其动态分配和释放内存,但是中国有句古话:“最大的长处也可能成为最大的弱点”,那么 C/C++ 应用程序正好印证了这句话。在 C/C++ 应用程序开发过程中,动态分配的内存处理不当是...
  • 序号根据osd诞生的顺序排列,并且是全局唯一的。存储了相同PG的osd节点除了向mon节点发送心跳外,还会互相发送心跳信息以检测pg数据副本是否正常。 之前在介绍数据流向时说过,每个osd节点都包含一个journal文件...
  • TCP协议内存空间管理

    千次阅读 2019-03-13 00:28:20
    PROC文件tcp_mem包括3个TCP协议内存空间的控制值,单位为页面数,如下,分别为最小空间值34524页面,承压值46032和最大值69048个页面,此三个值与系统的内存大小相关,示例为一个3G内存的Ubuntu系统。内核中对应的...
  • 检测内存泄漏

    千次阅读 2010-02-26 15:32:00
    最近看了周星星 Blog 中的一篇文章:“VC++6.0中内存泄漏...没有办法,最后我一头栽进 MSDN 库狂搜了一把,功夫不负有心人,我搜出很多有关这方面的资料,没过多久我便基本上就找到了答案...... 首先,检测内存泄漏的基
  • 主要用于打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。 jmap命令可以获得运行中的jvm的堆的快照,从而可以离线分析堆,以检查内存泄漏,检查一些严重影响性能的大对象的创建,...
  • C/C++编程语言的最强大功能之一便是其动态分配和释放内存,但是中国有句古话:“最大的长处也可能成为最大的弱点”,那么C/C++ 应用程序正好印证了这句话。在 C/C++应用程序开发过程中,动态分配的内存处理不当是最...
  • CUDA总结:纹理内存

    万次阅读 2017-01-04 14:35:25
    纹理内存和表面内存(surface memory)实质上是全局内存的一个特殊形态,全局内存被绑定为纹理内存(表面内存),对其的读(写)操作将通过专门的texture cache(纹理缓存)进行,其实称为纹理缓存更加贴切。...
  • 内存基础知识汇总指南

    千次阅读 2011-11-24 13:49:44
    不久前,个人计算机能够拥有超过1或2MB的内存还是闻所未闻的,而今天,大多数的系统需要64MB以执行基本的应用程式,且需要至少256MB或更多内存才能流畅地执行图像动画及多媒体应用等其他效能。 想要理解过去二十...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 23,033
精华内容 9,213
关键字:

内存基本单位的序号