精华内容
下载资源
问答
  • 但是RxJava使用起来也是有副作用的,使用越来越多的订阅,内存开销也会变得很大,稍不留神就会出现内存溢出的情况,这篇文章就是介绍Rxjava使用过程中应该注意的事项。 1、取消订阅 subscripti

    转:http://www.cnblogs.com/zhaoyanjun/p/5523454.html


    RxJava使我们很方便的使用链式编程,代码看起来既简洁又优雅。但是RxJava使用起来也是有副作用的,使用越来越多的订阅,内存开销也会变得很大,稍不留神就会出现内存溢出的情况,这篇文章就是介绍Rxjava使用过程中应该注意的事项。

    1、取消订阅 subscription.unsubscribe() ;

    package lib.com.myapplication;
    import android.os.Bundle;
    import android.support.v7.app.AppCompatActivity;
    import rx.Observable;
    import rx.Subscription;
    import rx.functions.Action1;
    
    public class MainActivity extends AppCompatActivity {
    
        Subscription subscription ;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            subscription =  Observable.just( "123").subscribe(new Action1<String>() {
                @Override
                public void call(String s) {
                    System.out.println( "tt--" + s );
                }
            }) ;
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            //取消订阅
            if ( subscription != null ){
                subscription.unsubscribe();
            }
        }
    }

    2、线程调度

    • Scheduler调度器,相当于线程控制器

      • Schedulers.immediate() : 直接在当前线程运行,相当于不指定线程。这是默认的 Scheduler。

      • Schedulers.newThread() :总是启用新线程,并在新线程执行操作.

      • Schedulers.io():I/O 操作(读写文件、读写数据库、网络信息交互等)所使用的 Scheduler。行为模式和 newThread() 差不多,区别在于 io() 的内部实现是是用一个无数量上限的线程池,可以重用空闲的线程,因此多数情况下 io() 比 newThread() 更有效率。不要把计算工作放在 io() 中,可以避免创建不必要的线程。

      • Schedulers.computation() : 计算所使用的 Scheduler。这个计算指的是 CPU 密集型计算,即不会被 I/O 等操作限制性能的操作,例如图形的计算。这个 Scheduler 使用的固定的线程池,大小为 CPU 核数。不要把 I/O 操作放在 computation() 中,否则 I/O 操作的等待时间会浪费 CPU。

      • 还有RxAndroid里面专门提供了AndroidSchedulers.mainThread(),它指定的操作将在 Android 主线程运行。

    • 常见的场景:为了不阻塞UI,在子线程加载数据,在主线线程显示数据

       Observable.just( "1" , "2" , "3" )
                .subscribeOn(Schedulers.io())  //指定 subscribe() 发生在 IO 线程
                .observeOn( AndroidSchedulers.mainThread() )  //指定 Subscriber 的回调发生在主线程
                .subscribe(new Action1<String>() {
                    @Override
                    public void call(String s) {
                        textView.setText( s );
                    }
                }) ;

    上面这段代码,数据"1"、"2"、"3"将在io线程中发出,在android主线程中接收数据。这种【后台获取数据,前台显示数据】模式适用于大多数的程序策略。

    Scheduler 自由多次切换线程。恩,这个更为牛逼

    Observable.just(1, 2, 3, 4) // IO 线程,由 subscribeOn() 指定
     .subscribeOn(Schedulers.io())
     .observeOn(Schedulers.newThread())
     .map(mapOperator) // 新线程,由 observeOn() 指定
     .observeOn(Schedulers.io())
     .map(mapOperator2) // IO 线程,由 observeOn() 指定
     .observeOn(AndroidSchedulers.mainThread) 
     .subscribe(subscriber);  // Android 主线程,由 observeOn() 指定

    从上面的代码可以看出

    • observeOn() 可以调用多次来切换线程,observeOn 决定他下面的方法执行时所在的线程。

    • subscribeOn() 用来确定数据发射所在的线程,位置放在哪里都可以,但它是只能调用一次的。



    • 上面介绍了两种控制Rxjava生命周期的方式,第一种:取消订阅 ;第二种:线程切换 。这两种方式都能有效的解决android内存的使用问题,但是在实际的项目中会出现很多订阅关系,那么取消订阅的代码也就越来越多。造成了项目很难维护。所以我们必须寻找其他可靠简单可行的方式,也就是下面要介绍的。

    3、rxlifecycle 框架的使用

    • github地址: https://github.com/trello/RxLifecycle

    • 在android studio 里面添加引用
      compile 'com.trello:rxlifecycle-components:0.6.1'

    • 让你的activity继承RxActivity,RxAppCompatActivity,RxFragmentActivity
      让你的fragment继承RxFragment,RxDialogFragment;下面的代码就以RxAppCompatActivity举例

    • bindToLifecycle 方法
      在子类使用Observable中的compose操作符,调用,完成Observable发布的事件和当前的组件绑定,实现生命周期同步。从而实现当前组件生命周期结束时,自动取消对Observable订阅。

    public class MainActivity extends RxAppCompatActivity {
            TextView textView ;
            
            @Override
            protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            textView = (TextView) findViewById(R.id.textView);
        
            //循环发送数字
            Observable.interval(0, 1, TimeUnit.SECONDS)
                .subscribeOn( Schedulers.io())
                .compose(this.<Long>bindToLifecycle())   //这个订阅关系跟Activity绑定,Observable 和activity生命周期同步
                .observeOn( AndroidSchedulers.mainThread())
                .subscribe(new Action1<Long>() {
                    @Override
                    public void call(Long aLong) {
                        System.out.println("lifecycle--" + aLong);
                        textView.setText( "" + aLong );
                    }
                });
           }
        }

    • 上面的代码是Observable循环的发送数字,并且在textview中显示出来
      1、没加 compose(this.<Long>bindToLifecycle()) 当Activiry 结束掉以后,Observable还是会不断的发送数字,订阅关系没有解除
      2、添加compose(this.<Long>bindToLifecycle()) 当Activity结束掉以后,Observable停止发送数据,订阅关系解除。

    • 从上面的例子可以看出bindToLifecycle() 方法可以使Observable发布的事件和当前的Activity绑定,实现生命周期同步。也就是Activity 的 onDestroy() 方法被调用后,Observable 的订阅关系才解除。那能不能指定在Activity其他的生命状态和订阅关系保持同步,答案是有的。就是 bindUntilEvent()方法。这个逼装的好累!

    • bindUntilEvent( ActivityEvent event)

      • ActivityEvent.CREATE: 在Activity的onCreate()方法执行后,解除绑定。

      • ActivityEvent.START:在Activity的onStart()方法执行后,解除绑定。

      • ActivityEvent.RESUME:在Activity的onResume()方法执行后,解除绑定。

      • ActivityEvent.PAUSE: 在Activity的onPause()方法执行后,解除绑定。

      • ActivityEvent.STOP:在Activity的onStop()方法执行后,解除绑定。

      • ActivityEvent.DESTROY:在Activity的onDestroy()方法执行后,解除绑定

         //循环发送数字
             Observable.interval(0, 1, TimeUnit.SECONDS)
                     .subscribeOn( Schedulers.io())
                     .compose(this.<Long>bindUntilEvent(ActivityEvent.STOP ))   //当Activity执行Onstop()方法是解除订阅关系
                     .observeOn( AndroidSchedulers.mainThread())
                     .subscribe(new Action1<Long>() {
                         @Override
                         public void call(Long aLong) {
                             System.out.println("lifecycle-stop-" + aLong);
                             textView.setText( "" + aLong );
                         }
                     });

    经过测试发现,当Activity执行了onStop()方法后,订阅关系已经解除了。
    上面说的都是订阅事件与Activity的生命周期同步,那么在Fragment里面又该怎么处理的?


    • FragmentEvent 这个类是专门处理订阅事件与Fragment生命周期同步的大杀器

      public enum FragmentEvent {
      
      ATTACH,
      CREATE,
      CREATE_VIEW,
      START,
      RESUME,
      PAUSE,
      STOP,
      DESTROY_VIEW,
      DESTROY,
      DETACH
      }
      

      可以看出FragmentEvent ActivityEvent 类似,都是枚举类,用法是一样的。这里就不举例了!

    总结
    1、这三篇文章的相关代码示例都在 http://git.oschina.net/zyj1609/RxAndroid_RxJava
    2、通过上面的三种方法,我相信你在项目中使用Rxjava的时候,已经能够很好的控制了 Rxjava对内存的开销。如果你有其他的方法或者问题,可以留言给我。


    展开全文
  • 固定大小内存池设计与实现

    千次阅读 2014-06-17 22:10:59
    developerWorks 图书频道: C++ 应用程序性能优化,第 6 章:内存池 本书主要针对的是 C++ 程序的性能优化,深入介绍 C++ 程序性能优化的方法和实例。全书由 4 个篇组成,第 1 篇介绍 C++ 语言的对象...

    developerWorks 图书频道: C++ 应用程序性能优化,第 6 章:内存池

    本书主要针对的是 C++ 程序的性能优化,深入介绍 C++ 程序性能优化的方法和实例。全书由 4 个篇组成,第 1 篇介绍 C++ 语言的对象模型,该篇是优化 C++ 程序的基础;第 2 篇主要针对如何优化 C++ 程序的内存使用;第 3 篇介绍如何优化程序的启动性能;第 4 篇介绍了三类性能优化工具,即内存分析工具、性能分析工具和 I/O 检测工具,它们是测量程序性能的利器。

    本章首先简单介绍自定义内存池性能优化的原理,然后列举软件开发中常用的内存池的不同类型,并给出具体实现的实例。

    在此我们推出了本书的 前言 和第 26 章供大家在线浏览。更多推荐书籍请访问developerWorks 图书频道

    冯 宏华, 高级软件工程师, IBM 中国开发中心

    徐 莹, 开发经理, IBM 中国软件开发中心

    程 远, 高级软件工程师, IBM 软件开发中心

    汪 磊, 高级软件工程师, IBM 中国开发中心

    2007 年 11 月 29 日

    • +内容

    引言

    本书主要针对的是 C++ 程序的性能优化,深入介绍 C++ 程序性能优化的方法和实例。全书由 4 个篇组成,第 1 篇介绍 C++ 语言的对象模型,该篇是优化 C++ 程序的基础;第 2 篇主要针对如何优化 C++ 程序的内存使用;第 3 篇介绍如何优化程序的启动性能;第 4 篇介绍了三类性能优化工具,即内存分析工具、性能分析工具和 I/O 检测工具,它们是测量程序性能的利器。

    本章首先简单介绍自定义内存池性能优化的原理,然后列举软件开发中常用的内存池的不同类型,并给出具体实现的实例。

    6.1 自定义内存池性能优化的原理

    如前所述,读者已经了解到"堆"和"栈"的区别。而在编程实践中,不可避免地要大量用到堆上的内存。例如在程序中维护一个链表的数据结构时,每次新增或者删除一个链表的节点,都需要从内存堆上分配或者释放一定的内存;在维护一个动态数组时,如果动态数组的大小不能满足程序需要时,也要在内存堆上分配新的内存空间。

    6.1.1 默认内存管理函数的不足

    利用默认的内存管理函数new/delete或malloc/free在堆上分配和释放内存会有一些额外的开销。

    系统在接收到分配一定大小内存的请求时,首先查找内部维护的内存空闲块表,并且需要根据一定的算法(例如分配最先找到的不小于申请大小的内存块给请求者,或者分配最适于申请大小的内存块,或者分配最大空闲的内存块等)找到合适大小的空闲内存块。如果该空闲内存块过大,还需要切割成已分配的部分和较小的空闲块。然后系统更新内存空闲块表,完成一次内存分配。类似地,在释放内存时,系统把释放的内存块重新加入到空闲内存块表中。如果有可能的话,可以把相邻的空闲块合并成较大的空闲块。

    默认的内存管理函数还考虑到多线程的应用,需要在每次分配和释放内存时加锁,同样增加了开销。

    可见,如果应用程序频繁地在堆上分配和释放内存,则会导致性能的损失。并且会使系统中出现大量的内存碎片,降低内存的利用率。

    默认的分配和释放内存算法自然也考虑了性能,然而这些内存管理算法的通用版本为了应付更复杂、更广泛的情况,需要做更多的额外工作。而对于某一个具体的应用程序来说,适合自身特定的内存分配释放模式的自定义内存池则可以获得更好的性能。

    6.1.2 内存池的定义和分类

    自定义内存池的思想通过这个"池"字表露无疑,应用程序可以通过系统的内存分配调用预先一次性申请适当大小的内存作为一个内存池,之后应用程序自己对内存的分配和释放则可以通过这个内存池来完成。只有当内存池大小需要动态扩展时,才需要再调用系统的内存分配函数,其他时间对内存的一切操作都在应用程序的掌控之中。

    应用程序自定义的内存池根据不同的适用场景又有不同的类型。

    从线程安全的角度来分,内存池可以分为单线程内存池和多线程内存池。单线程内存池整个生命周期只被一个线程使用,因而不需要考虑互斥访问的问题;多线程内存池有可能被多个线程共享,因此则需要在每次分配和释放内存时加锁。相对而言,单线程内存池性能更高,而多线程内存池适用范围更广。

    从内存池可分配内存单元大小来分,可以分为固定内存池和可变内存池。所谓固定内存池是指应用程序每次从内存池中分配出来的内存单元大小事先已经确定,是固定不变的;而可变内存池则每次分配的内存单元大小可以按需变化,应用范围更广,而性能比固定内存池要低。

    6.1.3 内存池工作原理示例

    下面以固定内存池为例说明内存池的工作原理,如图6-1所示。

    图6-1 固定内存池
    图6-1 固定内存池

    固定内存池由一系列固定大小的内存块组成,每一个内存块又包含了固定数量和大小的内存单元。

    如图6-1所示,该内存池一共包含4个内存块。在内存池初次生成时,只向系统申请了一个内存块,返回的指针作为整个内存池的头指针。之后随着应用程序对内存的不断需求,内存池判断需要动态扩大时,才再次向系统申请新的内存块,并把所有这些内存块通过指针链接起来。对于操作系统来说,它已经为该应用程序分配了4个等大小的内存块。由于是大小固定的,所以分配的速度比较快;而对于应用程序来说,其内存池开辟了一定大小,内存池内部却还有剩余的空间。

    例如放大来看第4个内存块,其中包含一部分内存池块头信息和3个大小相等的内存池单元。单元1和单元3是空闲的,单元2已经分配。当应用程序需要通过该内存池分配一个单元大小的内存时,只需要简单遍历所有的内存池块头信息,快速定位到还有空闲单元的那个内存池块。然后根据该块的块头信息直接定位到第1个空闲的单元地址,把这个地址返回,并且标记下一个空闲单元即可;当应用程序释放某一个内存池单元时,直接在对应的内存池块头信息中标记该内存单元为空闲单元即可。

    可见与系统管理内存相比,内存池的操作非常迅速,它在性能优化方面的优点主要如下。

    (1)针对特殊情况,例如需要频繁分配释放固定大小的内存对象时,不需要复杂的分配算法和多线程保护。也不需要维护内存空闲表的额外开销,从而获得较高的性能。

    (2)由于开辟一定数量的连续内存空间作为内存池块,因而一定程度上提高了程序局部性,提升了程序性能。

    (3)比较容易控制页边界对齐和内存字节对齐,没有内存碎片的问题。

    6.2 一个内存池的实现实例

    本节分析在某个大型应用程序实际应用到的一个内存池实现,并详细讲解其使用方法与工作原理。这是一个应用于单线程环境且分配单元大小固定的内存池,一般用来为执行时会动态频繁地创建且可能会被多次创建的类对象或者结构体分配内存。

    本节首先讲解该内存池的数据结构声明及图示,接着描述其原理及行为特征。然后逐一讲解实现细节,最后介绍如何在实际程序中应用此内存池,并与使用普通内存函数申请内存的程序性能作比较。

    6.2.1 内部构造

    内存池类MemoryPool的声明如下:

    class MemoryPool
    {
    private:
        MemoryBlock*   pBlock;
        USHORT          nUnitSize;
        USHORT          nInitSize;
        USHORT          nGrowSize;
    
    public:
                         MemoryPool( USHORT nUnitSize,
                                      USHORT nInitSize = 1024,
                                      USHORT nGrowSize = 256 );
                        ~MemoryPool();
    
        void*           Alloc();
        void            Free( void* p );
    };

    MemoryBlock为内存池中附着在真正用来为内存请求分配内存的内存块头部的结构体,它描述了与之联系的内存块的使用信息:

    struct MemoryBlock
    {
        USHORT          nSize;
        USHORT          nFree;
        USHORT          nFirst;
        USHORT          nDummyAlign1;
        MemoryBlock*  pNext;
        char            aData[1];
    
    	static void* operator new(size_t, USHORT nTypes, USHORT nUnitSize)
    	{
    		return ::operator new(sizeof(MemoryBlock) + nTypes * nUnitSize);
    	}
    	static void  operator delete(void *p, size_t)
    	{
    		::operator delete (p);
    	}
    
    	MemoryBlock (USHORT nTypes = 1, USHORT nUnitSize = 0);
    	~MemoryBlock() {}
    };

    此内存池的数据结构如图6-2所示。

    图6-2 内存池的数据结构
    图6-2 内存池的数据结构

    6.2.2 总体机制

    此内存池的总体机制如下。

    (1)在运行过程中,MemoryPool内存池可能会有多个用来满足内存申请请求的内存块,这些内存块是从进程堆中开辟的一个较大的连续内存区域,它由一个MemoryBlock结构体和多个可供分配的内存单元组成,所有内存块组成了一个内存块链表,MemoryPool的pBlock是这个链表的头。对每个内存块,都可以通过其头部的MemoryBlock结构体的pNext成员访问紧跟在其后面的那个内存块。

    (2)每个内存块由两部分组成,即一个MemoryBlock结构体和多个内存分配单元。这些内存分配单元大小固定(由MemoryPool的nUnitSize表示),MemoryBlock结构体并不维护那些已经分配的单元的信息;相反,它只维护没有分配的自由分配单元的信息。它有两个成员比较重要:nFree和nFirst。nFree记录这个内存块中还有多少个自由分配单元,而nFirst则记录下一个可供分配的单元的编号。每一个自由分配单元的头两个字节(即一个USHORT型值)记录了紧跟它之后的下一个自由分配单元的编号,这样,通过利用每个自由分配单元的头两个字节,一个MemoryBlock中的所有自由分配单元被链接起来。

    (3)当有新的内存请求到来时,MemoryPool会通过pBlock遍历MemoryBlock链表,直到找到某个MemoryBlock所在的内存块,其中还有自由分配单元(通过检测MemoryBlock结构体的nFree成员是否大于0)。如果找到这样的内存块,取得其MemoryBlock的nFirst值(此为该内存块中第1个可供分配的自由单元的编号)。然后根据这个编号定位到该自由分配单元的起始位置(因为所有分配单元大小固定,因此每个分配单元的起始位置都可以通过编号分配单元大小来偏移定位),这个位置就是用来满足此次内存申请请求的内存的起始地址。但在返回这个地址前,需要首先将该位置开始的头两个字节的值(这两个字节值记录其之后的下一个自由分配单元的编号)赋给本内存块的MemoryBlock的nFirst成员。这样下一次的请求就会用这个编号对应的内存单元来满足,同时将此内存块的MemoryBlock的nFree递减1,然后才将刚才定位到的内存单元的起始位置作为此次内存请求的返回地址返回给调用者。

    (4)如果从现有的内存块中找不到一个自由的内存分配单元(当第1次请求内存,以及现有的所有内存块中的所有内存分配单元都已经被分配时会发生这种情形),MemoryPool就会从进程堆中申请一个内存块(这个内存块包括一个MemoryBlock结构体,及紧邻其后的多个内存分配单元,假设内存分配单元的个数为n,n可以取值MemoryPool中的nInitSize或者nGrowSize),申请完后,并不会立刻将其中的一个分配单元分配出去,而是需要首先初始化这个内存块。初始化的操作包括设置MemoryBlock的nSize为所有内存分配单元的大小(注意,并不包括MemoryBlock结构体的大小)、nFree为n-1(注意,这里是n-1而不是n,因为此次新内存块就是为了满足一次新的内存请求而申请的,马上就会分配一块自由存储单元出去,如果设为n-1,分配一个自由存储单元后无须再将n递减1),nFirst为1(已经知道nFirst为下一个可以分配的自由存储单元的编号。为1的原因与nFree为n-1相同,即立即会将编号为0的自由分配单元分配出去。现在设为1,其后不用修改nFirst的值),MemoryBlock的构造需要做更重要的事情,即将编号为0的分配单元之后的所有自由分配单元链接起来。如前所述,每个自由分配单元的头两个字节用来存储下一个自由分配单元的编号。另外,因为每个分配单元大小固定,所以可以通过其编号和单元大小(MemoryPool的nUnitSize成员)的乘积作为偏移值进行定位。现在唯一的问题是定位从哪个地址开始?答案是MemoryBlock的aData[1]成员开始。因为aData[1]实际上是属于MemoryBlock结构体的(MemoryBlock结构体的最后一个字节),所以实质上,MemoryBlock结构体的最后一个字节也用做被分配出去的分配单元的一部分。因为整个内存块由MemoryBlock结构体和整数个分配单元组成,这意味着内存块的最后一个字节会被浪费,这个字节在图6-2中用位于两个内存的最后部分的浓黑背景的小块标识。确定了分配单元的起始位置后,将自由分配单元链接起来的工作就很容易了。即从aData位置开始,每隔nUnitSize大小取其头两个字节,记录其之后的自由分配单元的编号。因为刚开始所有分配单元都是自由的,所以这个编号就是自身编号加1,即位置上紧跟其后的单元的编号。初始化后,将此内存块的第1个分配单元的起始地址返回,已经知道这个地址就是aData。

    (5)当某个被分配的单元因为delete需要回收时,该单元并不会返回给进程堆,而是返回给MemoryPool。返回时,MemoryPool能够知道该单元的起始地址。这时,MemoryPool开始遍历其所维护的内存块链表,判断该单元的起始地址是否落在某个内存块的地址范围内。如果不在所有内存地址范围内,则这个被回收的单元不属于这个MemoryPool;如果在某个内存块的地址范围内,那么它会将这个刚刚回收的分配单元加到这个内存块的MemoryBlock所维护的自由分配单元链表的头部,同时将其nFree值递增1。回收后,考虑到资源的有效利用及后续操作的性能,内存池的操作会继续判断:如果此内存块的所有分配单元都是自由的,那么这个内存块就会从MemoryPool中被移出并作为一个整体返回给进程堆;如果该内存块中还有非自由分配单元,这时不能将此内存块返回给进程堆。但是因为刚刚有一个分配单元返回给了这个内存块,即这个内存块有自由分配单元可供下次分配,因此它会被移到MemoryPool维护的内存块的头部。这样下次的内存请求到来,MemoryPool遍历其内存块链表以寻找自由分配单元时,第1次寻找就会找到这个内存块。因为这个内存块确实有自由分配单元,这样可以减少MemoryPool的遍历次数。

    综上所述,每个内存池(MemoryPool)维护一个内存块链表(单链表),每个内存块由一个维护该内存块信息的块头结构(MemoryBlock)和多个分配单元组成,块头结构MemoryBlock则进一步维护一个该内存块的所有自由分配单元组成的"链表"。这个链表不是通过"指向下一个自由分配单元的指针"链接起来的,而是通过"下一个自由分配单元的编号"链接起来,这个编号值存储在该自由分配单元的头两个字节中。另外,第1个自由分配单元的起始位置并不是MemoryBlock结构体"后面的"第1个地址位置,而是MemoryBlock结构体"内部"的最后一个字节aData(也可能不是最后一个,因为考虑到字节对齐的问题),即分配单元实际上往前面错了一位。又因为MemoryBlock结构体后面的空间刚好是分配单元的整数倍,这样依次错位下去,内存块的最后一个字节实际没有被利用。这么做的一个原因也是考虑到不同平台的移植问题,因为不同平台的对齐方式可能不尽相同。即当申请MemoryBlock大小内存时,可能会返回比其所有成员大小总和还要大一些的内存。最后的几个字节是为了"补齐",而使得aData成为第1个分配单元的起始位置,这样在对齐方式不同的各种平台上都可以工作。

    6.2.3 细节剖析

    有了上述的总体印象后,本节来仔细剖析其实现细节。

    (1)MemoryPool的构造如下:

    MemoryPool::MemoryPool( USHORT _nUnitSize,
                                USHORT _nInitSize, USHORT _nGrowSize )
    {
        pBlock      = NULL;	            ①
        nInitSize   = _nInitSize;       ②
        nGrowSize   = _nGrowSize;       ③
    
        if ( _nUnitSize > 4 )
            nUnitSize = (_nUnitSize + (MEMPOOL_ALIGNMENT-1)) & ~(MEMPOOL_ALIGNMENT-1); ④
        else if ( _nUnitSize <= 2 )
            nUnitSize = 2;              ⑤
        else
            nUnitSize = 4;
    }

    从①处可以看出,MemoryPool创建时,并没有立刻创建真正用来满足内存申请的内存块,即内存块链表刚开始时为空。

    ②处和③处分别设置"第1次创建的内存块所包含的分配单元的个数",及"随后创建的内存块所包含的分配单元的个数",这两个值在MemoryPool创建时通过参数指定,其后在该MemoryPool对象生命周期中一直不变。

    后面的代码用来设置nUnitSize,这个值参考传入的_nUnitSize参数。但是还需要考虑两个因素。如前所述,每个分配单元在自由状态时,其头两个字节用来存放"其下一个自由分配单元的编号"。即每个分配单元"最少"有"两个字节",这就是⑤处赋值的原因。④处是将大于4个字节的大小_nUnitSize往上"取整到"大于_nUnitSize的最小的MEMPOOL_ ALIGNMENT的倍数(前提是MEMPOOL_ALIGNMENT为2的倍数)。如_nUnitSize为11时,MEMPOOL_ALIGNMENT为8,nUnitSize为16;MEMPOOL_ALIGNMENT为4,nUnitSize为12;MEMPOOL_ALIGNMENT为2,nUnitSize为12,依次类推。

    (2)当向MemoryPool提出内存请求时:

    void* MemoryPool::Alloc()
    {
        if ( !pBlock )           ①
        {
    			……							
        }
    
        MemoryBlock* pMyBlock = pBlock;
        while (pMyBlock && !pMyBlock->nFree )②
            pMyBlock = pMyBlock->pNext;
    
        if ( pMyBlock )	         ③
        {
            char* pFree = pMyBlock->aData+(pMyBlock->nFirst*nUnitSize);			
            pMyBlock->nFirst = *((USHORT*)pFree);
    							
            pMyBlock->nFree--;	
            return (void*)pFree;
        }
        else                    ④
        {
            if ( !nGrowSize )
                return NULL;
    
    		pMyBlock = new(nGrowSize, nUnitSize) FixedMemBlock(nGrowSize, nUnitSize);
            if ( !pMyBlock )
                return NULL;
    
            pMyBlock->pNext = pBlock;
            pBlock = pMyBlock;
    
            return (void*)(pMyBlock->aData);
        }
    
    }

    MemoryPool满足内存请求的步骤主要由四步组成。

    ①处首先判断内存池当前内存块链表是否为空,如果为空,则意味着这是第1次内存申请请求。这时,从进程堆中申请一个分配单元个数为nInitSize的内存块,并初始化该内存块(主要初始化MemoryBlock结构体成员,以及创建初始的自由分配单元链表,下面会详细分析其代码)。如果该内存块申请成功,并初始化完毕,返回第1个分配单元给调用函数。第1个分配单元以MemoryBlock结构体内的最后一个字节为起始地址。

    ②处的作用是当内存池中已有内存块(即内存块链表不为空)时遍历该内存块链表,寻找还有"自由分配单元"的内存块。

    ③处检查如果找到还有自由分配单元的内存块,则"定位"到该内存块现在可以用的自由分配单元处。"定位"以MemoryBlock结构体内的最后一个字节位置aData为起始位置,以MemoryPool的nUnitSize为步长来进行。找到后,需要修改MemoryBlock的nFree信息(剩下来的自由分配单元比原来减少了一个),以及修改此内存块的自由存储单元链表的信息。在找到的内存块中,pMyBlock->nFirst为该内存块中自由存储单元链表的表头,其下一个自由存储单元的编号存放在pMyBlock->nFirst指示的自由存储单元(亦即刚才定位到的自由存储单元)的头两个字节。通过刚才定位到的位置,取其头两个字节的值,赋给pMyBlock->nFirst,这就是此内存块的自由存储单元链表的新的表头,即下一次分配出去的自由分配单元的编号(如果nFree大于零的话)。修改维护信息后,就可以将刚才定位到的自由分配单元的地址返回给此次申请的调用函数。注意,因为这个分配单元已经被分配,而内存块无须维护已分配的分配单元,因此该分配单元的头两个字节的信息已经没有用处。换个角度看,这个自由分配单元返回给调用函数后,调用函数如何处置这块内存,内存池无从知晓,也无须知晓。此分配单元在返回给调用函数时,其内容对于调用函数来说是无意义的。因此几乎可以肯定调用函数在用这个单元的内存时会覆盖其原来的内容,即头两个字节的内容也会被抹去。因此每个存储单元并没有因为需要链接而引入多余的维护信息,而是直接利用单元内的头两个字节,当其分配后,头两个字节也可以被调用函数利用。而在自由状态时,则用来存放维护信息,即下一个自由分配单元的编号,这是一个有效利用内存的好例子。

    ④处表示在②处遍历时,没有找到还有自由分配单元的内存块,这时,需要重新向进程堆申请一个内存块。因为不是第一次申请内存块,所以申请的内存块包含的分配单元个数为nGrowSize,而不再是nInitSize。与①处相同,先做这个新申请内存块的初始化工作,然后将此内存块插入MemoryPool的内存块链表的头部,再将此内存块的第1个分配单元返回给调用函数。将此新内存块插入内存块链表的头部的原因是该内存块还有很多可供分配的自由分配单元(除非nGrowSize等于1,这应该不太可能。因为内存池的含义就是一次性地从进程堆中申请一大块内存,以供后续的多次申请),放在头部可以使得在下次收到内存申请时,减少②处对内存块的遍历时间。

    可以用图6-2的MemoryPool来展示MemoryPool::Alloc的过程。图6-3是某个时刻MemoryPool的内部状态。

    图6-3 某个时刻MemoryPool的内部状态
    图6-3 某个时刻MemoryPool的内部状态

    因为MemoryPool的内存块链表不为空,因此会遍历其内存块链表。又因为第1个内存块里有自由的分配单元,所以会从第1个内存块中分配。检查nFirst,其值为m,这时pBlock->aData+(pBlock->nFirst*nUnitSize)定位到编号为m的自由分配单元的起始位置(用pFree表示)。在返回pFree之前,需要修改此内存块的维护信息。首先将nFree递减1,然后取得pFree处开始的头两个字节的值(需要说明的是,这里aData处值为k。其实不是这一个字节。而是以aData和紧跟其后的另外一个字节合在一起构成的一个USHORT的值,不可误会)。发现为k,这时修改pBlock的nFirst为k。然后,返回pFree。此时MemoryPool的结构如图6-4所示。

    图6-4 MemoryPool的结构
    图6-4 MemoryPool的结构

    可以看到,原来的第1个可供分配的单元(m编号处)已经显示为被分配的状态。而pBlock的nFirst已经指向原来m单元下一个自由分配单元的编号,即k。

    (3)MemoryPool回收内存时:

    void MemoryPool::Free( void* pFree )
    {
        ……
    
        MemoryBlock* pMyBlock = pBlock;
    
        while ( ((ULONG)pMyBlock->aData > (ULONG)pFree) ||
             ((ULONG)pFree >= ((ULONG)pMyBlock->aData + pMyBlock->nSize)) )①
        {
             ……
        }
    
        pMyBlock->nFree++;                     ②
        *((USHORT*)pFree) = pMyBlock->nFirst;  ③
        pMyBlock->nFirst = (USHORT)(((ULONG)pFree-(ULONG)(pBlock->aData)) / nUnitSize);④
    
        if (pMyBlock->nFree*nUnitSize == pMyBlock->nSize )⑤
        {
            ……
        }
        else
        {
            ……
        }
    }

    如前所述,回收分配单元时,可能会将整个内存块返回给进程堆,也可能将被回收分配单元所属的内存块移至内存池的内存块链表的头部。这两个操作都需要修改链表结构。这时需要知道该内存块在链表中前一个位置的内存块。

    ①处遍历内存池的内存块链表,确定该待回收分配单元(pFree)落在哪一个内存块的指针范围内,通过比较指针值来确定。

    运行到②处,pMyBlock即找到的包含pFree所指向的待回收分配单元的内存块(当然,这时应该还需要检查pMyBlock为NULL时的情形,即pFree不属于此内存池的范围,因此不能返回给此内存池,读者可以自行加上)。这时将pMyBlock的nFree递增1,表示此内存块的自由分配单元多了一个。

    ③处用来修改该内存块的自由分配单元链表的信息,它将这个待回收分配单元的头两个字节的值指向该内存块原来的第一个可分配的自由分配单元的编号。

    ④处将pMyBlock的nFirst值改变为指向这个待回收分配单元的编号,其编号通过计算此单元的起始位置相对pMyBlock的aData位置的差值,然后除以步长(nUnitSize)得到。

    实质上,③和④两步的作用就是将此待回收分配单元"真正回收"。值得注意的是,这两步实际上是使得此回收单元成为此内存块的下一个可分配的自由分配单元,即将它放在了自由分配单元链表的头部。注意,其内存地址并没有发生改变。实际上,一个分配单元的内存地址无论是在分配后,还是处于自由状态时,一直都不会变化。变化的只是其状态(已分配/自由),以及当其处于自由状态时在自由分配单元链表中的位置。

    ⑤处检查当回收完毕后,包含此回收单元的内存块的所有单元是否都处于自由状态,且此内存是否处于内存块链表的头部。如果是,将此内存块整个的返回给进程堆,同时修改内存块链表结构。

    注意,这里在判断一个内存块的所有单元是否都处于自由状态时,并没有遍历其所有单元,而是判断nFree乘以nUnitSize是否等于nSize。nSize是内存块中所有分配单元的大小,而不包括头部MemoryBlock结构体的大小。这里可以看到其用意,即用来快速检查某个内存块中所有分配单元是否全部处于自由状态。因为只需结合nFree和nUnitSize来计算得出结论,而无须遍历和计算所有自由状态的分配单元的个数。

    另外还需注意的是,这里并不能比较nFree与nInitSize或nGrowSize的大小来判断某个内存块中所有分配单元都为自由状态,这是因为第1次分配的内存块(分配单元个数为nInitSize)可能被移到链表的后面,甚至可能在移到链表后面后,因为某个时间其所有单元都处于自由状态而被整个返回给进程堆。即在回收分配单元时,无法判定某个内存块中的分配单元个数到底是nInitSize还是nGrowSize,也就无法通过比较nFree与nInitSize或nGrowSize的大小来判断一个内存块的所有分配单元是否都为自由状态。

    以上面分配后的内存池状态作为例子,假设这时第2个内存块中的最后一个单元需要回收(已被分配,假设其编号为m,pFree指针指向它),如图6-5所示。

    不难发现,这时nFirst的值由原来的0变为m。即此内存块下一个被分配的单元是m编号的单元,而不是0编号的单元(最先分配的是最新回收的单元,从这一点看,这个过程与栈的原理类似,即先进后出。只不过这里的"进"意味着"回收",而"出"则意味着"分配")。相应地,m的"下一个自由单元"标记为0,即内存块原来的"下一个将被分配出去的单元",这也表明最近回收的分配单元被插到了内存块的"自由分配单元链表"的头部。当然,nFree递增1。

    图6-5 分配后的内存池状态
    图6-5 分配后的内存池状态

    处理至⑥处之前,其状态如图6-6所示。

    图6-6 处理至⑥处之前的内存池状态
    图6-6 处理至⑥处之前的内存池状态

    这里需要注意的是,虽然pFree被"回收",但是pFree仍然指向m编号的单元,这个单元在回收过程中,其头两个字节被覆写,但其他部分的内容并没有改变。而且从整个进程的内存使用角度来看,这个m编号的单元的状态仍然是"有效的"。因为这里的"回收"只是回收给了内存池,而并没有回收给进程堆,因此程序仍然可以通过pFree访问此单元。但是这是一个很危险的操作,因为首先该单元在回收过程中头两个字节已被覆写,并且该单元可能很快就会被内存池重新分配。因此回收后通过pFree指针对这个单元的访问都是错误的,读操作会读到错误的数据,写操作则可能会破坏程序中其他地方的数据,因此需要格外小心。

    接着,需要判断该内存块的内部使用情况,及其在内存块链表中的位置。如果该内存块中省略号"……"所表示的其他部分中还有被分配的单元,即nFree乘以nUnitSize不等于nSize。因为此内存块不在链表头,因此还需要将其移到链表头部,如图6-7所示。

    图6-7 因回收引起的MemoryBlock移动
    图6-7 因回收引起的MemoryBlock移动

    如果该内存块中省略号"……"表示的其他部分中全部都是自由分配单元,即nFree乘以nUnitSize等于nSize。因为此内存块不在链表头,所以此时需要将此内存块整个回收给进程堆,回收后内存池的结构如图6-8所示。

    图6-8 回收后内存池的结构
    图6-8 回收后内存池的结构

    一个内存块在申请后会初始化,主要是为了建立最初的自由分配单元链表,下面是其详细代码:

    MemoryBlock::MemoryBlock (USHORT nTypes, USHORT nUnitSize)
    	: nSize  (nTypes * nUnitSize),
    	  nFree  (nTypes - 1),                     ④
    	  nFirst (1),                              ⑤
    	  pNext  (0)
    {
    		char * pData = aData;                  ①
    		for (USHORT i = 1; i < nTypes; i++) ②
    		{
    			*reinterpret_cast<USHORT*>(pData) = i; ③
    			pData += nUnitSize;
    		}
    }

    这里可以看到,①处pData的初值是aData,即0编号单元。但是②处的循环中i却是从1开始,然后在循环内部的③处将pData的头两个字节值置为i。即0号单元的头两个字节值为1,1号单元的头两个字节值为2,一直到(nTypes-2)号单元的头两个字节值为(nTypes-1)。这意味着内存块初始时,其自由分配单元链表是从0号开始。依次串联,一直到倒数第2个单元指向最后一个单元。

    还需要注意的是,在其初始化列表中,nFree初始化为nTypes-1(而不是nTypes),nFirst初始化为1(而不是0)。这是因为第1个单元,即0编号单元构造完毕后,立刻会被分配。另外注意到最后一个单元初始并没有设置头两个字节的值,因为该单元初始在本内存块中并没有下一个自由分配单元。但是从上面例子中可以看到,当最后一个单元被分配并回收后,其头两个字节会被设置。

    图6-9所示为一个内存块初始化后的状态。

    图6-9 一个内存块初始化后的状态
    图6-9 一个内存块初始化后的状态

    当内存池析构时,需要将内存池的所有内存块返回给进程堆:

    MemoryPool::~MemoryPool()
    {
        MemoryBlock* pMyBlock = pBlock;
        while ( pMyBlock )
        {
            ……
        }
    }

    6.2.4 使用方法

    分析内存池的内部原理后,本节说明如何使用它。从上面的分析可以看到,该内存池主要有两个对外接口函数,即Alloc和Free。Alloc返回所申请的分配单元(固定大小内存),Free则回收传入的指针代表的分配单元的内存给内存池。分配的信息则通过MemoryPool的构造函数指定,包括分配单元大小、内存池第1次申请的内存块中所含分配单元的个数,以及内存池后续申请的内存块所含分配单元的个数等。

    综上所述,当需要提高某些关键类对象的申请/回收效率时,可以考虑将该类所有生成对象所需的空间都从某个这样的内存池中开辟。在销毁对象时,只需要返回给该内存池。"一个类的所有对象都分配在同一个内存池对象中"这一需求很自然的设计方法就是为这样的类声明一个静态内存池对象,同时为了让其所有对象都从这个内存池中开辟内存,而不是缺省的从进程堆中获得,需要为该类重载一个new运算符。因为相应地,回收也是面向内存池,而不是进程的缺省堆,还需要重载一个delete运算符。在new运算符中用内存池的Alloc函数满足所有该类对象的内存请求,而销毁某对象则可以通过在delete运算符中调用内存池的Free完成。

    6.2.5 性能比较

    为了测试利用内存池后的效果,通过一个很小的测试程序可以发现采用内存池机制后耗时为297 ms。而没有采用内存池机制则耗时625 ms,速度提高了52.48%。速度提高的原因可以归结为几点,其一,除了偶尔的内存申请和销毁会导致从进程堆中分配和销毁内存块外,绝大多数的内存申请和销毁都由内存池在已经申请到的内存块中进行,而没有直接与进程堆打交道,而直接与进程堆打交道是很耗时的操作;其二,这是单线程环境的内存池,可以看到内存池的Alloc和Free操作中并没有加线程保护措施。因此如果类A用到该内存池,则所有类A对象的创建和销毁都必须发生在同一个线程中。但如果类A用到内存池,类B也用到内存池,那么类A的使用线程可以不必与类B的使用线程是同一个线程。

    另外,在第1章中已经讨论过,因为内存池技术使得同类型的对象分布在相邻的内存区域,而程序会经常对同一类型的对象进行遍历操作。因此在程序运行过程中发生的缺页应该会相应少一些,但这个一般只能在真实的复杂应用环境中进行验证。

    6.3 本章小结

    内存的申请和释放对一个应用程序的整体性能影响极大,甚至在很多时候成为某个应用程序的瓶颈。消除内存申请和释放引起的瓶颈的方法往往是针对内存使用的实际情况提供一个合适的内存池。内存池之所以能够提高性能,主要是因为它能够利用应用程序的实际内存使用场景中的某些"特性"。比如某些内存申请与释放肯定发生在一个线程中,某种类型的对象生成和销毁与应用程序中的其他类型对象要频繁得多,等等。针对这些特性,可以为这些特殊的内存使用场景提供量身定做的内存池。这样能够消除系统提供的缺省内存机制中,对于该实际应用场景中的不必要的操作,从而提升应用程序的整体性能。



    读者反馈

    欢迎您对本书提出宝贵的反馈意见。您可以通过本页面最下方的 建议 栏目为本文打分,并反馈您的建议和意见。

    如果您对 developerWorks 图书频道有什么好的建议,欢迎您将建议发给我们

    参考资料



    original  link:http://www.ibm.com/developerworks/cn/linux/l-cn-ppp/index6.html
    展开全文
  • 内存抖动出现原因主要是频繁(很重要)在循环里创建对象(导致大量对象在短时间内被创建,由于新对象是要占用内存空间的而且是频繁,如果一次或者两次在循环里创建对象对内存影响不大,不会造成严重内存抖动这样可以...

    内存抖动是指在短时间内有大量的对象被创建或者被回收的现象,内存抖动出现原因主要是频繁(很重要)在循环里创建对象(导致大量对象在短时间内被创建,由于新对象是要占用内存空间的而且是频繁,如果一次或者两次在循环里创建对象对内存影响不大,不会造成严重内存抖动这样可以接受也不可避免,频繁的话就很内存抖动很严重),内存抖动的影响是如果抖动很频繁,会导致垃圾回收机制频繁运行(短时间内产生大量对象,需要大量内存,而且还是频繁抖动,就可能会需要回收内存以用于产生对象,垃圾回收机制就自然会频繁运行了)。综上就是频繁内存抖动会导致垃圾回收频繁运行。

    内存泄漏是指某一段内存在程序里功能上已经不需要了,但是垃圾回收机制回收内存时检测那段内存还是被需要的,不能被回收,这种在程序中在没有使用的但是又不能被回收的内存就是被泄漏的内存,那为什么会这样呢?正常的话应该是程序里不需要的内存就可以被回收,这是垃圾回收机制做的事呀,如果垃圾回收机制正常运行的情况下,不应该这样啊,但是实际就是垃圾回收机制正常的情况下发生的内存泄漏。其实到这里Java程序员就得知道垃圾回收机制中,判断一段内存是否是垃圾,是否可回收的条件,这个条件是通过检查这段内存是否存在引用和被引用关系,不存在这关系时,就认为可回收,若还存在引用或被引用关系,就认为不可回收,现在就可以知道导致内存泄漏的原因是程序员没有将不用的内存去掉引用关系(因为程序中大多内存石油对象指向的,所以去掉引用关系就是置空)。内存泄漏会导致一些内存没法被正常利用,话句话就是可以使用内存变少了,这样轻则增加垃圾回收机制运行频率,重则内存溢出(当系统需要分配一段内存,但是现有内存在垃圾回收运行后任然不足时,就会内存溢出);为避免内存泄漏,在写程序时已经确定不需要的引用型变量,就置空;虽然即使内存没泄露,也有可能出现内存溢出,这时的内存溢出就是有别的问题导致的。

    1) Memory Churn and performance(内存抖动和性能)

    虽然Android有自动管理内存的机制,但是对内存的不恰当使用仍然容易引起严重的性能问题。在同一帧里面创建过多的对象是件需要特别引起注意的事情。

    Android系统里面有一个Generational Heap Memory的模型,系统会根据内存中不同的内存数据类型分别执行不同的GC操作。例如,最近刚分配的对象会放在Young Generation区域,这个区域的对象通常都是会快速被创建并且很快被销毁回收的,同时这个区域的GC操作速度也是比Old Generation区域的GC操作速度更快的。

     

    除了速度差异之外,执行GC操作的时候,任何线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行(所以垃圾回收运行的次数越少,对性能的影响就越少)

     

    通常来说,单个的GC并不会占用太多时间,但是大量不停的GC操作则会显著占用帧间隔时间(16ms)。如果在帧间隔时间里面做了过多的GC操作,那么自然其他类似计算,渲染等操作的可用时间就变得少了。

    导致GC频繁执行有两个原因:

    ·Memory Churn内存抖动,内存抖动是因为大量的对象被创建又在短时间内马上被释放。

    ·瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题(帧率是Android渲染机制中的概念,导致卡顿慢的直接原因,就是渲染机制受阻,关于渲染机制有另一篇博客特别说了,想了解的可以点击这里)。

     

    解决上面的问题有简洁直观方法,如果你在Memory Monitor里面查看到短时间发生了多次内存的涨跌,这意味着很有可能发生了内存抖动。

     

    同时我们还可以通过Allocation Tracker来查看在短时间内,同一个栈中不断进出的相同对象。这是内存抖动的典型信号之一。

    当你大致定位问题之后,接下去的问题修复也就显得相对直接简单了。例如,你需要避免在for循环里面分配对象占用内存,需要尝试把对象的创建移到循环体之外,自定义View中的onDraw方法也需要引起注意,每次屏幕发生绘制以及动画执行过程中,onDraw方法都会被调用到,避免在onDraw方法里面执行复杂的操作,避免创建对象。对于那些无法避免需要创建对象的情况,我们可以考虑对象池模型,通过对象池来解决频繁创建与销毁的问题,但是这里需要注意结束使用之后,需要手动释放对象池中的对象。

    2) Garbage Collection in Android(Android垃圾回收)

    JVM的回收机制给开发人员带来很大的好处,不用时刻处理对象的分配与回收,可以更加专注于更加高级的代码实现。相比起JavaCC++等语言具备更高的执行效率,他们需要开发人员自己关注对象的分配与回收,但是在一个庞大的系统当中,还是免不了经常发生部分对象忘记回收的情况,这就是内存泄漏。

    原始JVM中的GC机制在Android中得到了很大程度上的优化。Android里面是一个三级Generation的内存模型,最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后到Permanent Generation区域。

     

    每一个级别的内存区域都有固定的大小,此后不断有新的对象被分配到此区域,当这些对象总的大小快达到这一级别内存区域的阀值时,会触发GC的操作,以便腾出空间来存放其他新的对象。

     

    前面提到过每次GC发生的时候,所有的线程都是暂停状态的。GC所占用的时间和它是哪一个Generation也有关系,Young Generation的每次GC操作时间是最短的,Old Generation其次,Permanent Generation最长。执行时间的长短也和当前Generation中的对象数量有关,遍历查找20000个对象比起遍历50个对象自然是要慢很多的。

    虽然Google的工程师在尽量缩短每次GC所花费的时间,但是特别注意GC引起的性能问题还是很有必要。如果不小心在最小的for循环单元里面执行了创建对象的操作,这将很容易引起GC并导致性能问题。通过Memory Monitor我们可以查看到内存的占用情况,每一次瞬间的内存降低都是因为此时发生了GC操作,如果在短时间内发生大量的内存上涨与降低的事件(内存严重抖动),这说明很有可能这里有性能问题。我们还可以通过Heap and Allocation Tracker工具来查看此时内存中分配的到底有哪些对象。

    到这里为止就简单介绍了内存抖动(概念及判断方法,方法是两个工具使用,一个用于判断有没有严重内存抖动Memory Monitor,一个用于确认抖动位置Heap and Allocation Tracker),及较详细的介绍了Android中垃圾回收机制。

    2) Performance Cost of Memory Leaks(内存泄漏)

    虽然Java有自动回收的机制,可是这不意味着Java中不存在内存泄漏的问题,而内存泄漏会很容易导致严重的性能问题。

    内存泄漏指的是那些程序不再使用的对象无法被GC识别,这样就导致这个对象一直留在内存当中,占用了宝贵的内存空间。显然,这还使得每级Generation的内存区域可用空间变小,GC就会更容易被触发,从而引起性能问题。

    寻找内存泄漏并修复这个漏洞是件很棘手的事情,你需要对执行的代码很熟悉,清楚的知道在特定环境下是如何运行的,然后仔细排查。例如,你想知道程序中的某个activity退出的时候,它之前所占用的内存是否有完整的释放干净了?首先你需要在activity处于前台的时候使用Heap Tool获取一份当前状态的内存快照,然后你需要创建一个几乎不这么占用内存的空白activity用来给前一个Activity进行跳转,其次在跳转到这个空白的activity的时候主动调用System.gc()方法来确保触发一个GC操作。最后,如果前面这个activity的内存都有全部正确释放,那么在空白activity被启动之后的内存快照中应该不会有前面那个activity中的任何对象了。


    关于内存抖动和内存泄漏就到这里了,接下来就说一下Android studio 提供的内存优化方面的工具

    Android Studio提供了工具来帮助开发者发现和解决内存抖动和内存泄漏。

     Tool - Memory Monitor(用于发现内存抖动及内存泄漏的)

    Android Studio中的Memory Monitor可以很好的帮组我们查看程序的内存使用情况。


    以下内容很重要,以下内容很重要,以下内容很重要重要的事情说三遍

    ·Memory Monitor:查看整个app所占用的内存,以及发生GC的时刻,短时间内发生大量的GC操作是一个危险的信号(用于发现有没有内存泄漏和严重内存抖动)。

    后面两个是用于定位的内存抖动和内存泄漏发生的具体位置·

    Allocation Tracker:使用此工具来追踪内存的分配,前面有提到过。

    Heap Tool:查看当前内存快照,便于对比分析哪些对象有可能是泄漏了的.


    现在就可以定位到某一段代码发生了内存泄漏或抖动。

    如果是内存泄漏解决方法就很直接,在适当的时候把泄漏的对象置空就可以了。

    但是如果只内存抖动的话就得分两种情况,由于内存抖动是在短时间内创建释放大量对象导致的(一般是循环内创建对象),直接办法就是不再短时间内创建大量对象,如果创建对象的过程可以拿到循环外而不影响功能,这种情况比较容易解决。但是更多的是另一种情况,就是不能拿到循环外,否则影响功能。对于第二种情况就要做到在循环内创建对象,但是又要控制对象个数,这个问题目前可以使用对象池的方法解决。

     3)Object Pools

    在程序里面经常会遇到的一个问题是短时间内创建大量的对象,导致内存紧张,从而触发GC导致性能问题。对于这个问题,我们可以使用对象池技术来解决它。通常对象池中的对象可能是bitmapsviewspaints等等。关于对象池的操作原理,不展开述说了,请看下面的图示:

     

    使用对象池技术有很多好处,它可以避免内存抖动,提升性能,但是在使用的时候有一些内容是需要特别注意的。通常情况下,初始化的对象池里面都是空白的,当使用某个对象的时候先去对象池查询是否存在,如果不存在则创建这个对象然后加入对象池,但是我们也可以在程序刚启动的时候就事先为对象池填充一些即将要使用到的数据,这样可以在需要使用到这些对象的时候提供更快的首次加载速度,这种行为就叫做预分配。使用对象池也有不好的一面,程序员需要手动管理这些对象的分配与释放,所以我们需要慎重地使用这项技术,避免发生对象的内存泄漏。为了确保所有的对象能够正确被释放,我们需要保证加入对象池的对象和其他外部对象没有互相引用的关系。

    其实对象池给笔者感觉与线程池相似,不同的是重心不同,线程池考虑的是运行速度提高(使用预先产生空闲线程的方式),对象池更侧重与数量(可能是分配对象内存时间是很短的,所以不需要预分配,导致对象池的预分配优势不明显)。

    现在问题还没解决呢,关于解决内存抖动,对象池很好,但是仅仅是一个思想概念,没具体化。怎么实现呢,笔者推荐使用Java的一个LinkedHashMap 这个类,与普通hashmap有不同,就是可以控制数量关于LinkedHashMap 更详细的信息,笔者已转载一篇感觉很棒的关于LinkedHashMap的博客,点击这里可查看

    在这里Android已提供了一个类可以解决控制数量问题

    LRU Cache 通过使用LinkedHashMap实现了LRU Cache (最近最少使用)算法,这是操作系统的一个算法,具体的自己百度很多,在这里不祥细说明。

    LRUCache 的实现和使用,笔者也转载了一篇博客,感觉很全点击这里可查看,看了妈妈再也不用担心我的Android程序出现内存抖动了。


    一般情况下,常见发生内存泄漏定位到的地方及解决方法如下:

    1.集合类

    集合类如果仅仅有添加元素的机制,而没有相应删除元素机制,这样就会造成内存被占用,如果这个类是全局性变量(比如类中有静态属性,全局性的map等即有静态引用或final一直指向它)。那么没有相应删除机制,很可能导致集合所占内存只增不减。  解决办法:在使用集合类时,增加删除元素机制,并适当调用减少集合所占内存。

    2.单例模式

    不正确使用单例模式,也会引起内存泄漏单例对象在初始化后将在JVM的整个生命周期存在(以静态变量方式),如果单例对象持有外部对象的引用,那么这个外部对象就会一直占用着内存,可能导致内存泄漏(取决于这外部对象是否一致有用)。   解决办法:单例对象中避免含有不是一直都有用的外部对象引用。

    3.Android组件或特殊集合对象的使用

    BraodcastReceiver ,ContentObserver,fileObserver,Cursor,Callback等在Activity onDestory或者某类生命周期结束之后一定要unregistere或者close掉,否则这个Activity类会被system强引用,不会被回收。不要直接对Activity进行直接引用作为成员变量,如果不得不这么做,调用private WeakPeferense mActivity 来做,相同的,对与Service等其他有自己生命周期的对象来说,直接引用都需要考虑是否会存在内存泄露的可能。

    4.Handler

    要知道,只要Handler 发送的Message尚未被处理,则该Message及发送它的Handler对象将被线程MessageQueue一直持有。由于Handler属于TLSThread Local Storage)变量,生命周期和Activity是不一致的。因此这种实现方式一般很难保证跟view或者Activity的生命周期保持一致,故很容易导致无法正确释放。如上所述,Handler使用要特别小心,否则很可能内存泄漏。   解决办法:在view 或者Activity生命周期结束前,确保Handler已没有未处理的消息(特别是延时消息)。

    5.Thread 内存泄漏

    线程也是造成内存泄露的一个重要源头,线程产生内存泄露的主要原因在于线程生命周期不可控,比如线程是Activity的内部类,则线程对象中保存了Activity的一个引用,当线程的run函数耗时较长没有结束时,线程对象是不会被销毁的,因此它所引用的老的Activity就出现了内存泄漏问题。解决办法:1.简化线程run函数执行的任务,使他在Activity生命周期结束前,任务运行完。2.Thread增加撤销机制,当Activity生命周期结束时,将Thread的耗时任务撤销(笔者推荐这种)。

    6.一些不良代码造成的内存压力  

    有些代码并不造成内存泄漏,但是他们是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。

    (1) Bitmap 没调用recycle()

    Bitmap 对象在不使用时,我们应该先调用recycle()释放内存,然后才置空,因为加载bitmap对象的内存空间,一部分是java的,一部分是c的(因为Bitmap分配的底层是通过jni调用的,Android的Bitmap底层是使用skia图形库实现,skia是用c实现的)。这个recycle()函数就是针对c部分的内存释放。

    2)构造Adapter时,没有使用缓存的convertView。   解决办法:使用静态holdview的方式构造Adapter


    转载自:

    http://blog.csdn.net/huang_rong12/article/details/51628264

    http://blog.csdn.net/huang_rong12/article/details/51628750

    展开全文
  • 深入理解Java虚拟机-Java内存区域与内存溢出异常

    万次阅读 多人点赞 2020-01-03 21:42:24
    Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来。 文章目录概述运行时数据区域程序计数器(线程私有)Java虚拟机栈(线程私有)局部变量表操作数栈动态链接...

    本博客主要参考周志明老师的《深入理解Java虚拟机》第二版

    读书是一种跟大神的交流。阅读《深入理解Java虚拟机》受益匪浅,对Java虚拟机有初步的认识。这里写博客主要出于以下三个目的:一方面是记录,方便日后阅读;一方面是加深对内容的理解;一方面是分享给大家,希望对大家有帮助。

    《深入理解Java虚拟机》全书总结如下:

    序号内容链接地址
    1深入理解Java虚拟机-走近Javahttps://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膨胀(重量级锁定)
    空,不需要记录信息11GC标记
    偏向线程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有垃圾收集机制,但内存溢出异常离我们仍然并不遥远,本章只是讲解了各个区域出现内存溢出异常的原因。

    展开全文
  • App内存占用优化

    千次阅读 2017-01-12 18:47:04
    RAM(Random-access memory)在任何软件开发中都是非常宝贵的资源,...我们应当避免引起内存泄露,如持有静态成员变量而导致无法释放,应当在应用的生命周期回调中释放掉所有的引用。本文主要介绍如何减少App中的内存
  • 内存泄漏和内存溢出详解

    千次阅读 热门讨论 2019-12-24 19:54:59
    内存泄漏的根本原因是:长生命周期的对象,持有短生命周期对象的引用,尽管短生命周期的对象已经不再需要,单因为长生命周期的对象持有它的引用而导致不能被GC回收。 发生条件 内存泄漏必须满足以下两个条件 对象...
  • 内存越界问题

    千次阅读 2018-04-24 11:04:31
    最近在做视频编码的工作,在PC上调试没有问题,但是移植到嵌入式ARM设备上的时候就出现了内存越界问题。起初也不知道是内存越界的问题,只是程序运行会出现非常异常的情况。在PC机上做测试的时候,在内存映射的时候...
  • 在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用函数来释放内存,但也随之带来了内存泄漏的可能
  • JVM之内存结构详解

    万次阅读 2019-11-27 23:40:17
    对于开发人员来说,如果不了解Java的JVM,那真的是很难写得一手好代码,很难查得一手好bug。同时,JVM也是面试环节的中重灾区。...下面,开启我们的第一篇文章《JVM之内存结构详解》。 思考一下 ...
  • Android内存优化汇总

    千次阅读 2017-09-05 22:07:47
    所以我将本文定义为一个工具类的文章,如果你在ANDROID开发中遇到关于内存问题,或者马上要参加面试,或者就是单纯的学习或复习一下内存相关知识,都欢迎阅读。(本文最后我会尽量列出所参考的文章)。 内存...
  • JavaScript,会在创建变量(对象,字符串等)时分配内存,并且在不再使用它们时...内存生命周期 JS 环境中分配的内存有如下声明周期内存分配:当我们申明变量、函数、对象的时候,系统会自动为他们分配内存 ...
  • java内存垃圾回收模型

    千次阅读 2016-09-22 23:08:25
    一.java的内存模型介绍如下6个组成部分...2.Java虚拟机栈:线程私有的,其生命周期和线程一致,每个方法执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。3.本地方法栈:与虚拟机栈功能
  • JVM内存模型及分区

    万次阅读 2016-05-30 23:43:12
    重点是Java虚拟机栈,它是线程私有的,生命周期与线程相同。 每个方法执行都会创建一个栈帧,用于存放局部变量表,操作栈,动态链接,方法出口等。每个方法从被调用,直到被执行完。对应着一个栈帧在虚
  • 对于Java开发人员来讲,在虚拟机自动内存管理机制的帮助下,不需要为每一个new操作去写配对的delete/free代码, 不容易出现内存泄露和内存溢出的问题。 正是因为JVM管理内存控制的权利,一旦出现内存泄露和内存溢出...
  • 内存泄漏和内存溢出

    万次阅读 多人点赞 2018-08-22 15:28:58
    内存溢出:(out of memory)通俗理解就是内存不够,通常在运行大型软件或游戏时,软件或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。 内存泄漏:(Memory Leak)是指程序中己动态分配...
  • Java内存区域

    千次阅读 多人点赞 2019-10-30 20:42:05
    对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像 C/C++程序开发程序员这样为每一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利...
  • 日期 内核版本 架构 作者 GitHub ... Linux内存管理在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到
  • JavaScript中的垃圾回收和内存泄漏

    千次阅读 多人点赞 2019-04-30 09:13:36
    程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存。所谓的内存泄漏简单来说是不再用到的内存,没有及时释放。为了更好避免内存泄漏,我们先介绍Javascript垃圾回收机制。 在C与C++等语言中...
  • JVM内存分配与管理详解

    万次阅读 2018-01-23 16:17:58
    了解C++的程序员都知道,在内存管理领域,都是由程序员维护与管理,程序员用于最高的管理权限,但对于java程序员来说,在内存管理领域,程序员不必去关心内存的分配以及回收,在jvm自动内存管理机制的帮助下,不需要...
  • *内存泄漏和内存溢出

    千次阅读 2017-06-27 14:03:20
    内存泄漏:分配出去的内存无法回收(不再使用的对象或者变量仍占内存空间),在Java中内存泄漏就是存在一些被分配的对象(可达的,却是无用的)无法被gc回收。 内存溢出:程序要求的内存超出了系统所能分配的范围...
  • Java之:JVM内存模型

    千次阅读 2016-06-17 15:49:35
    二、JVM内存模型总图Java中通过多线程机制使得多个任务同时执行处理,所有的线程共享JVM内存区域main memory,而每个线程又单独的有自己的工作内存,当线程与内存区域进行交互时,数据从主存拷贝到工作内存,进而交...
  • Java应用程序是运行在JVM上的,得益于JVM的内存管理和垃圾收集机制,开发人员的效率得到了显著提升,也不容易出现内存溢出和泄漏问题。但正是因为开发人员把内存的控制权交给了JVM,一旦出现内存方面的问题,如果不...
  • Java内存区域与内存溢出

    千次阅读 2020-02-09 17:27:55
    在C语言中,开发者需要维护对象的出生和死亡,往往需要为每个new出来的对象编写配套的delete/free代码来释放内存,否则可能发生内存泄漏或溢出。 而在Java中,内存由JVM管理,垃圾回收器GC会帮助开发者自动回收不再...
  • Java垃圾回收器与内存分配策略

    千次阅读 2017-08-26 15:38:52
    上一篇JVM内存模型讲述了Java虚拟机在运行时所管理的内存划分下的每个数据区域的各自用途,以及创建和销毁时间。当需要排查各种内存泄漏、内存溢出问题时,当来及收集成为系统达到更高并发量的瓶颈时,我们需要对JVM...
  • Android内存泄露与内存溢出

    万次阅读 2016-12-26 20:50:17
    一、 内存泄漏与内存溢出(OOM) 1. 内存泄露 垃圾回收器无法回收原本应该被回收的对象,这个对象就引发了内存泄露。 内存泄露的危害: (1)过多的内存泄露最终会导致内存溢出(OOM)(2)内存泄露导致可用内存...
  • java内存泄露和内存溢出

    千次阅读 2017-07-06 16:36:04
    内存泄露:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。 内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于...
  • Java基础恶补——内存泄露、内存溢出

    万次阅读 多人点赞 2013-11-19 09:29:28
    Java基础恶补——内存泄露、内存溢出 (2010-09-15 15:56:26) 转载 标签: 杂谈   要点 内存泄露是指程序中间动态分配了内存,但在程序结束时没有释放这部分内存,从而造成那部分...
  • 深入理解Java虚拟机--JVM内存模型

    万次阅读 2017-05-13 23:10:24
    7、直接内存 二、OutOfMemoryError异常 1、Java堆溢出 2、虚拟机栈和本地方法栈溢出 3、方法区和运行时常量池溢出 4、本机直接内存溢出 参考书籍:《深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)》 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 98,491
精华内容 39,396
关键字:

内存循环周期大小