精华内容
下载资源
问答
  • 深入理解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有垃圾收集机制,但内存溢出异常离我们仍然并不遥远,本章只是讲解了各个区域出现内存溢出异常的原因。

    展开全文
  • 内存管理

    千次阅读 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推荐使用后者。

     

     

    展开全文
  • Java虚拟机中对象内存的分配情况

    千次阅读 2019-02-20 17:50:16
    本文主要是介绍下对象在虚拟机中的内存布局分配情况。 JVM的内存对象介绍[创建和访问] 对象的内存布局   在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头,实例数据和对齐填充。 对象头 ...

      在前面的文章介绍了对象在虚拟机中的创建过程。本文主要是记录下对象在虚拟机中的内存布局分配情况。


    JVM的内存对象介绍[创建和访问]


    对象的内存布局

      在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头,实例数据和对齐填充。

    序号 区域 说明
    1 对象头 存储对象的hashCode或锁的相关信息
    2 实例数据 存储对象实例相关的数据
    3 对齐填充 占位符的作用

    1.对象头

      对象头包含两部分

    第一部分内容

      第一部分存储自身的运行时数据,如hashCode,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳,对象分代年龄,这部分信息称为"Mark Word",Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据自己的状态复用自己的存储空间。32位或者64位存储也有区别。
      例如:在 32 位的 HotSpot 虚拟机中,如果对象处于未被锁定的状态下,那么 Mark Word 的 32bit 空间中的 25bit 用于存储对象哈希码,4bit 用于存储对象分代年龄,2bit 用于存储锁标志位,1bit 固定为 0,如下表所示

    锁状态 25bit 4bit 1bit是否是偏向锁 2bit锁标志位
    无锁状态 对象的hashCode 对象的分代年龄 0 01

    对象在其他状态下(轻量级锁定,重量级锁定,GC标记,可偏向)对象的存储内容如下:

    存储内容 标志位 状态
    对象哈希码,对象分代年龄 01 未锁定
    指向锁记录的指针 00 轻量级锁定
    指向重量级锁的指针 10 膨胀(重量级锁定)
    空,不需要记录信息 11 GC标记
    偏向线程ID,偏向时间戳,
    对象分代年龄
    01 可偏向

    在这里插入图片描述

    第二部分内容

      第二部分存储类型的指针,既对象执行它的类元数据的执行(方法区),虚拟机通过这个指针来确定这个对象是那个类的实例。如果对象是一个Java数组,在对象头中还须有一块用于记录数组长度的数据,因为虚拟机可通过普通Java对象的元数据信息确定Java对象的大小,但从数组的元数据中无法确定数组的大小。

    2.实例数据(Instance Data)

      实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在 Java 源码中定义顺序的影响。

    3.对齐填充(Padding)

      对齐填充不是必然存在的,没有特别的含义,它仅起到占位符的作用。
      由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,也就是说对象的大小必须是 8 字节的整数倍。对象头部分是 8 字节的倍数,所以当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

    对象大小估算

      32 位系统下,当使用 new Object() 时,JVM 将会分配 8(Mark Word+类型指针) 字节的空间,128 个 Object 对象将占用 1KB 的空间。如果是 new Integer(),那么对象里还有一个 int 值,其占用 4 字节,这个对象也就是 8+4=12 字节,对齐后,该对象就是 16 字节。
    以上只是一些简单的对象,那么对象的内部属性是怎么排布的?

    Class A {
        int i;
        byte b;
        String str;
    }
    

      其中对象头部占用 ‘Mark Word’4 + ‘类型指针’4 = 8 字节;byte 8 位长,占用 1 字节;int 32 位长,占用 4 字节;String 只有引用,占用 4 字节;
    那么对象 A 一共占用了 8+1+4+4=17 字节,按照 8 字节对齐原则,对象大小也就是 24 字节。
      这个计算看起来是没有问题的,对象的大小也确实是 24 字节,但是对齐(padding)的位置并不对:
      在 HotSpot VM 中,对象排布时,间隙是在 4 字节基础上的(在 32 位和 64 位压缩模式下),上述例子中,int 后面的 byte,空隙只剩下 3 字节,接下来的 String 对象引用需要 4 字节来存放,因此 byte 和对象引用之间就会有 3 字节对齐,对象引用排布后,最后会有 4 字节对齐,因此结果上依然是 7 字节对齐。此时对象的结构示意图,如下图所示:
    在这里插入图片描述
    参考资料:<深入理解Java虚拟机>

    展开全文
  • 内存溢出和内存泄漏的区别

    千次阅读 2015-07-11 15:55:31
    内存溢出 out of memory,是指程序在...内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会占光。 memory leak会

    内存泄露是指程序在运行过程中动态申请的内存空间不再使用后没有及时释放,从而很可能导致应用程序内存无线增长。更广义的内存泄露包括未对系统的资源的及时释放,比如句柄等。

    内存溢出即用户在对其数据缓冲区操作时,超过了其缓冲区的边界;尤其是对缓冲区写操作时,缓冲区的溢出很可能导致程序的异常。

    一.内存泄露

    “知己知彼,方能百战不殆”,如果我们能够比较清楚的了解在编程的时候哪些情况容易导致内存泄露,通过避免这些糟糕的情况,从提高代码的质量本身出发,来抵御潜在导致内存泄露的发生。

    1.1先来看看内存泄露可能发生的一些场景:

    (1)程序员常常忽略在所有的分支都加上内存的回收处理

    1. int size = 100;  
    2. char *pointer = new char[size];  
    3. if (!xxxAPI(pointer, size)  
    4. {  
    5.     return;  
    6. }  
    7. delete[]pointer;  

    (2)构造函数中申请空间,析构函数中释放空间

    (3)库函数或者系统API会在内部申请空间,然后返回指针给用户;以strdup为例

    1. char *str;  
    2. str = strdup("hello World!");  

      strdup申请了一段空间存储字符串"hello World",然后返回空间地址,这个时候用户经常会忘记释放str;

    上面只是列出了简单的三种情况,尤其在一个复杂的大型系统中,一段内存的使用周期太长或者嵌套太深,还需要程序员自己去把握。

    1.2.内存泄露的检测

    (1)利用内存泄露检测工具

    常用的有 BoundsCheaker、Deleaker、Visual Leak Detector等,工具毕竟熟能生巧,用户选择先自己喜欢的一款去用即可。

    BoundsChecker没有找到win7下支持VS2005的破解版,用盗版的伤不起啊。

    (2)使用Deleaker(本文采用vs2005)进行内存泄露检查

    如下图所示:

    A) Deleak安装后自动集成到VS中,在VS“工具”菜单中会加入一个“Deleaker”菜单项。

    B) Deleaker能够对GDI,USER对象以及句柄进行检测,是否及时释放。

    C) Deleaker能够检测泄露的内存发生地点,即展示其函数栈;双击能够转到相应的文件;

    PS:Deleaker对中文不支持

    如果有内存泄露Deleaker会在程序调试完弹出对话框如下图所示:


    (3)使用Viual Leak detector

    使用Deleak方便灵活,除了其对中文路径支持问题,但感觉和vs的集成度并不是很高。

    Viual Leak detector安装后,要在VS中设置相应的头文件和库路径,在Debug模式下如果要检测相应源文件的内存泄露,则加上"#include <vld.h>"即可;

    这样在检测内存泄露,可以在VS的输出窗口进行输出,感觉和VS的集成度更高,结果如下图所示:


    同样能够显示 内存泄露处的 调用栈,并且通过双击也可以跳转到文件的内存泄露行,个人还是比较喜欢这种方式的。

    (4)在没有工具的情况下,使用crtdbg.h中的api也是个很棒的选择

    在MFC中可以看到在程序退出的时候,输出框内结尾部分输出内存泄露,并且点击可以跳转到内存泄露的代码处。

    那么在console程序下呢,当然我们同样可以做到(做那些MFC帮我们完成了的细节);

    A) _CrtSetDbgFlag函数

    1. int _CrtSetDbgFlag(  
    2. int newFlag  
    3. );  

    (函数详细信息参考:http://msdn.microsoft.com/zh-cn/library/5at7yxcs.aspx

    这个函数用于控制debug模式下堆管理的分配行为;

    在main函数开始处添加:

    1. _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);  
    2. //_CRTDBG_REPORT_FLAG:表示获取当前的标示位  
    3. //_CRTDBG_LEAK_CHECK_DF:表示检测内存泄露  

    则如果出现内存泄露Debug结束后,输出框将输出:

    {150}表示申请的第150块申请的内存空间;

    B) 显示内存泄露所在的文件以及行

    能够知道有内存泄露是不够的,更需要的信息是哪里内存泄露了?

    我们可以在每个源文件的开头定义写这样一条宏定义:

    1. //根据__FILE___和__LINE__能够确定文件和行  
    2. #define new   new(_NORMAL_BLOCK, __FILE__, __LINE__)    


    C) 显示内存泄露处的堆栈

    1. //lBreakAlloc,在申请的堆区序号为lBreakAlloc处设置一个断点  
    2. long _CrtSetBreakAlloc( long lBreakAlloc );  
    (函数详细信息参考:http://technet.microsoft.com/zh-cn/library/aa246759

    此函数在指定的申请堆区空间次序处(即lBreakAlloc)设置断点;

    很喜欢这个函数,这个函数结合"A)"中提到的{150},比如使用方法:

    1. _CrtSetBreakAlloc(150); //则在第150次申请堆空间时候设置断点  

    这样就可以看到函数调用栈,从而帮助我们更加精确的定位程序泄露的位置(调用栈可是个好玩意)。

    个人感觉这种方式虽然要手动的修改代码,但其功能却比前两个工具的有效,因为能够在程序运行的时候查看调用栈,这就意味着能够调试程序

    展示结果如下图所示(自动在第150次申请堆空间处中断):


    二.内存溢出

    本篇最想分享的就是内存溢出的调试方法,内存溢出能够导致程序异常,而且这种异常使程序员难以下手。

    2.1 内存溢出导致的异常症状

    (1)内存异常经常产生的程序报错,如下图所示:

    (2)有可能调试的时候不错,运行的时候出错,而且随机出现,这绝对让人很头疼的问题。

    (3)庆幸的是,如果编译后的debug程序,直接运行后,如果出错,可以选择调试程序(如下图所示);

    千万别以为麻烦就此可以解决了,进入调试状态后,发现出错的地方根本代码没有任何问题,可见内存溢出是个多么令人讨厌的家伙;

    2.2 解决方法

    虽然他是那么可恶,但也不要忘了是程序员自己一手创建了出来的。也不要灰心,困难总是有方法去解决的。

    (1)等到生病的时候,再去看病,或许已经晚了;最好是提前做好预防准备;

            A) 比如在程序中多使用strcpy_s、memcpy_s等具有缓冲区大小检查的函数,去取代strcpy、memcpy等;

            B)给工程设置编译选项/WX开启(“将警告视为错误”),严格要求自己,这样很可能避免了不少潜在的bug;

            C)  对自己的代码做好单元测试

    (2)如果出现了这种难以查找的错误,可以从程序源码着手,查看一些和内存操作相关的函数,比如strcpy、memcpy等。

    本人曾经在项目中就遇到用一个项目组成员在使用,strcpy拷贝一个字符串到一个空间不够的内存,从而导致程序异常:

    1. //拷贝字符串,并且返回新的字符串地址  
    2. char * string_copy(const char *source)  
    3. {  
    4.     char *p_string;  
    5.     int string_len;  
    6.     string_len = strlen(source);  
    7.     if(source == NULL)  
    8.     {  
    9.         p_string = (char *)malloc(2*sizeof(char));  
    10.         strcpy(p_string, "");  
    11.     }  
    12.     else  
    13.     {   //这里错误 string_len+1  
    14.     p_string = (char *)malloc((string_len)*sizeof(char));   
    15.     strcpy(p_string, source);  
    16.     }  
    17.     return p_string;  
    18. }  

    静态地去检查代码方法比较慢,而且不适用于大工程。

    (3)检查工具

    幸运的是本人接触了一个代码量较大的工程,不幸的是发生了内存溢出问题,而导致程序异常。而且出现的症状,就是调试不错,运行出错,

    而且随机出现,并且内存异常的代码处,代码没有任何问题。这个问题纠结了至少一个月,病极乱投医,但找了一些工具大多用于检查内存泄露的。

    最终确定了两个工具:

    A)BoudsChecker,除了能够检查内存泄露,也能检查内存溢出问题;可惜的是没有找到Win7 下支持VS2005的破解版本

    B)AppVerifier,专门用来检测那些用普通方法检测不出的意想不到的bug(比如内存溢出、错误句柄使用等)。而且AppVerifier使用非常简单,

    只需要绑定需要测试的的应用程序,并且勾选测试项后保存,使用VS2005进行调试即可。AppVier:

    PS:文中所称的内存溢出,用英文专业术语叫做heap corruption





    内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

    内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

    memory leak会最终会导致out of memory!

    内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。 

        内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出!比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出. 

       以发生的方式来分类,内存泄漏可以分为4类: 

    1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。 
    2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。 
    3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。 
    4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。 

    从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到 






    展开全文
  • C++内存泄露和内存管理

    千次阅读 2016-05-24 22:26:12
    一直没有找到系统的讲解C++内存管理的文章,所以结合自己的工作经验,以及网友的一些总结,分析了内存泄露检测的方法,一般原则,最后还补充了内存溢出
  • 为什么32位cpu只支持4G内存

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

    万次阅读 2020-07-06 15:54:20
    JVM内存模型 JVM的内存模型也就是JVM中的内存布局,不要与java的内存模型(与多线程相关)混淆。 下图是jdk8 jvm内存模型图: 程序计数器 程序计数器是当前线程所执行的字节码的行号指示器。 JVM支持多个线程同时...
  • 深入理解Java虚拟机-垃圾回收器与内存分配策略

    万次阅读 多人点赞 2020-01-04 13:08:32
    Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来。 文章目录概述对象已死吗引用计数法可达性分析算法再谈引用生存还是死亡回收方法区垃圾收集算法标记-清除...
  • Linux内存管理之高端内存映射

    千次阅读 2012-07-16 16:07:16
    一:引子 我们在前面分析过,在...通常,我们把物理地址超过896M的区域称为高端内存。内核怎样去管理高端内存呢?今天就来分析这个问题。 内核有三种方式管理高端内存。第一种是非连续映射。这我们在前面的vmalloc
  • 内存溢出和泄露的区别

    千次阅读 2015-06-13 12:08:15
    内存溢出 out of memory,是指程序在...内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会占光。 memory leak会
  • 内存知识梳理 4. 内存管理

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

    万次阅读 2020-08-15 11:01:26
    这段代码的思想是将主内存区分为 4K 为一段来管理,mem_map 的一个 entry 项代表一个 4K 的页,当其值为 0 时,表示该页没有占用,大于 0 表示占用,大于 1 表示页面共享。MAP_NR 求给定地址 addr 所在页面在...
  • Oracle内存详解之一 整体架构

    千次阅读 2011-09-02 21:25:23
    Oracle的内存配置与oracle性能息息相关。关于内存的配置,是最影响Oracle性能的配置。内存还直接影响到其他两个重要资源的消耗:CPU和IO.    先看Oracle内存存储的主要内容是什么: 程序代码(PLSQL、Java); ...
  • 内存知识大全

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

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

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

    千次阅读 2018-10-28 23:17:02
    为一个用户程序分配一个连续的内存空间 20世纪六、七十年代的OS中,分类: 单一连续分配 固定分区分配 动态分区分配 动态重定位分区分配 其他 (1)单一连续分配 内存分为系统区和用户区两部分: 系统区:仅...
  • linux内存管理之数据结构

    千次阅读 2016-04-21 23:46:31
    linux内存管理之数据结构linux内存管理之数据结构 一物理空间管理 1 页表项 2 物理页面管理对象page 二内存分区 1 过去的分区 2 当下的分区情况 三 虚拟空间管理 1 进程虚存区域 2 进程地址空间 3 进程地址空间和...
  • GDB观察栈的内存布局

    千次阅读 2017-03-29 12:38:43
    进程的内存布局如下图所示,栈是其中一块向下(低地址处)增长的内存。  栈的英文是stack,堆的英文是heap,很多人把stact翻译成堆栈,是不对的。  栈由栈帧组成。当一个函数调用时,栈会为这个函数分配...
  • 缓存/内存

    千次阅读 2020-04-09 14:19:10
    随机存取存储器(Random Access Memory, RAM),也称主存(或内存),与CPU直接交换数据的内部存储器,可随时读写,速度很快.主存(Main Memory)计算机内最主要的存储器,用于加载各种程序与数据以供CP...
  • 内存溢出 out of memory,...内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会占光。 memory leak会最终会导...
  • 内存分区分配算法

    千次阅读 2016-04-21 22:15:15
    称为分类搜索法,是将空闲分区根据其容量按照大小进行分类,对每类相同容量的所有空闲分区,单独设立一个空闲分区链表,这样,就会在系统中存在多个空闲分区链表,同时会在内存中设立一张管理索引表,该表的每一个...
  • java内存模型与线程

    千次阅读 2016-05-15 23:20:23
    Java内存模型规定了所有的变量都存储在主内存中(此处的主内存仅仅指虚拟机内存),每条线程还有自己的工作内存,线程的工作内存中保存了线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存...
  • Oracle 内存分析

    万次阅读 2012-04-10 17:52:01
    Oracle的内存配置与oracle性能息息相关。而且关于内存的错误(如4030、4031错误)都是十分令人头疼的问题。可以说,关于内存的配置,是最影响Oracle性能的配置。内存还直接影响到其他两个重要资源的消耗:CPU和IO。 ...
  • 1、pmap查看进程内存运行命令使用pmap可以查看某一个进程(非java的也可以)的内存使用使用情况,命令格式:pmap 进程id示例说明pmap 12358 第一列,内存块起始地址 第二列,占用内存大小 第三列,内存权限 ...
  • 内存管理单元MMU

    千次阅读 2012-06-24 16:27:48
    MMU使得每个用户进程拥有自己独立的地址空间,并通过内存访问权限的检查保护每个进程所用的内存其他进程破坏。 重点就在于地址映射:页表的结构与建立、映射的过程。   1、S3C2440 MMU地址变换过程 1)...
  • 一维数组在内存中的存放

    千次阅读 2018-04-04 19:51:20
    1 硬盘上不可能运行程序的,必须在内存中运行。2 低地址到高地址存储 3 数组元素通常也称为下标变量。4 在C语言中,只能逐个地使用下标变量, 不能用一个语句输出整个数组5 int a[10]和t=a[6]分别是定义数组长度为...
  • 主要用于打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。 jmap命令可以获得运行中的jvm的堆的快照,从而可以离线分析堆,以检查内存泄漏,检查一些严重影响性能的大对象的创建,...
  • 内存管理 -3

    千次阅读 多人点赞 2018-10-08 11:39:03
    1.分区分配内存管理方式的主要保护措施是(A) 界地址保护 程序代码保护 数据保护 栈保护 解析: 每个进程都拥有自己独立的进程空间,如果一个进程在运行时所产生的地址在其地址空间之外,...
  • 内存控制单元(MMU)通过一种称为分段单元(segmentation unit)的硬件电路把一个逻辑地址转换成线性地址;接着,第二个称为分页单元(paging unit)的硬件电路把线性地址转换成一个物理地址。 分段管理机制 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 40,351
精华内容 16,140
关键字:

内存的序号被称为