精华内容
下载资源
问答
  • 内存分配方式和控制内存分配

    千次阅读 2017-08-12 13:41:18
    内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不在,内存泄漏几乎在每个C++...

        内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不在,内存泄漏几乎在每个C++程序中都会发生,因此要想成为C++高手,内存管理一关是必须要过的,除非放弃C++,转到Java或者.NET,他们的内存管理基本是自动的,当然你也放弃了自由和对内存的支配权,还放弃了C++超绝的性能。

     

    一、内存分配方式

     

    1、简介

     

        在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

    • <font size=4>:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
    • <font size=4>:就是那些由 new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
    • <font size=4>自由存储区:就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
    • <font size=4>全局/静态存储区全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
    • <font size=4>常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。


    2、明确区分堆与栈

     

        堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。
    首先,我们举一个例子:

      void f() { int* p=new int[5]; }

        这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中。

        这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete [] p,这是为了告诉编译器:我删除的是一个数组,编译器就会根据相应的Cookie信息去进行释放内存的工作。

    堆和栈究竟有什么区别

        好了,我们回到我们的主题:堆和栈究竟有什么区别?
     主要的区别由以下几点:
     (1). 管理方式不同
     (2). 空间大小不同
     (3). 能否产生碎片不同
     (4). 生长方向不同
     (5). 分配方式不同
     (6). 分配效率不同
     

        管理方式对于来讲,是由编译器自动管理,无需我们手工控制;对于来说,释放工作由程序员控制,容易产生memory leak。
     

        空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:
     打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。
     注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。

     碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。

     生长方向对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

     分配方式堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

     分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高堆则是C/C++函数库提供的它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
     从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
     虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
     无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的。

     

    二、控制内存分配

     

        在嵌入式系统中使用C++的一个常见问题是内存分配,即对new 和 delete 操作符的失控。
     具有讽刺意味的是,问题的根源却是C++对内存的管理非常的容易而且安全。具体地说,当一个对象被消除时,它的析构函数能够安全的释放所分配的内存。
     这当然是个好事情,但是这种使用的简单性使得程序员们过度使用new 和 delete,而不注意在嵌入式C++环境中的因果关系。并且,在嵌入式系统中,由于内存的限制,频繁的动态分配不定大小的内存会引起很大的问题以及堆破碎的风险。
     作为忠告,保守的使用内存分配是嵌入式环境中的第一原则
     但当你必须要使用new和delete时,你不得不控制C++中的内存分配。你需要用一个全局的new 和delete来代替系统的内存分配符,并且一个类一个类的重载new和delete。
     一个防止堆破碎的通用方法是从不同固定大小的内存持中分配不同类型的对象。对每个类重载new 和delete就提供了这样的控制。

     

    1、重载全局的new和delete操作符


     可以很容易地重载new 和 delete 操作符,如下所示:

     

    void * operator new(size_t size){
        void *p = malloc(size);
        return (p);
    }
    void operator delete(void *p){
        free(p);
    }

        这段代码可以代替默认的操作符来满足内存分配的请求。出于解释C++的目的,我们也可以直接调用malloc() 和free()。
     也可以对单个类的new 和 delete操作符重载。这是你能灵活的控制对象的内存分配。

     

    #include<iostream>
    #include<malloc.h>
    using namespace std;
    class TestClass
    {
        public:
            void * operator new(size_t size);//size_t为无符号整形
            void operator delete(void *p);
            void * operator new [] (size_t size);
            void operator delete [] (void *p);
    };
    void *TestClass::operator new(size_t size)
    {
        void *p=malloc(size);
        return p;
    }
    void TestClass::operator delete(void *p)
    {
        cout << (int)p << endl;
        free(p);
    }
    void *TestClass::operator new [] (size_t size)
    {
        void *p = malloc(size);
        return (p);
    }
    void TestClass::operator delete [] (void *p)
    {
        cout << (int)p << endl;
        free(p);
    }
    int main(void)
    {
        TestClass *p1 = new TestClass;
        delete  p1;
        TestClass *p2 = new TestClass[10];
        delete [] p2;
        system("pause");
    }

        但是注意:对于多数C++的实现,new[]操作符中的个数参数是数组的大小加上额外的存储对象数目的一些字节。在你的内存分配机制重要考虑的这一点。你应该尽量避免分配对象数组,从而使你的内存分配策略简单。
     

    三、关于内存碎片

     

        首先看碎片,32位系统的内存是按“页”管理的,一页内存为64K,只有当前在使用的页面才会在内存中,其他页面不一定总是在内存。因此,分配连续的内存时,当请求少于64K,系统会尽量将其分配在一个内存页面中,当大于64K时,会分配在连续的页面中。以实例说明,例如首先请求30K内存,那么会在第一页分配,第二次请求50K,就必须在第二页面分配。于是第一次返回的地址为0,第二次为64K,30K~64K之间的内存就是所谓的“碎片”。可以将“碎片”简单理解为两块已分配内存之间的空间。当然,“碎片”也可能被利用,但是考虑一种极端的情况:如果一台电脑拥有4G内存,但是每个页面都只分配了32K,那么你将不能申请一片大于32K的内存,即使目前你的物理内存还有2G没有使用。这种情况是很常见的,当程序比较大时如果你没作好这些管理,你会发现new个3~5百MB内存会经常失败。
        系统中的new会实施一些算法或者策略,防止内存碎片过快产生,这些算法类似于数据结构中的“堆”,所以new被称之为“堆分配”。但是,系统的堆管理策略是宏观的,通用的。你只要使用它,一定会产生内存碎片。同时,随着堆的规模的加大,会有很多时间浪费在页面在主存与虚拟内存的交换中,这是因为一般情况下,系统返回给你的内存指针是不能改变的,试想你的程序中new了一个新指针,可这片内存不知什么时候被系统换到其他地方了,那么你的程序离奔溃就不远了。这说明在C++中,出现内存碎片后系统是无法执行“碎片整理”的,然而在底层的windows接口中,你能使用“内存句柄”代替指针,内存句柄代表的内存是操作系统管理的而不是地址本身,因此这种情况下操作系统能帮你完成“碎片整理”。只是,内存句柄即使在微软自己的平台上也不及指针通用,各种内库的接口中,绝大部分只认指针,因此你可能在“内存句柄”与指针间不停转换消耗掉程序的时间。
        重写内存管理要根据实际需要,这没有统一的方法。最简单的做法是让new返回一个全局数组的地址。全局数组的内存空间在程序启动时就初始化好了,因此你立即就能获取到地址并且这个分配一定是成功的(要是内存耗尽,程序会在启动时挂掉)。全局数组好处在于它的内存一定是连续的,32位系统上能保证2G左右的长度,因此对于操作系统而言可以做到消除碎片,至于如何高效使用这些内存就是程序员的事情了。最后,全局数组的方法也要注意64K对其,否则程序性能会因为内存交换收到影响,特别是当内存使用量很大的时候。

    参考:《c++内存管理技术内幕》

    展开全文
  • ESXi 内存分配原理

    千次阅读 2019-11-05 10:28:02
    运行在ESXi主机上的虚拟机分配内存之和可以超过物理机的实际内存大小,这个技术叫做超额分配(overcommitment),即使单个虚拟机的内存分配值都可以超分。但是超分的结果就是可能会引起内存资源竞争,从而有可能影响...

    上一篇我们详细讲述了CPU的调度原理,本篇讲一下内存的分配过程。

     

    运行在ESXi主机上的虚拟机分配内存之和可以超过物理机的实际内存大小,这个技术叫做超额分配(overcommitment),即使单个虚拟机的内存分配值都可以超分。但是超分的结果就是可能会引起内存资源竞争,从而有可能影响到性能。

     

    VMkernel中有另外一个组件叫做memory allocator,用来负责内存资源的分配,(负责CPU调度的是scheduler,上一篇提到过),如下图:

     

     

    那么内存分配的过程究竟是怎样的呢?我们来详细描述一下:

     

    • 首先,内存分为三个级别,主机物理内存,客户机物理内存(虚拟机分配的内存大小),客户机虚拟内存(应用程序使用的内存)。虚拟机开机后,VMkernel 不会立即分配所有的客户机物理内存给虚拟机,初始阶段是根据OS和应用程序的需要按需分配,如下图所示。这个时候,VMkernel会把物理机的内存页面地址映射给客户机物理内存页面,从而实现内存虚拟化。

     

     

    • 其次,当OS和应用程序释放内存的时候,Guest OS会把这些内存页面地址放在free list,但是VMkernel并不知道这个list,也就是说,VMkernel是不知道Guest里边有内存释放,也不会知道哪些应用程序正在占用哪些内存页面,它只知道虚拟机分配的内存大小。(是不是感觉VMkernel挺傻的,自己的东西分给别人了,但是别人用不用也不知道,直到最后自己的东西被分完了。)如下图所示:

     

     

    其中,free list是Guest OS没有分配给应用程序的,左侧第一个“红色”内存页面是应用程序暂时不用的,被称为“Idle--闲置”内存,其他三个内存页面(黄色,蓝色,灰色)是应用程序正在使用的,被称为“Active--活动”内存,所以Guest OS会把内存分为下图的树形结构:

     

     

    • 最后,当内存超分时,虚拟机请求的内存达到或者超过实际的物理内存大小时,VMkernel就要对分配出去的内存进行回收,从而触发四种内存回收机制,目的是为了保证所有的虚拟机都可以共享到内存资源,默认情况下,物理内存是平均分配的,但是如果管理员设置了资源控制参数(预留,限制,份额),那么VMkernel就会按照优先级对虚拟机的内存请求进行重分配。

       

    关于超分情况下,在内存竞争时,VMkernel如何进行内存回收呢?且听下回分解~

    展开全文
  • JVM初探- 内存分配、GC原理与垃圾收集器

    万次阅读 多人点赞 2016-12-30 20:45:21
    JVM初探- 内存分配、GC原理与垃圾收集器 JVM内存分配与回收大致可分为如下4个步骤: ...VM内存分配策略: 对象内存主要分配在新生代Eden区, 如果启用了本地线程分配缓冲, 则优先在TLAB上分配, 少数情况能会直接分配

    JVM初探- 内存分配、GC原理与垃圾收集器

    标签 : JVM


    JVM内存的分配与回收大致可分为如下4个步骤: 何时分配 -> 怎样分配 -> 何时回收 -> 怎样回收.
    除了在概念上可简单认为new时分配外, 我们着重介绍后面的3个步骤:


    I. 怎样分配- JVM内存分配策略

    对象内存主要分配在新生代Eden区, 如果启用了本地线程分配缓冲, 则优先在TLAB上分配, 少数情况能会直接分配在老年代, 或被拆分成标量类型在栈上分配(JIT优化). 分配的规则并不是百分百固定, 细节主要取决于垃圾收集器组合, 以及VM内存相关的参数.


    对象分配

    • 优先在Eden区分配
      JVM内存模型一文中, 我们大致了解了VM年轻代堆内存可以划分为一块Eden区和两块Survivor区. 在大多数情况下, 对象在新生代Eden区中分配, 当Eden区没有足够空间分配时, VM发起一次Minor GC, 将Eden区和其中一块Survivor区内尚存活的对象放入另一块Survivor区域, 如果在Minor GC期间发现新生代存活对象无法放入空闲的Survivor区, 则会通过空间分配担保机制使对象提前进入老年代(空间分配担保见下).
    • 大对象直接进入老年代
      Serial和ParNew两款收集器提供了-XX:PretenureSizeThreshold的参数, 令大于该值的大对象直接在老年代分配, 这样做的目的是避免在Eden区和Survivor区之间产生大量的内存复制(大对象一般指 需要大量连续内存的Java对象, 如很长的字符串和数组), 因此大对象容易导致还有不少空闲内存就提前触发GC以获取足够的连续空间.

    对象晋升

    • 年龄阈值
      VM为每个对象定义了一个对象年龄(Age)计数器, 对象在Eden出生如果经第一次Minor GC后仍然存活, 且能被Survivor容纳的话, 将被移动到Survivor空间中, 并将年龄设为1. 以后对象在Survivor区中每熬过一次Minor GC年龄就+1. 当增加到一定程度(-XX:MaxTenuringThreshold, 默认15), 将会晋升到老年代.

    • 提前晋升: 动态年龄判定
      然而VM并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代: 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代, 而无须等到晋升年龄.


    II. 何时回收-对象生死判定

    (哪些内存需要回收/何时回收)

    在堆里面存放着Java世界中几乎所有的对象实例, 垃圾收集器在对堆进行回收前, 第一件事就是判断哪些对象已死(可回收).


    可达性分析算法

    在主流商用语言(如Java、C#)的主流实现中, 都是通过可达性分析算法来判定对象是否存活的: 通过一系列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链/Reference Chain, 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的, 如下图: Object5、6、7 虽然互有关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象:

    • 在Java, 可作为GC Roots的对象包括:
      1. 方法区: 类静态属性引用的对象;
      2. 方法区: 常量引用的对象;
      3. 虚拟机栈(本地变量表)中引用的对象.
      4. 本地方法栈JNI(Native方法)中引用的对象。

    注: 即使在可达性分析算法中不可达的对象, VM也并不是马上对其回收, 因为要真正宣告一个对象死亡, 至少要经历两次标记过程: 第一次是在可达性分析后发现没有与GC Roots相连接的引用链, 第二次是GC对在F-Queue执行队列中的对象进行的小规模标记(对象需要覆盖finalize()方法且没被调用过).


    III. GC原理- 垃圾收集算法

    分代收集算法 VS 分区收集算法

    • 分代收集
      当前主流VM垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据对象存活周期的不同将内存划分为几块, 如JVM中的 新生代老年代永久代. 这样就可以根据各年代特点分别采用最适当的GC算法:
      • 在新生代: 每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集.
      • 在老年代: 因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”“标记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存.
    • 分区收集
      上面介绍的分代收集算法是将对象的生命周期按长短划分为两个部分, 而分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间.
      在相同条件下, 堆空间越大, 一次GC耗时就越长, 从而产生的停顿也越长. 为了更好地控制GC产生的停顿时间, 将一块大的内存区域分割为多个小块, 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次GC所产生的停顿.

    分代收集

    新生代-复制算法

    该算法的核心是将可用内存按容量划分为大小相等的两块, 每次只用其中一块, 当这一块的内存用完, 就将还存活的对象复制到另外一块上面, 然后把已使用过的内存空间一次清理掉.

    (图片来源: jvm垃圾收集算法)

    这使得每次只对其中一块内存进行回收, 分配也就不用考虑内存碎片等复杂情况, 实现简单且运行高效.

    现代商用VM的新生代均采用复制算法, 但由于新生代中的98%的对象都是生存周期极短的, 因此并不需完全按照1∶1的比例划分新生代空间, 而是将新生代划分为一块较大的Eden区和两块较小的Survivor区(HotSpot默认Eden和Survivor的大小比例为8∶1), 每次只用Eden和其中一块Survivor. 当发生MinorGC时, 将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor上, 最后清理掉Eden和刚才用过的Survivor的空间. 当Survivor空间不够用(不足以保存尚存活的对象)时, 需要依赖老年代进行空间分配担保机制, 这部分内存直接进入老年代.


    老年代-标记清除算法

    该算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象(可达性分析), 在标记完成后统一清理掉所有被标记的对象.

    该算法会有以下两个问题:
    1. 效率问题: 标记和清除过程的效率都不高;
    2. 空间问题: 标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集.


    老年代-标记整理算法

    标记清除算法会产生内存碎片问题, 而复制算法需要有额外的内存担保空间, 于是针对老年代的特点, 又有了标记整理算法. 标记整理算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理, 而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存.


    永久代-方法区回收

    • 在方法区进行垃圾回收一般”性价比”较低, 因为在方法区主要回收两部分内容: 废弃常量无用的类. 回收废弃常量与回收其他年代中的对象类似, 但要判断一个类是否无用则条件相当苛刻:
      1. 该类所有的实例都已经被回收, Java堆中不存在该类的任何实例;
      2. 该类对应的Class对象没有在任何地方被引用(也就是在任何地方都无法通过反射访问该类的方法);
      3. 加载该类的ClassLoader已经被回收.

    但即使满足以上条件也未必一定会回收, Hotspot VM还提供了-Xnoclassgc参数控制(关闭CLASS的垃圾回收功能). 因此在大量使用动态代理、CGLib等字节码框架的应用中一定要关闭该选项, 开启VM的类卸载功能, 以保证方法区不会溢出.


    补充: 空间分配担保

    在执行Minor GC前, VM会首先检查老年代是否有足够的空间存放新生代尚存活对象, 由于新生代使用复制收集算法, 为了提升内存利用率, 只使用了其中一个Survivor作为轮换备份, 因此当出现大量对象在Minor GC后仍然存活的情况时, 就需要老年代进行分配担保, 让Survivor无法容纳的对象直接进入老年代, 但前提是老年代需要有足够的空间容纳这些存活对象. 但存活对象的大小在实际完成GC前是无法明确知道的, 因此Minor GC前, VM会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小, 如果条件成立, 则进行Minor GC, 否则进行Full GC(让老年代腾出更多空间).
    然而取历次晋升的对象的平均大小也是有一定风险的, 如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure, 老年代也无法存放这些对象了), 此时就只好在失败后重新发起一次Full GC(让老年代腾出更多空间).


    IX. GC实现- 垃圾收集器

    GC实现目标: 准确、高效、低停顿、空闲内存规整.


    新生代

    1. Serial收集器

    Serial收集器是Hotspot运行在Client模式下的默认新生代收集器, 它的特点是 只用一个CPU/一条收集线程去完成GC工作, 且在进行垃圾收集时必须暂停其他所有的工作线程(“Stop The World” -后面简称STW).

    虽然是单线程收集, 但它却简单而高效, 在VM管理内存不大的情况下(收集几十M~一两百M的新生代), 停顿时间完全可以控制在几十毫秒~一百多毫秒内.


    2. ParNew收集器

    ParNew收集器其实是前面Serial的多线程版本, 除使用多条线程进行GC外, 包括Serial可用的所有控制参数、收集算法、STW、对象分配规则、回收策略等都与Serial完全一样(也是VM启用CMS收集器-XX: +UseConcMarkSweepGC的默认新生代收集器).

    由于存在线程切换的开销, ParNew在单CPU的环境中比不上Serial, 且在通过超线程技术实现的两个CPU的环境中也不能100%保证能超越Serial. 但随着可用的CPU数量的增加, 收集效率肯定也会大大增加(ParNew收集线程数与CPU的数量相同, 因此在CPU数量过大的环境中, 可用-XX:ParallelGCThreads参数控制GC线程数).


    3. Parallel Scavenge收集器

    与ParNew类似, Parallel Scavenge也是使用复制算法, 也是并行多线程收集器. 但与其他收集器关注尽可能缩短垃圾收集时间不同, Parallel Scavenge更关注系统吞吐量:

    =(+)

    停顿时间越短就越适用于用户交互的程序-良好的响应速度能提升用户的体验;而高吞吐量则适用于后台运算而不需要太多交互的任务-可以最高效率地利用CPU时间,尽快地完成程序的运算任务. Parallel Scavenge提供了如下参数设置系统吞吐量:

    Parallel Scavenge参数描述
    MaxGCPauseMillis(毫秒数) 收集器将尽力保证内存回收花费的时间不超过设定值, 但如果太小将会导致GC的频率增加.
    GCTimeRatio(整数:0 < GCTimeRatio < 100) 是垃圾收集时间占总时间的比率
    -XX:+UseAdaptiveSizePolicy启用GC自适应的调节策略: 不再需要手工指定-Xmn-XX:SurvivorRatio-XX:PretenureSizeThreshold等细节参数, VM会根据当前系统的运行情况收集性能监控信息, 动态调整这些参数以提供最合适的停顿时间或最大的吞吐量

    老年代

    Serial Old收集器

    Serial Old是Serial收集器的老年代版本, 同样是单线程收集器,使用“标记-整理”算法:

    • Serial Old应用场景如下:
      • JDK 1.5之前与Parallel Scavenge收集器搭配使用;
      • 作为CMS收集器的后备预案, 在并发收集发生Concurrent Mode Failure时启用(见下:CMS收集器).

    Parallel Old收集器

    Parallel Old是Parallel Scavenge收老年代版本, 使用多线程和“标记-整理”算法, 吞吐量优先, 主要与Parallel Scavenge配合在 注重吞吐量CPU资源敏感 系统内使用:


    CMS收集器

    CMS(Concurrent Mark Sweep)收集器是一款具有划时代意义的收集器, 一款真正意义上的并发收集器, 虽然现在已经有了理论意义上表现更好的G1收集器, 但现在主流互联网企业线上选用的仍是CMS(如Taobao、微店).
    CMS是一种以获取最短回收停顿时间为目标的收集器(CMS又称多并发低暂停的收集器), 基于”标记-清除”算法实现, 整个GC过程分为以下4个步骤:
    1. 初始标记(CMS initial mark)
    2. 并发标记(CMS concurrent mark: GC Roots Tracing过程)
    3. 重新标记(CMS remark)
    4. 并发清除(CMS concurrent sweep: 已死象将会就地释放, 注意: 此处没有压缩)
    其中两个加粗的步骤(初始标记重新标记)仍需STW. 但初始标记仅只标记一下GC Roots能直接关联到的对象, 速度很快; 而重新标记则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录, 虽然一般比初始标记阶段稍长, 但要远小于并发标记时间.

    (由于整个GC过程耗时最长的并发标记和并发清除阶段的GC线程可与用户线程一起工作, 所以总体上CMS的GC过程是与用户线程一起并发地执行的.

    由于CMS收集器将整个GC过程进行了更细粒度的划分, 因此可以实现并发收集、低停顿的优势, 但它也并非十分完美, 其存在缺点及解决策略如下:

    1. CMS线=(CPU+3)4

      当CPU数>4时, GC线程最多占用不超过25%的CPU资源, 但是当CPU数<=4时, GC线程可能就会过多的占用用户CPU资源, 从而导致应用程序变慢, 总吞吐量降低.
    2. 无法处理浮动垃圾, 可能出现Promotion FailureConcurrent Mode Failure而导致另一次Full GC的产生: 浮动垃圾是指在CMS并发清理阶段用户线程运行而产生的新垃圾. 由于在GC阶段用户线程还需运行, 因此还需要预留足够的内存空间给用户线程使用, 导致CMS不能像其他收集器那样等到老年代几乎填满了再进行收集. 因此CMS提供了-XX:CMSInitiatingOccupancyFraction参数来设置GC的触发百分比(以及-XX:+UseCMSInitiatingOccupancyOnly来启用该触发百分比), 当老年代的使用空间超过该比例后CMS就会被触发(JDK 1.6之后默认92%). 但当CMS运行期间预留的内存无法满足程序需要, 就会出现上述Promotion Failure等失败, 这时VM将启动后备预案: 临时启用Serial Old收集器来重新执行Full GC(CMS通常配合大内存使用, 一旦大内存转入串行的Serial GC, 那停顿的时间就是大家都不愿看到的了).
    3. 最后, 由于CMS采用”标记-清除”算法实现, 可能会产生大量内存碎片. 内存碎片过多可能会导致无法分配大对象而提前触发Full GC. 因此CMS提供了-XX:+UseCMSCompactAtFullCollection开关参数, 用于在Full GC后再执行一个碎片整理过程. 但内存整理是无法并发的, 内存碎片问题虽然没有了, 但停顿时间也因此变长了, 因此CMS还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction用于设置在执行N次不进行内存整理的Full GC后, 跟着来一次带整理的(默认为0: 每次进入Full GC时都进行碎片整理).

    分区收集- G1收集器

    G1(Garbage-First)是一款面向服务端应用的收集器, 主要目标用于配备多颗CPU的服务器治理大内存.
    - G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS).
    - -XX:+UseG1GC 启用G1收集器.

    与其他基于分代的收集器不同, G1将整个Java堆划分为多个大小相等的独立区域(Region), 虽然还保留有新生代和老年代的概念, 但新生代和老年代不再是物理隔离的了, 它们都是一部分Region(不需要连续)的集合.

    每块区域既有可能属于O区、也有可能是Y区, 因此不需要一次就对整个老年代/新生代回收. 而是当线程并发寻找可回收的对象时, 有些区块包含可回收的对象要比其他区块多很多. 虽然在清理这些区块时G1仍然需要暂停应用线程, 但可以用相对较少的时间优先回收垃圾较多的Region(这也是G1命名的来源). 这种方式保证了G1可以在有限的时间内获取尽可能高的收集效率.


    新生代收集

    G1的新生代收集跟ParNew类似: 存活的对象被转移到一个/多个Survivor Regions. 如果存活时间达到阀值, 这部分对象就会被提升到老年代.

    • G1的新生代收集特点如下:
      • 一整块堆内存被分为多个Regions.
      • 存活对象被拷贝到新的Survivor区或老年代.
      • 年轻代内存由一组不连续的heap区组成, 这种方法使得可以动态调整各代区域尺寸.
      • Young GCs会有STW事件, 进行时所有应用程序线程都会被暂停.
      • 多线程并发GC.

    老年代收集

    G1老年代GC会执行以下阶段:

    注: 一下有些阶段也是年轻代垃圾收集的一部分.

    indexPhaseDescription
    (1)初始标记 (Initial Mark: Stop the World Event)在G1中, 该操作附着一次年轻代GC, 以标记Survivor中有可能引用到老年代对象的Regions.
    (2)扫描根区域 (Root Region Scanning: 与应用程序并发执行)扫描Survivor中能够引用到老年代的references. 但必须在Minor GC触发前执行完.
    (3)并发标记 (Concurrent Marking : 与应用程序并发执行)在整个堆中查找存活对象, 但该阶段可能会被Minor GC中断.
    (4)重新标记 (Remark : Stop the World Event)完成堆内存中存活对象的标记. 使用snapshot-at-the-beginning(SATB, 起始快照)算法, 比CMS所用算法要快得多(空Region直接被移除并回收, 并计算所有区域的活跃度).
    (5)清理 (Cleanup : Stop the World Event and Concurrent)见下 5-1、2、3
    5-1 (Stop the world)在含有存活对象和完全空闲的区域上进行统计
    5-2 (Stop the world)擦除Remembered Sets.
    5-3 (Concurrent)重置空regions并将他们返还给空闲列表(free list)
    (*)Copying/Cleanup (Stop the World Event)选择”活跃度”最低的区域(这些区域可以最快的完成回收). 拷贝/转移存活的对象到新的尚未使用的regions. 该阶段会被记录在gc-log内(只发生年轻代[GC pause (young)], 与老年代一起执行则被记录为[GC Pause (mixed)].

    详细步骤可参考 Oracle官方文档-The G1 Garbage Collector Step by Step.

    • G1老年代GC特点如下:
      • 并发标记阶段(index 3)
        1. 在与应用程序并发执行的过程中会计算活跃度信息.
        2. 这些活跃度信息标识出那些regions最适合在STW期间回收(which regions will be best to reclaim during an evacuation pause).
        3. 不像CMS有清理阶段.
      • 再次标记阶段(index 4)
        1. 使用Snapshot-at-the-Beginning(SATB)算法比CMS快得多.
        2. 空region直接被回收.
      • 拷贝/清理阶段(Copying/Cleanup Phase)
        • 年轻代与老年代同时回收.
        • 老年代内存回收会基于他的活跃度信息.

    补充: 关于Remembered Set

    G1收集器中, Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用都是使用Remembered Set来避免扫描全堆. G1中每个Region都有一个与之对应的Remembered Set, VM发现程序对Reference类型数据进行写操作时, 会产生一个Write Barrier暂时中断写操作, 检查Reference引用的对象是否处于不同的Region中(在分代例子中就是检查是否老年代中的对象引用了新生代的对象), 如果是, 便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中. 当内存回收时, 在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏.


    V. JVM小工具

    在${JAVA_HOME}/bin/目录下Sun/Oracle给我们提供了一些处理应用程序性能问题、定位故障的工具, 包含

    bin描述功能
    jps打印Hotspot VM进程VMID、JVM参数、main()函数参数、主类名/Jar路径
    jstat查看Hotspot VM 运行时信息类加载、内存、GC[可分代查看]、JIT编译
    jinfo查看和修改虚拟机各项配置-flag name=value
    jmapheapdump: 生成VM堆转储快照、查询finalize执行队列、Java堆和永久代详细信息jmap -dump:live,format=b,file=heap.bin [VMID]
    jstack查看VM当前时刻的线程快照: 当前VM内每一条线程正在执行的方法堆栈集合Thread.getAllStackTraces()提供了类似的功能
    javap查看经javac之后产生的JVM字节码代码自动解析.class文件, 避免了去理解class文件格式以及手动解析class文件内容
    jcmd一个多功能工具, 可以用来导出堆, 查看Java进程、导出线程信息、 执行GC、查看性能相关数据等几乎集合了jps、jstat、jinfo、jmap、jstack所有功能
    jconsole基于JMX的可视化监视、管理工具可以查看内存、线程、类、CPU信息, 以及对JMX MBean进行管理
    jvisualvmJDK中最强大运行监视和故障处理工具可以监控内存泄露、跟踪垃圾回收、执行时内存分析、CPU分析、线程分析…

    VI. VM常用参数整理

    参数描述
    -Xms最小堆大小
    -Xmx最大堆大小
    -Xmn新生代大小
    -XX:PermSize永久代大小
    -XX:MaxPermSize永久代最大大小
    -XX:+PrintGC输出GC日志
    -verbose:gc-
    -XX:+PrintGCDetails输出GC的详细日志
    -XX:+PrintGCTimeStamps输出GC时间戳(以基准时间的形式)
    -XX:+PrintHeapAtGC在进行GC的前后打印出堆的信息
    -Xloggc:/path/gc.log日志文件的输出路径
    -XX:+PrintGCApplicationStoppedTime打印由GC产生的停顿时间

    在此处无法列举所有的参数以及他们的应用场景, 详细移步Oracle官方文档-Java HotSpot VM Options.


    参考 & 扩展
    深入理解Java虚拟机
    JVM内幕:Java虚拟机详解 (力荐)
    JVM中的G1垃圾回收器
    G1垃圾收集器入门
    Getting Started with the G1 Garbage Collector
    深入理解G1垃圾收集器
    解析JDK 7的Garbage-First收集器
    The Garbage-First Garbage Collector
    Memory Management in the Java HotSpot Virtual Machine
    Java HotSpot VM Options
    JVM实用参数(一)JVM类型以及编译器模式
    JVM内存回收理论与实现
    基于OpenJDK深度定制的淘宝JVM(TaobaoVM)
    展开全文
  • 这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“栈”就是虚拟机栈,或者说是虚拟机栈中的局部变量表部分 在方法中定义的一些基本类型的变量和对象的...

    前言

    文章所涉及的Java代码可以在这地方下载:https://github.com/chengqianbygithub/JavaLearningDemos/tree/develop

    概述

    我们通常把Java内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“栈”就是虚拟机栈,或者说是虚拟机栈中的局部变量表部分

    在方法中定义的一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配,当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量分配的内存空间,该内存空间可以立即被另作它用。

    堆内存用来存放由new创建的对象和数组,在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。在堆中产生了一个数组或者对象之后,还可以在栈中定义一个特殊的变量,让栈中的这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或者对象。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用new产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)

    运行时数据区域

    Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间。Java虚拟机所管理的内存将会包括以下几个运行时数据区。
    在这里插入图片描述

    程序计数器

    程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
    由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

    虚拟机栈

    描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)。

    Java虚拟机栈是线程私有的,它的生命周期与线程相同。

    本地方法栈

    本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的。很多虚拟机中都会将本地方法栈与虚拟机栈放在一起使用。本地方法栈也是线程私有的。

    堆区

    堆区是理解Java GC机制最重要的区域。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域。

    堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存。在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

    根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

    方法区

    方法区也是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。

    虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。

    运行时常量池

    运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量、符号引用和翻译出来的直接引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

    Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。

    直接内存

    并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。

    JDK1.4加入了NIO,引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。因为避免了在Java堆和Native堆中来回复制数据,提高了性能。

    当各个内存区域总和大于物理内存限制,抛出OutOfMemoryError异常。

    示例

    下面以一个简单示例来分析Java中对象内存分配的情况。

    /**
     * 葡萄酒类
     */
    public class Wine {
    
        private int id; // 编号
        private int price; // 价格
    
        public Wine() {}
    
        public Wine(int id, int price) {
            this.id = id;
            this.price = price;
        }
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public int getPrice() {
            return price;
        }
    
        public void setPrice(int price) {
            this.price = price;
        }
    }
    
    /**
     * 内存分配测试
     */
    public class MallocTest {
    
        public static void main(String[] args) {
            // 创建内存分配测试类对象
            MallocTest mt = new MallocTest();
            // 创建白葡萄酒类,id=1,价格=199
            Wine whiteWine = new Wine(1,199);
            // 创建红葡萄酒类,id=2,价格=299
            Wine redWine = new Wine(2,299);
            // 定义基础变量
            int testNum = 123;
    
            /* 下面是方法调用 */
            mt.changeBasicVar(testNum);
            mt.changeWineProp(whiteWine);
            mt.changeWine(redWine);
        }
    
        /**
         * 修改对象属性
         * 疑问:对象属性修改成功吗?
         * @param wine 葡萄酒类
         */
        public void changeWineProp(Wine wine){
            wine.setId(20);
            wine.setPrice(500);
        }
    
        /**
         * 修改对象引用
         * 疑问:对象引用能修改成功吗?
         * @param wine 葡萄酒类
         */
        public void changeWine(Wine wine){
            wine = new Wine(15,800);
        }
    
        /**
         * 修改基础类型变量值
         * 疑问:能修改成功吗?
         * @param num 基础类型变量
         */
        public void changeBasicVar(int num){
            num = 1250;
        }
    
    }
    

    内存分配情况分析

    程序从入口方法main()进入后,开始为main方法中所使用到的变量分配内存空间(引用变量所存储的值其实就是所指对象在堆内存中的首地址)。当执行到定义变量testNum后,调用方法前的内存分配情况为:
    在这里插入图片描述

    接着执行changeBasicVar()方法调用,转到changeBasicVar()方法执行时,该方法中使用到的参数是一个局部变量,我们仍在内存栈中分配空间保存,在进入方法后执行方法体前,此时将main()方法中的变量testNum副本拷贝到changeBasicVar()方法的局部变量num中,此时内存分配情况为:
    在这里插入图片描述
    执行changeBasicVar()方法体,修改方法中的局部变量值num,因为num中直接存放的是数值,所以它的修改不会影响到main()方法中的testNum,此时内存分配情况为:
    在这里插入图片描述
    changeBasicVar()方法调用完毕退出,局部变量num的作用域结束,从栈中回收内存,此时内存分配情况为:
    在这里插入图片描述
    接下来执行调用方法部分代码,首先调用changeWineProp()方法,main()方法中传递的whiteWine作为实际参数,在此处传递的实际上是whiteWine所引用的堆中对象的地址 (这儿有个参数传递的特点需要引申说明一下,在Java中参数的传递形式只有值传递方式) ,所以changeWineProp()方法中的wine也指向堆中已经创建的Wine对象,此时内存分配情况为:
    在这里插入图片描述
    转到changeWineProp()方法中执行,在方法体中,我们修改了wine变量引用的对象的id与price属性值,因为它与main()方法中的whiteWine引用的是同一个堆内存中的空间,所以修改后,main()方法中的whiteWine也受到了影响,此时内存分配情况为:
    在这里插入图片描述
    changeWineProp()方法调用结束后,释放掉它在栈中所分配的内存,我们看到,main()方法中的whiteWine确实受到了影响,此时内存分配情况为:
    在这里插入图片描述
    最后再调用changeWine()方法,main()方法中传递的redWine作为实际参数,与上一个方法调用一样,也是将引用地址传递给changeWine()方法的wine局部变量保存,此时内存分配情况为:
    在这里插入图片描述
    在changeWine()方法中,执行方法体的操作(使用new创建了一个新对象)之后,我们在堆中重新分配了一个对象的空间来保存新创建出来的对象,并将重新创建出来的对象首地址引用交给wine局部变量保存,所以wine中保存的引用就改变了,此时内存分配情况为:
    在这里插入图片描述
    这个时候大家可以观察一下,在main()方法中的redWine引用及其引用在堆中内存空间中保存的值是否已经改变了?

    changeWine()方法调用结束,在栈中释放掉其局部变量的空间,那么在堆中刚被引用到的对象空间就变成无引用状态了,没有被引用到的对象在堆中就等待GC来作垃圾回收,此时内存分配情况为:
    在这里插入图片描述
    到此,main()方法执行完毕,退出,栈中释放掉main()方法中分配的资源,此时内存分配情况为:
    在这里插入图片描述

    在堆中所分配的空间不再引用,则等待GC自动垃圾回收。

    根据以上分析,就可以思考一下,在各方法调用前后,变量testNum,对象whiteWine、redWIne各属性的值是什么。

    String字符串对象内存分配情况分析

    上边分析了基本数据类型的内存分配,简单的对象内存分配。下面来分析一下String字符串的内存分配,因为字符串是我们在编程过程中需要经常用到的对象。

    字符串对象在创建的时候有两种方式:

    String str1 = "abc";
    
    String str2 = new String("abc");
    

    这两种方式都是经常用到的,尤其是第一种方式。不管是哪一种创建字符串对象的方式,最终在程序中展现出来的效果是一样的。但这两种创建方式有什么不同呢?下面就从内存分析角度说明一下。

    这两种实现其实存在着一些性能和内存占用的差别。这一切主要是源于JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存结构,这个内存被称为 字符串常量池。

    字符串是常量,我们可以从字符串底层实现来看:

     String 源码
     ... 
     private final char value[];
     ...
    

    它使用了一个final的char型数组保存字符串的每个字符,那么final所修饰的数组引用是不能被改变的,而该char型数组中也未提供任何可供修改单个元素的方法,所以一旦确定字符串内容,也就不能再改变底层数组中保存的元素了。

    当代码中出现以字面量形式(上述第一种方式)创建字符串对象时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用。下面示例将详细分析String对象的内存分配情况。

    示例

     String str = "abc"; 
     String test = "abc"; 
     String strObj = new String("abc"); 
    

    当执行第1条语句时,JVM检测 “abc” 这个字面量,这里我们认为没有内容为 “abc” 的对象存在。JVM通过字符串常量池查找不到内容为 “abc” 的字符串对象存在,那么就创建这个字符串对象,然后将刚创建的对象放入到字符串常量池中,并且将引用返回给变量str。

    当执行第2条语句时,JVM还是要检测这个字面量,JVM通过查找字符串常量池,发现内容为 “abc” 字符串对象存在,于是将已经存在的字符串对象的引用返回给变量test,这里不会重新创建新的字符串对象,此时内存分配情况为:
    在这里插入图片描述
    我们也可以通过:

     System.out.println(str == test);
    

    来验证str与test是否指向同一个对象,==在此判断的是变量str与test中存放的引用,而不是对象本身的值。

    当执行第3条语句时,使用new创建了一个字符串对象,会在堆上分配一个存放字符串对象的空间,然后查找常量池中是否存在内容为 “abc” 的字符串对象,如果存在则返回引用,不存在则创建再返回引用,此时内存分配情况为:
    在这里插入图片描述
    实际上现在demo所保存的引用是在堆内存上的地址,我们可以通过:

    System.out.println(strObj == test);
    

    来验证strObj 与test是否是指向同一个对象,答案当然是false。

    字符串的连接中对运算符“+”作了重载,即“+”用于字符串运算时是将字符串连接在一起。我们知道,字符串是常量,那么要做字符串连接,不可能在原有字符串对象的后边直接连接,那是如何来实现连接操作的呢?

    在字符串连接时,如:

    String str = "hello" + "  world";
    

    首先会在字符串常量内存区创建”hello”字符串与”world”字符串两个对象,然后再开辟一个空间存放连接后的”hello world”字符串,所以这就造成了在常量内存区中创建了三个对象的现象,最后将连接后的”hello world”字符串的引用赋给变量str,此时内存分配情况为:
    在这里插入图片描述

    那么,当我们需要频繁修改字符串时,如果使用String来操作,则明显会使执行效率受影响,如何解决这个问题呢,请参考StringBuffer和StringBuilder的使用。


    技 术 无 他, 唯 有 熟 尔。
    知 其 然, 也 知 其 所 以 然。
    踏 实 一 些, 不 要 着 急, 你 想 要 的 岁 月 都 会 给 你。


    展开全文
  • Windows内存分配

    千次阅读 2013-12-18 08:35:31
    一、预备知识—程序的内存分配 一个由C/C++编译的程序占用的内存分为以下几个部分 1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 2、堆区...
  • 内存分配 内存回收 ------------------------------------------------------ 内存回收经常也被叫做垃圾回收。(附带资料:JVM面试题超链接、JVM性能调优和 参数说明) *很多人迷惑一个问题,既然J...
  • java垃圾回收与内存分配

    千次阅读 2015-07-14 16:00:43
    在Java中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给了JVM来处理。顾名思义,垃圾回收就是释放垃圾占用的空间,那么在Java中,什么样的对象会被认定为“垃圾”?那么当一些对象被确定为垃圾...
  • C++内存分配和管理

    千次阅读 2016-08-16 11:05:40
    内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不在,内存泄漏几乎在每个C++...
  • linux 内存分配机制

    千次阅读 2015-10-28 17:02:49
    这几天在观察apache使用内存情况,所以特意了解了下linux的内存机制,发现一篇写得还不错。转来看看。 一般来说在ps aux中看到的rss就是进程所占用的物理内存。但是如果将所有程序的rss加起来的话。会发现比实际的...
  • 第3章 垃圾收集器与内存分配策略

    千次阅读 2013-11-17 11:26:56
    Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。 3.1 概述 说起垃圾收集(Garbage Collection,GC),大部分人都把这项技术当做Java语言的伴生产物。...
  • STM32动态内存分配

    千次阅读 2014-03-19 08:17:31
    //限制,在某些情况下[比如分配内存给结构体指针],可能出现错误,所以一定要加上这个; __align(4) u8 membase[MEM_MAX_SIZE]; //内部SRAM内存池 //内存管理表 u16 memmapbase[MEM_ALLOC_TABLE_SIZE]; //内部...
  • 详解技术(路由聚合技术

    万次阅读 多人点赞 2018-03-05 21:58:38
    子网划分将一个单一的IP地址划分成多个子网,以延缓大型网络地址(主要是B类)的分配速度[2] 。子网划分从20世纪80年代提出以后的确起到了这个作用。但是到了20世纪90年代,子网划分也就无法阻止B类网络地址最后耗尽...
  • 性能优化|JVM内存分配机制
  • Windows内存分配(转)

    千次阅读 2011-01-15 06:15:00
    堆和栈的区别 一、预备知识—程序的内存分配 一个由C/C++编译的程序占用的内存分为以下几个部分 1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 2...
  • 内存分配对多线程程序性能的影响

    千次阅读 2016-11-04 15:41:37
    内存分配对多线程程序性能的影响 作者:Rickey C. Weisner,2012 年 3 月 如果您的应用程序在新的多处理器、多核、多线程硬件上运行时不能伸缩,问题可能在于内存分配器中的
  • 文章目录不随意调节jvm和thread pool的原因jvm gcthreadpooljvm和服务器内存分配的最佳实践jvm heap分配将机器上少于一半的内存分配给es不要给jvm分配超过32G内存在32G以内的话具体应该设置heap为多大?对于有1TB...
  • STM32实现动态内存分配

    千次阅读 2016-11-02 09:46:13
    //限制,在某些情况下[比如分配内存给结构体指针],可能出现错误,所以一定要加上这个; __align(4) u8 membase[MEM_MAX_SIZE]; //内部SRAM内存池 //内存管理表 u16 memmapbase[MEM_ALLOC_TABLE_SIZE]; //内部...
  • Android 内存泄漏总结(超级实用)

    万次阅读 2016-06-22 16:45:25
    Android 内存泄漏总结内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,就是该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用...
  • 堆和栈——内存分配探密

    千次阅读 2008-03-07 15:42:00
    在计算机领域,堆栈是一...要点:堆:顺序随意栈:先进后出堆和栈的区别 一、预备知识—程序的内存分配 一个由c/C++编译的程序占用的内存分为以下几个部分 1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值
  • Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。3.1 概述说起垃圾收集(Garbage Collection,GC),大部分人都把这项技术当做Java语言的伴生产物。事实上...
  • 本章介绍的垃圾收集器与内存分配策略主要就三点。 第一点:垃圾收集(垃圾回收)。问题:哪些内存需要回收?什么时候回收?如何回收? 第二点:介绍垃圾收集器。问题:有几种类型是垃圾收集器?根据第一点的介绍,...
  • c++五种内存分配、堆与栈区别

    千次阅读 2015-10-12 10:14:30
    在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、... 堆,就是那些由new分配内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序
  • C++内存管理技术内幕

    千次阅读 2015-11-22 09:19:04
    这几天在整理硬盘的资料,发现一个PDF,名字叫《C++内存管理技术内幕》,名字很霸气,于是顺着好奇心打开看看。花了一个多小时,终于看完,看完的感觉就是相见恨晚啊,写的如此之好,想看看这篇文章是谁写的,结果找...
  • 公司为这个项目专门配备了几台高性能务器,清一色的双路四核线程CPU,外加32G内存,运维人员安装好MongoDB后,就交我手里了,我习惯于在使用新服务器前先看看相关日志,了解一下基本情况,当我浏览MongoD
  • 3 虚拟内存分配和分页   3.1 进程内存概念 内存是进程可以使用的最基本资源之一。典型的,每个进程都有一个虚拟地址空间,从0到一个最大值(4G)。 不需要连续,也就是说,并不是所有地址都可以用来存储数据。 ...
  • M. Tim Jones, 顾问...现在,Linux® 内核使用了源自于 Solaris 的一种方法,但是这种方法在嵌入式系统中已经使用了很长时间了,它是将内存作为对象按照大小进行分配。本文将探索 slab 分配器背后所采用的思想,并
  • 主存(RAM) 是一件非常重要的资源,必须要认真对待内存。虽然目前大多数内存的增长速度要比 IBM 7094 要快的多,但是,程序大小的增长要比内存的增长还快很多。不管存储器有多大,程序大小的增长速度比内存容量的增长...
  • java程序运行时如何分配内存

    千次阅读 2015-09-18 17:52:41
    会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的...
  • 1、设备直接分配技术  如何提高虚拟化设备的性能问题是虚拟化领域长期的研究重点。如前所述,设备模拟模型会导致虚拟化性能大大下降;泛虚拟化设备模型虽然在性能上拥有一定的优势,但由于需要修改操作系统,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 64,661
精华内容 25,864
关键字:

内存超分配技术