精华内容
下载资源
问答
  • Linux内核中常见内存分配函数

    千次阅读 2019-04-17 09:13:56
    转载:Linux内核中常见内存分配函数 1.原理说明 Linux内核中采用了一种同时适用于32位和64位系统的内存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系统中,用到了四级页表,如图2-1所示。四级页表...

    转载:Linux内核中常见内存分配函数

    1.   原理说明

    Linux内核中采用了一种同时适用于32位和64位系统的内存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系统中,用到了四级页表,如图2-1所示。四级页表分别为:

    l  页全局目录(Page Global Directory)

    l  页上级目录(Page Upper Directory)

    l  页中间目录(Page Middle Directory)

    l  页表(Page Table)

        页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录的地址,而页中间目录又包含若干页表的地址,每一个页表项指向一个页框。Linux中采用4KB大小的页框作为标准的内存分配单元。

    多级分页目录结构

    1.1.   伙伴系统算法

        在实际应用中,经常需要分配一组连续的页框,而频繁地申请和释放不同大小的连续页框,必然导致在已分配页框的内存块中分散了许多小块的空闲页框。这样,即使这些页框是空闲的,其他需要分配连续页框的应用也很难得到满足。

        为了避免出现这种情况,Linux内核中引入了伙伴系统算法(buddy system)。把所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块。最大可以申请1024个连续页框,对应4MB大小的连续内存。每个页框块的第一个页框的物理地址是该块大小的整数倍。

        假设要申请一个256个页框的块,先从256个页框的链表中查找空闲块,如果没有,就去512个页框的链表中找,找到了则将页框块分为2个256个页框的块,一个分配给应用,另外一个移到256个页框的链表中。如果512个页框的链表中仍没有空闲块,继续向1024个页框的链表查找,如果仍然没有,则返回错误。

        页框块在释放时,会主动将两个连续的页框块合并为一个较大的页框块。

    1.2.   slab分配器

        slab分配器源于 Solaris 2.4 的分配算法,工作于物理内存页框分配器之上,管理特定大小对象的缓存,进行快速而高效的内存分配。

        slab分配器为每种使用的内核对象建立单独的缓冲区。Linux 内核已经采用了伙伴系统管理物理内存页框,因此 slab分配器直接工作于伙伴系统之上。每种缓冲区由多个 slab 组成,每个 slab就是一组连续的物理内存页框,被划分成了固定数目的对象。根据对象大小的不同,缺省情况下一个 slab 最多可以由 1024个页框构成。出于对齐等其它方面的要求,slab 中分配给对象的内存可能大于用户要求的对象实际大小,这会造成一定的内存浪费。

    2.    常用内存分配函数

    2.1.   __get_free_pages

        unsigned long __get_free_pages(gfp_tgfp_mask, unsigned int order)

     

        __get_free_pages函数是最原始的内存分配方式,直接从伙伴系统中获取原始页框,返回值为第一个页框的起始地址。__get_free_pages在实现上只是封装了alloc_pages函数,从代码分析,alloc_pages函数会分配长度为1<<order的连续页框块。order参数的最大值由include/linux/Mmzone.h文件中的MAX_ORDER宏决定,在默认的2.6.18内核版本中,该宏定义为10。也就是说在理论上__get_free_pages函数一次最多能申请1<<10 * 4KB也就是4MB的连续物理内存。但是在实际应用中,很可能因为不存在这么大量的连续空闲页框而导致分配失败。在测试中,order为10时分配成功,order为11则返回错误。

    2.2.   kmem_cache_alloc

        struct kmem_cache *kmem_cache_create(constchar *name, size_t size,

            size_talign, unsigned long flags,

            void(*ctor)(void*, struct kmem_cache *, unsigned long),

            void(*dtor)(void*, struct kmem_cache *, unsigned long))

        void *kmem_cache_alloc(struct kmem_cache *c,gfp_t flags)

     

        kmem_cache_create/ kmem_cache_alloc是基于slab分配器的一种内存分配方式,适用于反复分配释放同一大小内存块的场合。首先用kmem_cache_create创建一个高速缓存区域,然后用kmem_cache_alloc从该高速缓存区域中获取新的内存块。 kmem_cache_alloc一次能分配的最大内存由mm/slab.c文件中的MAX_OBJ_ORDER宏定义,在默认的2.6.18内核版本中,该宏定义为5,于是一次最多能申请1<<5 * 4KB也就是128KB的连续物理内存。分析内核源码发现,kmem_cache_create函数的size参数大于128KB时会调用BUG()。测试结果验证了分析结果,用kmem_cache_create分配超过128KB的内存时使内核崩溃。

    2.3.   kmalloc

        void *kmalloc(size_t size, gfp_t flags)

     

        kmalloc是内核中最常用的一种内存分配方式,它通过调用kmem_cache_alloc函数来实现。kmalloc一次最多能申请的内存大小由include/linux/Kmalloc_size.h的内容来决定,在默认的2.6.18内核版本中,kmalloc一次最多能申请大小为131702B也就是128KB字节的连续物理内存。测试结果表明,如果试图用kmalloc函数分配大于128KB的内存,编译不能通过。

    2.4.   vmalloc

        void *vmalloc(unsigned long size)

     

        前面几种内存分配方式都是物理连续的,能保证较低的平均访问时间。但是在某些场合中,对内存区的请求不是很频繁,较高的内存访问时间也可以接受,这是就可以分配一段线性连续,物理不连续的地址,带来的好处是一次可以分配较大块的内存。图3-1表示的是vmalloc分配的内存使用的地址范围。vmalloc对一次能分配的内存大小没有明确限制。出于性能考虑,应谨慎使用vmalloc函数。在测试过程中,最大能一次分配1GB的空间。

    Linux内核部分内存分布

    2.5.   dma_alloc_coherent

        void *dma_alloc_coherent(struct device *dev,size_t size,

    ma_addr_t*dma_handle, gfp_t gfp)

        DMA是一种硬件机制,允许外围设备和主存之间直接传输IO数据,而不需要CPU的参与,使用DMA机制能大幅提高与设备通信的吞吐量。DMA操作中,涉及到CPU高速缓存和对应的内存数据一致性的问题,必须保证两者的数据一致,在x86_64体系结构中,硬件已经很好的解决了这个问题,dma_alloc_coherent和__get_free_pages函数实现差别不大,前者实际是调用__alloc_pages函数来分配内存,因此一次分配内存的大小限制和后者一样。__get_free_pages分配的内存同样可以用于DMA操作。测试结果证明,dma_alloc_coherent函数一次能分配的最大内存也为4M。

    2.6.   ioremap

        void * ioremap (unsigned long offset,unsigned long size)

        ioremap是一种更直接的内存“分配”方式,使用时直接指定物理起始地址和需要分配内存的大小,然后将该段物理地址映射到内核地址空间。ioremap用到的物理地址空间都是事先确定的,和上面的几种内存分配方式并不太一样,并不是分配一段新的物理内存。ioremap多用于设备驱动,可以让CPU直接访问外部设备的IO空间。ioremap能映射的内存由原有的物理内存空间决定,所以没有进行测试。

    2.7.   Boot Memory

        如果要分配大量的连续物理内存,上述的分配函数都不能满足,就只能用比较特殊的方式,在Linux内核引导阶段来预留部分内存。

    2.7.1.      在内核引导时分配内存

        void* alloc_bootmem(unsigned long size)

        可以在Linux内核引导过程中绕过伙伴系统来分配大块内存。使用方法是在Linux内核引导时,调用mem_init函数之前用alloc_bootmem函数申请指定大小的内存。如果需要在其他地方调用这块内存,可以将alloc_bootmem返回的内存首地址通过EXPORT_SYMBOL导出,然后就可以使用这块内存了。这种内存分配方式的缺点是,申请内存的代码必须在链接到内核中的代码里才能使用,因此必须重新编译内核,而且内存管理系统看不到这部分内存,需要用户自行管理。测试结果表明,重新编译内核后重启,能够访问引导时分配的内存块。

    2.7.2.      通过内核引导参数预留顶部内存

        在Linux内核引导时,传入参数“mem=size”保留顶部的内存区间。比如系统有256MB内存,参数“mem=248M”会预留顶部的8MB内存,进入系统后可以调用ioremap(0xF800000,0x800000)来申请这段内存。

    3.    几种分配函数的比较

     

    分配原理

    最大内存

    其他

    __get_free_pages

    alloc_pages

    直接对页框进行操作,返回线性地址

    返回页地址

    4MB

    适用于分配较大量的连续物理内存

    kmem_cache_alloc

    基于slab机制实现

    128KB

    适合需要频繁申请释放相同大小内存块时使用

    kmalloc

    基于kmem_cache_alloc实现

    128KB

    最常见的分配方式,需要小于页框大小的内存时可以使用

    vmalloc

    建立非连续物理内存到虚拟地址的映射

     

    物理不连续,适合需要大内存,但是对地址连续性没有要求的场合

    dma_alloc_coherent

    基于__alloc_pages实现

    4MB

    适用于DMA操作

    ioremap

    实现已知物理地址到虚拟地址的映射

     

    适用于物理地址已知的场合,如设备驱动

    alloc_bootmem

    在启动kernel时,预留一段内存,内核看不见

     

    小于物理内存大小,内存管理要求较高

      注:表中提到的最大内存数据来自CentOS5.3 x86_64系统,其他系统和体系结构会有不同

     

    展开全文
  • 文章目录1. Android虚拟机:Dalvik和ART1.1 JVM与Dalvik... 常见内存分析工具2.1 Android Profiler2.1.1 Allocation Tracker2.1.2 Heap Dump2.2 MAT2.3 LeakCanary 在Android性能优化(1):常见内存泄漏与优化(一...


    Android性能优化(1):常见内存泄漏与优化(一)一文中,我们详细阐述了Java虚拟机工作原理和Android开发中常见的内存泄漏及其优化方法,本文将在此基础上继续学习Android虚拟机发展历程、Dalvik/ART的运行时堆、Dalvik/ART启动流程以及常见的内存分析工具的特点和使用方法,包括Android Profiler、MAT、LeakCanary等。

    1. Android虚拟机:Dalvik和ART

     Dalvik是Google特别设计专门用于Android平台的虚拟机,它位于Android系统架构的Android的运行环境(Android Runtime)中,是Android移动设备平台的核心组成部分之一。类似于传统的JVMDalvik虚拟机是在Android操作系统上虚拟出来的一个“设备”,用来运行Android应用程序(APP),主要负责堆栈管理、线程管理、安全及异常管理、垃圾回收、对象的生命周期管理等。在Android系统中,每一个APP对应着一个Dalvik虚拟机实例。Dalvik虚拟机支持.dex(即"Dalvik Executable")格式的Java应用程序的运行,.dex是专为Dalvik设计的一种压缩格式,它是在.class字节码文件的基础上经过DEX工具压缩、优化后得到的,适用于内存和处理器速度有限的系统。在Android 4.4以前的系统中,Android系统均采用Dalvik作为运行Android应用的虚拟机,但随着Dalvik的不足逐渐暴露,到Android 5.0以后的系统使用ART虚拟机完全取代了Dalvik虚拟机。ART虚拟机在性能上做了很多优化,比如采用预编译(AOT,Ahead Of Time compilation)取代JIT编译器、支持64位CPU、改进垃圾回收机制等等,从而使得Android系统运行更为流畅。下图展示了Android系统架构和DVM架构:
    在这里插入图片描述
     Android虚拟机的使用,使得Android应用和Linux内核分离,从而使得Android系统更加稳定可靠,也就是说,即便是某个Android程序被嵌入了恶意代码,也不会直接影响系统的正常运行。接下来,我们就从分析JVM、Dalvik、ART三者之间的关系,来进一步了解它们。

    1.1 JVM与Dalvik区别

     在Android 4.4以前,Android中的所有Java程序都是运行在Dalvik虚拟机上的,每个Android应用进程对应着一个独立的Dalvik虚拟机实例并在其解释下执行。虽然Dalvik虚拟机也是用来运行Java程序,但是它并没有遵守Java虚拟机规范来实现,是Google为Android平台特殊设计且运行在Android 运行时库的虚拟机,因此Dalvik虚拟机并不是一个Java虚拟机。它们的主要区别如下:

    • (1) 基于的架构不同

    JVM基于栈架构,Dalvik虚拟机基于寄存器架构。JVM基于栈则意味着需要去栈中读写数据,所需更多的指令会更多(主要是load/store指令),这样会导致速度变慢,对于性能有限的移动设备,显然是不合适的;Dalvik虚拟机基于寄存器实现,则意味着它的指令会更加紧凑、简洁,因为虚拟机在复制数据时不需要使用大量的出入栈指令,但由于需要指定源地址和目标地址,所以基于寄存器的指令会比基于栈的指令要大,当然,由于指令数量的减少,总的代码数不会增加多少。下图的一段Java程序代码,展示了在JVM和Dalvik虚拟机中字节码的表现形式:
    在这里插入图片描述

    Java字节码以单字节(1 byte)为单元,JVM使用的指令只占1个单元;Dalvik字节码以双字节(2 byte)为单元,Dalvik虚拟机使用的指令占1个单元或2个单元。因此,在上面的代码中JVM字节码占11个单元=11字节,Dalvik字节码占6个单元=12字节(其中,mul-int/lit8指令占2单元)。

    • (2) 执行的字节码文件不同

    JVM运行的.class文件,Dalvik运行的是.dex(即Dalvik Executable)文件。在Java程序中,Java类会编译成一个或多个.class文件,然后打包到.jar文件中,.jar文件中的每个.class文件里面包含了该类的常量池、类信息、属性等。当JVM加载该.jar文件时,会加载里面的所有的.class文件,JVM的这种加载方式很慢,对于内存有限的移动设备并不合适;.dex文件是在.class文件的基础上,经过DEX工具压缩和优化后形成的,通常每一个.apk文件中只包含了一个.dex,这个.dex文件将所有的.class里面所包含的信息全部整合在一起了,这样做的好处就是减少了整体的文件尺寸(去除了.class文件中相同的冗余信息),同时减少了I/O操作加快了类的查找速度。下图展示了.jar和.dex的对比差异:
    在这里插入图片描述

    • (3) 在内存中的表现形式差异

    Dalvik经过优化,允许在有限的内存中同时运行多个进程,或说同时运行多个Dalvik虚拟机的实例。在Android中每一个应用都运行在一个Dalvik虚拟机实例中,每一个Dalvik虚拟机实例都运行在一个独立的进程空间中,因此都对应着一个独立的进程,独立的进程可以防止在虚拟机崩溃时所有程序都被关闭。而对于JVM来说,在其宿主OS的内存中只运行着一个JVM的实例,这个JVM实例中可以运行多个Java应用程序(进程),但是一旦JVM异常崩溃,就会导致运行在其中的所有程序被关闭。

    • (4) Dalvik拥有Zygote进程与共享机制

     在Android系统中有个一特殊的虚拟机进程--Zygote,它是虚拟机实例的孵化器。它在Android系统启动的时候就会产生,完成虚拟机的初始化、库的加载、预制类库和初始化操作。如果系统需要一个新的虚拟机实例,他会迅速复制自身,以最快的速度提供给系统。对于一些只读的系统库,所有的虚拟机实例都和Zygote共享一块区域。 Dalvik虚拟机拥有预加载-共享的机制,使得不同的应用之间在运行时可以共享相同的类,因此拥有更高的效率。而JVM则不存在这个共享机制,不同的程序被打包后都是彼此独立的,即便它们在包里使用了相同的类,运行时的都是单独加载和运行,无法进行共享。
    在这里插入图片描述

    1.2 Dalvik与ART区别

    ART虚拟机被引入于Android 4.4,用来替换Dalvik虚拟机,以缓解Dalvik虚拟机的运行机制导致Android应用运行变慢的问题。在Android 4.4中,可以选择使用Dalvik还是ART,而从Android 5.0开始,Dalvik被完全删除,Android系统默认采用ART。Dalvik与ART的主要区别如下:

    • (1) ART运行机制优于Dalvik

     对于运行在Dalvik虚拟机实例中的应用程序而言,在每一次重新运行的时候,都需要将字节码通过JIT(Just-In-Time)编译器编译成机器码,这会使用应用程序的运行效率降低,虽然Dalvik虚拟机已经被做过很多优化(.dex文件->.odex文件),但由于这种先翻译再执行的机制仍然无法有效解决Dalvik拖慢Android应用运行的事实。而在ART中,系统在安装应用程序时会进行一次AOT(Ahead Of Time compilication,预编译),即将字节码预先编译成机器码并存储在本地,这样应用程序每次运行时就不需要执行编译了,运行效率会大大提高。
    在这里插入图片描述

    • (2) 支持的CPU架构不同

     Dalvik是为32位CPU设计的,而ART支持64位并兼容32位的CPU。

     Dalvik虚拟机的运行时堆使用标记--清除(Mark--Sweep)算法进行GC,它由两个Space以及多个辅助数据结构组成,两个Space分别是Zygote Space(Zygote Heap)Allocation Space(Active Heap)Zygote Space用来管理Zygote进程在启动过程中预加载和创建的各种对象,Zygote Space中不会触发GC,应用进程和Zygote进程之间会共享Zygote Space。Zygote进程在fork第一个子进程之前,会把Zygote Space分为两个部分,原来被Zygote进程使用的部分仍然叫Zygote Space,而剩余未被使用的部分被称为Allocation Space,以后fork的子进程相关的所有的对象都会在Allocation Space上进行分配和释放。需要注意的是,Allocation Space不是进程共享的,在每个进程中都独立拥有一份。下图展示了Dalvik虚拟机的运行时堆结构:

     与Dalvik的GC不同,ART采用了多种垃圾收集方案,每个方案会运行不同的垃圾收集器,默认是采用了CMS(Concurrent Mark-Sweep)方案,该方案主要使用了sticky-CMS和patial-CMS。根据不同的CMS方案,ART的运行时堆的空间划分也会不同,默认是由4个Space和多个辅助数据结构组成,4个Space分别是Zygote SpaceAllocation SpaceImage Space以及Large Object Space,其中,Zygote SpaceAllocation Space和Dalvik的一样,Image Space用来存放一些预加载类,Large Object Space用来分配一些大对象(默认大小为12Kb)。需要注意的是,Zygote SpaceImage Space是进程间共享的。下图展示了ART采用标记–清除算法的堆划分:


    ART虚拟机的不足:

    • 安装时间变长。应用在安装的时候需要预编译,从而增大了安装时间。
    • 存储空间变大。ART引入AOT技术后,需要更多的空间存储预编译后的机器码。

     因此,从某些程度上来说,ART虚拟机是利用了“空间换时间”,来提高Android应用的运行速度。为了缓解上述的不足,Android7.0(N)版本中的ART加入了即时编译器JIT,作为AOT的一个补充,在应用程序安装时并不会将字节码全部编译成机器码,而是在运行中将热点代码编译成机器码,具体来说,就是当我们第一次运行应用相关程序后,JIT提供了一套追踪机制来决定哪一部分代码需要在手机处于idle状态和充电的时候来编译,并将编译得到的机器码存储到本地,这个追踪技术被称为Profile Guided Compilcation

    1.3 Dalvik/ART的启动流程

     从剖析Android系统的启动过程一文可知,init进程(pid=1)是Linux系统的用户进程,是所有用户进程的父进程。当Android系统在启动init进程后,该进程会孵化出一堆用户守护进程、ServiceManager服务以及Zygote进程等等,其中,Zygote进程是Android系统启动的第一个Java进程,或者说是虚拟机进程,因为它持有Dalvik或ART的实例。Zygote进程是所有Java进程的父进程,每当系统需要创建一个应用程序时,Zygote进程就会fork自身,并快速地创建和初始化一个虚拟机实例,用于应用程序的运行。下面我们就从Android9.0源码的角度,来分析Zygote进程中的Dalvik或ART虚拟机实例是如何创建的。

     首先,根据init.rc的启动服务的命令,将运行/system/bin/app_process可执行程序来启动zygote进程,该可执行程序对应的源文件为../base/cmds/app_process/app_main.cpp,也就是说,当从init进程发起启动Zygote进程后,会调用app_main.cppmain函数进入Zygote进程的启动流程。app_main.cpp的main函数源码如下:

    //app_main.cpp$main函数
    int main(int argc, char* const argv[])
    {
        ...
        // (1) 创建AppRuntime对象
        AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
        
        ...
    	// (2) 解析执行init.rc的启动服务的命令传入的参数
        // 解析后:zygote = true
        //        startSystemServer = true
        //        niceName = zygote (当前进程名称)
        bool zygote = false;
        bool startSystemServer = false;
        bool application = false;
        String8 niceName;
        String8 className;
        while (i < argc) {
            const char* arg = argv[i++];
            if (strcmp(arg, "--zygote") == 0) {
                zygote = true;
                niceName = ZYGOTE_NICE_NAME;
            } else if (strcmp(arg, "--start-system-server") == 0) {
                startSystemServer = true;
            } else if (strcmp(arg, "--application") == 0) {
                application = true;
            } else if (strncmp(arg, "--nice-name=", 12) == 0) {
                niceName.setTo(arg + 12);
            } else if (strncmp(arg, "--", 2) != 0) {
                className.setTo(arg);
                break;
            } else {
                --i;
                break;
            }
        }
    	...
        // (3) 设置进程名为Zygote,执行ZygoteInit类
        // Zygote = true
        if (!niceName.isEmpty()) {
            runtime.setArgv0(niceName.string());
            set_process_name(niceName.string());
        }
        if (zygote) {
            runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
        } else if (className) {
            runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
        } else {
            fprintf(stderr, "Error: no class name or --zygote supplied.\n");
            app_usage();
            LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
            return 10;
        }
    }
    
    

     从app_main.cpp$main函数源码可知,为了启动Zygote进程,该函数主要做个如下三个方面的工作,即:

    • 创建AppRuntime实例。AppRuntime是在app_process.cpp中定义的类,继承于系统的AndroidRuntime,主要用于创建和初始化虚拟机。AppRuntime类继承关系如下:
    class AppRuntime : public AndroidRuntime
    {};
    
    • 解析执行init.rc的启动服务的命令传入的参数。/init.zygote64_32.rc文件中启动Zygote的内容如下,在<Android源代码目录>/system/core/rootdir/ 目录下可以看到init.zygote32.rc、init.zygote32_64.rc、init.zygote64.rc、init.zygote64_32.rc等文件,这是因为Android5.0开始支持64位的编译,所以Zygote进程本身也有32位和64位版本。启动Zygote进程命令如下:

      service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
          class main
          priority -20
          socket zygote stream 660 root system
          onrestart write /sys/android_power/request_state wake
          onrestart write /sys/power/state on
          onrestart restart audioserver
          onrestart restart cameraserver
          onrestart restart media
          onrestart restart netd
          writepid /dev/cpuset/foreground/tasks /dev/stune/foreground/tasks
      
    • 执行ZygoteInit类。由前面 解析命令传入的参数可知,zygote=true说明当前程序运行的进程是Zygote进程,将调用AppRuntime的start函数执行ZygoteInit类,从类名可以看出执行该类将进入Zygote的初始化流程。

    runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    

     接着,我们详细分析下AppRuntime的start函数执行流程。由于AppRuntime继承于AndroidRuntime,因此start函数具体在AndroidRuntime中实现。该函数主要完成三个方面的工作:(a) 初始化JNI环境,启动虚拟机;(b) 为虚拟机注册JNI方法;(c)从传入的com.android.internal.os.ZygoteInit 类中找到main函数,即调用ZygoteInit.java类中的main方法。AndroidRuntime$start源码如下:

    void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
    {
        ...
        // (1) 初始化JNI环境、启动虚拟机
        JniInvocation jni_invocation;
        jni_invocation.Init(NULL);
        JNIEnv* env; 
        if (startVm(&mJavaVM, &env, zygote) != 0) {
            return;
        }
        onVmCreated(env);
    
        // (2) 为虚拟机注册JNI方法
        if (startReg(env) < 0) {
            ALOGE("Unable to register all android natives\n");
            return;
        }
        
        ...
        // (3) 从传入的com.android.internal.os.ZygoteInit 类中找到main函数,即调用	
        // ZygoteInit.java类中的main方法。AndroidRuntime及之前的方法都是native的方法,而此刻  	
            // 调用的ZygoteInit.main方法是java的方法,到这里我们就进入了java的世界
        char* slashClassName = toSlashClassName(className);
        jclass startClass = env->FindClass(slashClassName);
        if (startClass == NULL) {
            ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
            /* keep going */
        } else {
            jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
                "([Ljava/lang/String;)V");
            if (startMeth == NULL) {
                ALOGE("JavaVM unable to find main() in '%s'\n", className);
                /* keep going */
            } else {
                env->CallStaticVoidMethod(startClass, startMeth, strArray);
    			#if 0
                if (env->ExceptionCheck())
                    threadExitUncaughtException(env);
    			#endif
            }
        }
        ...
    }
    

     至此,随着AndroidRuntime$startVm函数被调用,Init进程是如何启动Zygote进程和在Zygote进程中创建虚拟机的实例的这个过程我们就分析完毕了,也验证了Zygote进程在被创建启动后,确实已经持有了虚拟机的实例,以至于Zygote进程fork自身创建应用程序时,应用程序也得到了虚拟机的实例,这样就不需要每次启动应用程序进程都要创建虚拟机实例,从而加快了应用程序进程的启动速度。至于被创建的是Dalvik还是ART实例,我们可以看注释(1)处调用了jni_invocationInit()函数,该函数源码如下,位于源码根目录下libnativehelper/Jnilnvocation.cpp源文件中。该函数源码如下:

    #ifdef __ANDROID__
    #include <sys/system_properties.h>
    #endif
    
    // JniInvocation::Init
    bool JniInvocation::Init(const char* library) {
        // Android平台标志
        #ifdef __ANDROID__
          char buffer[PROP_VALUE_MAX];
        #else
          char* buffer = NULL;
        #endif
          // 获取“libart.so”或“libdvm.so”
          library = GetLibrary(library, buffer);
          const int kDlopenFlags = RTLD_NOW | RTLD_NODELETE;
          // 加载“libart.so”或“libdvm.so”
          handle_ = dlopen(library, kDlopenFlags);
          if (handle_ == NULL) {
            if (strcmp(library, kLibraryFallback) == 0) {
              return false;
            }
            library = kLibraryFallback;
            handle_ = dlopen(library, kDlopenFlags);
            if (handle_ == NULL) {
              ALOGE("Failed to dlopen %s: %s", library, dlerror());
              return false;
            }
          }
          ...
          return true;
    }
    

     从JniInvocation::Init函数源码可知,它首先会调用JniInvocation::GetLibrary函数来获取要指定的虚拟机库名称–“libart.so”或“libdvm.so”,然后调用JniInvocation::dlopen函数加载这个虚拟机库。通过查阅JniInvocation::GetLibrary函数源码可知,如果当前不是Debug模式构建的,是不允许动态更改虚拟机动态库,即默认为"libart.so";如果当前是Debug模式构建且传入的buffer不为NULL时,就需要通过读取"persist.sys.dalvik.vm.lib.2"这个系统属性来设置返回的library。JniInvocation::GetLibrary函数源码如下:

    static const char* kLibraryFallback = "libart.so";
    const char* JniInvocation::GetLibrary(const char* library, char* buffer) {
      return GetLibrary(library, buffer, &IsDebuggable, &GetLibrarySystemProperty);
    }
    
    const char* JniInvocation::GetLibrary(const char* library, 
                             char* buffer, 
                             bool (*is_debuggable)(),
                             int (*get_library_system_property)(char* buffer)) {
    #ifdef __ANDROID__
      const char* default_library;
      // 如果不是debug构建,不允许更改虚拟机动态库
      // library = default_library = kLibraryFallback = "libart.so"
      if (!is_debuggable()) {
        library = kLibraryFallback;
        default_library = kLibraryFallback;
      } else {
        // 如果是debug构建,需要判断传入的buffer参数是否为空
        // 如果不为空,default_library赋值为buffer
        if (buffer != NULL) {
          if (get_library_system_property(buffer) > 0) {
            default_library = buffer;
          } else {
            default_library = kLibraryFallback;
          }
        } else {
          default_library = kLibraryFallback;
        }
      }
    #else
      UNUSED(buffer);
      UNUSED(is_debuggable);
      UNUSED(get_library_system_property);
      const char* default_library = kLibraryFallback;
    #endif
      if (library == NULL) {
        library = default_library;
      }
    
      return library;
    }
    
    // "persist.sys.dalvik.vm.lib.2"是系统属性
    // 它的取值可以为libdvm.so或libart.so
    int GetLibrarySystemProperty(char* buffer) {
    #ifdef __ANDROID__
      return __system_property_get("persist.sys.dalvik.vm.lib.2", buffer);
    #else
      UNUSED(buffer);
      return 0;
    #endif
    }
    

    2. 常见内存分析工具

    2.1 Android Profiler

    Android Profiler引入于Android Studio 3.0,是用来替换之前的Android Monitor,主要用来观察内存(Memory)、网络(Network)、CPU使用状态的实时变化。这里我们主要介绍Android Profiler中的Memory Profiler组件,它对应于Android Monitor的Memory Monitor,通过Memory Profiler可以实时查看/捕获存储内存的使用状态强制GC以及跟踪内存分配情况,以便于快速地识别可能会导致应用卡顿、冻结甚至崩溃的内存泄漏和内存抖动。我们可以通过依次点击AS控制面板的View->Tool Windows->Profiler或者点击左下角的图标,进入Memory Profiler监控面板。

    在这里插入图片描述

    • 标注(1~6)说明

      1:用于强制执行垃圾回收事件的按钮;
      2:用于捕获堆转储的按钮,即Dump the Java heap;
      3:用于放大、缩小、复位时间轴的按钮;
      4 :用于实时播放内存分配情况的按钮;
      5:发生一些事件的记录(如Activity的跳转,事件的输入,屏幕的旋转);
      6:内存使用量事件轴,它包括以下内容:

      • 一个堆叠图表。显示每个内存类别当前使用多少内存,如左侧的y轴和顶部的彩色健所示。
        • Java:从Java或Kotlin代码分配的对象的内存(重点关注);
        • Native:从C或C++代码分配的对象的内存(重点关注);
        • Graphics:图像缓存等,包括GL surfaces, GL textures等;
        • Stack:栈内存(包括java和c/c++);
        • Code:用于处理代码和资源(如 dex 字节码.so 库和字体)分配的内存;
        • Other:系统都不知道是什么类型的内存,放在这里;
        • Allocated:从Java或Kotlin代码分配的对象数。
      • 一条虚线。虚线表示分配的对象数量,如右侧的y轴所示(5000/15000)。
      • 每个垃圾回收时间的图标。
    2.1.1 Allocation Tracker

    Allocation Tracker,即跟踪一段时间内存分配情况,Memory Profiler能够显示内存中的每个Java对象和JNI引用时如何分配的。我们需要关注如下信息:

    • 分配了哪些类型的对象,分配了多大的空间;
    • 对象分配的栈调用,是在哪个线程中调用的;
    • 对象的释放时间(只针对8.0+);

     如果是Android 8.0以上的设备,支持随时查看对象的分配情况,具体的步骤如下:在时间轴上拖动以选择要查看的哪个区域(时间段)的内存分配情况,如下图所示:
    在这里插入图片描述
     接下来,我们就以上一篇文章中所提及的单例模式引起的内存泄漏为例,来检查内存分配的记录,排查可能存在内存泄漏的对象。具体的步骤如下:

     (1) 浏览列表以查找堆计数异常大且可能存在泄漏的对象,即大对象。为了帮助查找已知类,可以点击下图中黄色方框的选项选择使用Arrange by classArrange by Package按类名或者包名进行分组,然后再红色方框中的第一个选项就会列出Class NamePackage Name,我们可以直接去查找目标类,也可以点击下图中的Filter 图标 img快速查找某个类,比如SingleInstanceActivity,当然我们还可以使用正则表达式Regex和大小写匹配Match Case。红色方框中其他选项意义:

    • Allocations:堆中动态分配对象个数;
    • Deallocations:解除分配的对象个数;
    • Total Counts:目前存在的对象总数;
    • Shallow Size:堆中所有对象的总大小(以字节为单位),不包含其引用的对象;

     (2) 当点击SingleInstanceActivity类时,会出现一个Instance View窗口,该窗口完整得记录了该对象在这一段时间内的分配情况,如下图所示,Instance View窗口中显示了7个SingleInstanceActivity对象,并记录了每个对象被分配(Alloc Time)、释放(Dealloc Time)的时间。但是当我们强制GC后,仍然还存在两个SingleInstanceActivity对象,根据平时的开发经验,其中的一个对象可能被某个对象持有,导致无法被释放从而造成泄漏。
    在这里插入图片描述
      (3) 如果我们希望确定(2)中无法被GC的对象被谁持有,可以点击该对象,此时在Instance View窗口的下方就会出现Allocation Call Stack标签,如上图蓝色方框所示,该标签中显示了该对象被分配到何处以及哪里线程中,此外,我们还可以在标签中右键点击任意行并选择Jump to Source,以在编辑器中打开该代码。

    2.1.2 Heap Dump

    Head Dump,即捕获堆转储,它的作用是捕获某一时刻应用中有哪些对象正在使用内存,并将这些信息存储到文件中。Head Dump可以帮助我们找到大对象和通过数据的变化发现内存泄漏,比如当我们的应用使用一段时候后,捕获了一个heap dump,这个heap dump里面发现了并不应该存在的对象分配情况,这说明是存在内存泄漏的。捕获堆转储后,可以查看以下信息:

    • 该时刻应用分配了哪些类型的对象,每种对象有多少;
    • 每个对象当前时刻使用了多少内存;
    • 对象所分配到的调用堆栈(Android 7.1以下会有所区别);
       要捕获堆转储,通过点击 Memory Profiler 工具栏中的 Dump Java heap图标 img实现,获得某一时刻的Heap Dump如下图:
      在这里插入图片描述
       接下来,我们仍然以上一篇文章中所提及的单例模式引起的内存泄漏为例,来分析Heap Dump所表达的信息。从下图展示内容可看出,Heap Dump表达的窗体与Allocation Tracker差不多,只是展示的具体内容不同。具体如下图所示:
      在这里插入图片描述
       下面我们解释下上图颜色方框中相关标签名表示的意义。

    (1) 红色方框

    • Allocations: 堆中分配对象的个数;
    • Native Size: 此对象类型使用的native内存总量。 此列仅适用于Android 7.0及更高版本。您将在这里看到一些用Java分配内存的对象,因为Android使用native内存来处理某些框架类,例如Bitmap。
    • Shallow Size: 此对象类型使用的Java内存总量;
    • Retained Size: 因此类的所有实例而保留的内存总大小;

    (2) 黄色方框

    • Depth:从任意 GC root 到所选实例的最短 hop 数。
    • Native Size: native内存中此实例的大小。此列仅适用于Android 7.0及更高版本。
    • Shallow Size:此实例Java内存的大小。
    • Retained Size:此实例支配[dominator]的内存大小(根据 [支配树]
    2.2 MAT

     在进行内存分析时,我们可以使用Android Profiler的Memory Profiler组件来观察、追踪内存的分配使用情况(Allocation Tracker),也可以通过这个工具找到疑似发生内存泄漏的位置(Heap Dump)。但是如果想要深入地进行分析并确定内存泄漏,就要分析疑似发生内存泄漏时所生产的堆转储文件,该文件由点击 Memory Profiler工具栏中的 Dump Java heap图标 img生成,输出的文件格式为hprof,分析工具使用的是MAT。由于Memory Profiler生成的hprof文件不是标准的hprof文件,需要使用SDK自带的hprof-conv进行转换,它的路径在sdk/platform-tools中,执行命令:hprof-conv E:\1.hprof E:\standar.hprof
    在这里插入图片描述
    MAT,全称"Memory Analysis Tool",是对内存进行详细分析的工具,它是eclipse的一个插件,对于AS开发来说,需要单独下载MAT(当前最新版本为1.9.1)。使用MAT打开一个标准的hprof文件如上图所示,选择Leak Suspects Report选项,MAT为hprof文件生成的报告,该报告为中给出了MAT认为可能出现内存泄漏问题的地方,除非内存泄漏特别明显,通过Leak Suspects还是很难发现内存泄漏的位置。因此,我们还是老老实实自己来动手分析,这里打开Overview标签(一般打开文件时会自动打开),具体如下图:
    在这里插入图片描述
     在上述图中,我们主要关注两个部分:饼状图和Actions,其中,饼状图主要用来显示内存的消耗,它的彩色部分表示被分配的内存,灰色部分则是空闲区域,单击每个彩色区域可以看到这块区域的详细信息;Actons一栏列出了4种Action,其作用与区别如下。

    • Historgram:列出每个类的所有对象。从类的角度进行分析,注重量的分析;
    • Dominator Tree:列出大对象和它们的引用关系。从对象的角度分析,注重引用关系分析;
    • Top Consumers:获取开销最大的对象,可通过类或包形式分组;
    • Duplicate Classes:检测出被多个类加载器加载的类;

     其中,分析内存泄漏最常用的就是HistogramDominator Tree。这两种方式只是判断内存问题的方式不同,但是最终都会归结到通过查看GC引用链来确定内存泄漏的具体位置(原因)。接下来,我们就以Dominator Tree为例来讲解如何使用MAT来判定是否有内存泄漏以及泄漏的具体原因。Dominator Tree,即支配树,点击Dominator Tree选项如下图所示,然后使用条件过滤(黄色方框输入),找一个我们认为可能发生了内存泄漏的类:
    在这里插入图片描述
     从上图可以看到,在Dominator Tree列出了很多SingleInstanceActivity的实例,而一般SingleInstanceActivity是不该有这么多实例的,因此,基本可以断定发生了内存泄漏,至于内存泄漏的具体原因,就需要查看GC引用链。但在查看之前,我们需要理解下红色方框几个标签的意义。

    • Shallow Heap

     对象自身占用的内存大小,不包括引用的对象。如果是数组类型的对象,它的大小由数组元素的类型和长度决定;如果是非数组类型的对象,它的大小由其成员变量的数量和类型决定。

    • Retained Heap

     Retained Heap就是当前对象被GC后,从Heap上总共能释放掉多大的内存空间,这部分内存空间被称之为Retained Set。Retained Set指的是这个对象本身和它持有引用的对象以及这些引用对象的Retained Set所占内存大小的总和。下面我们从一颗引用树(GC引用链)来理解下Retained Set:
    在这里插入图片描述
     假设A、B为GC Root对象,根据Retained Set定义可知,对象E的Retained Set为对象E、G,对象C的Retained Set为对象C、D、E、F、G、H。另外,通过引用树我们还可以演化得到本小节最重要的部分–支配树(Dominator Tree),即在引用树中如果一条到对象Y的路径一定(必然)会经过对象X,那么称为X支配Y,并且在所有支配Y的对象中,XY最近的一个对象就称为X直接支配Y,支配树就是反应的这种直接支配关系。在支配树中,父节点直接支配子节点。上图就是引用树转换为支配树的例子,由此可以得到:

      • 对象C直接支配对象D、E、H,故C是D、E、H的父节点;
      • 对象D直接支配对象F,故D是F的父节点;
      • 对象E直接支配对象G,故E是G的父节点;

     通过支配树,我们可以知道假如对象E被回收了,则会释放E、G的内存,而不会释放H的内存,因为F可能还会引用着H,只有C被回收了,H的内存才会被释放。因此,我们可以得到一个结论通过MAT的Dominator Tree,可以清晰地得到一个对象的直接支配的对象,如果直接支配对象中出现了不该有的对象,就说明发生了内存泄漏。示例如下:
    在这里插入图片描述
     从上图可知,被选中的SingleInstanceActivity对象的直接支配对象出现了不该有的CommonUtils对象,因为SingleInstanceActivity是要被回收的。换句话说,CommonUtils持有SingleInstanceActivity对象的引用,导致SingleInstanceActivity对象无法被正常回收,从而导致了内存泄漏。

    2.3 LeakCanary

     考虑到篇幅原因,请移步至《Android性能优化3:常见内存泄漏与优化(三)》。

     文章最后,我们借助Dalvik 虚拟机和 Sun JVM 在架构和执行方面有什么本质区别?一文中的一段话作个总结,个人觉得这对理解JVM/Dalvik/ART的本质比较有启发意义,即JVM其核心目的,是为了构建一个真正跨OS平台,跨指令集的程序运行环境(VM)。DVM的目的是为了将android OS的本地资源和环境,以一种统一的界面提供给应用程序开发。严格来说,DVM不是真正的VM,它只是开发的时候提供了VM的环境,并不是在运行的时候提供真正的VM容器。这也是为什么JVM必须设计成stack-based的原因。

    参考文献:

    展开全文
  • Linux内核中常见内存分配函数(一)

    千次阅读 2013-08-08 17:57:48
    Linux内核中常见内存分配函数(一)
    linux内核中采 用了一种同时适用于32位和64位系统的内存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系 统中,用到了四级页表。
      * 页全局目录(Page Global Directory)
      * 页上级目录(Page Upper Directory)
      * 页中间目录(Page Middle Directory)
      * 页表(Page Table)
      页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录的地址
    ,而页中间目录又包含若干页表的地址,每一个页表项指 向一个页框。linux中采用4KB大小的 页框作为标准的内存分配单元。
      多级分页目录结构
      伙伴系统算法
      在实际应用中,经常需要分配一组连续的页框,而频繁地申请和释放不同大小的连续页
    框,必然导致在已分配页框的内存块中分散了许多小块的 空闲页框。这样,即使这些页框是空闲的,其他需要分配连续页框的应用也很难得到满足。
      为了避免出现这种情况,linux内核中引入了伙伴系统算法(buddy system)。把所有的空
           闲页框分组为11个 块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块。最大可以申请1024个连续页框,对应4MB大小的连续内存。每个页框块的第一个页框的物理地址是该块大小的整数倍。
      假设要申请一个256个页框的块,先从256个页框的链表中查找空闲块,如果没有,就
    512个 页框的链表中找,找到了则将页框块分为2个256个 页框的块,一个分配给应用,另外一个移到256个页框的链表中。如果512个页框的链表中仍没有空闲块,继续向1024个页 框的链表查找,如果仍然没有,则返回错误。
      页框块在释放时,会主动将两个连续的页框块合并为一个较大的页框块。
      slab分 配器
      slab分配器源于 Solaris 2.4 的分配算法,工作于物理内存页框分配器之上,管理特
    定大小对象的缓存,进行快速而高效的内存分配。
      slab分配器为每种使用的内核对象建立单独的缓冲区。linux 内核已经采用了伙伴系统
    管理物理内存页框,因此 slab分配器直接工作于伙伴系统之上。每种缓冲区由多个 slab
    组成,每个 slab就是一组连续的物理内存页框,被划分成了固定数目的对象。根据对象大小的不同,缺省情况下一个slab 最多可以由 1024个页框构成。出于对齐等其它方面的要求,slab 中分配给对象的内存可能大于用户要求的对象实际大小,这会造成一定的内存浪费。
    展开全文
  • 因此,在概念上,我们可以将整个计算机内存看作时我们可以读和写的一个组成的大数组。 因为作为人类,我们不是很善于在   位   上进行思考和计算,所以,我们将位组成更大的组,它们可以一起用来表示数字...

    几周前,我们新开了一系列文章,旨在深入 JavaScript,探寻其工作原理。我们认为通过了解 JavaScript 的构建方式和其运行规则,我们能写出更好的代码和应用。

    第一篇文章重点介绍了引擎、运行时和调用栈的概述。第二篇文章仔细地分析了 Google's V8 JavaScript 引擎的内部部分并且为如何编写更好的 JavaScript 代码提供了一些建议。

    这是第三篇文章,我们将会讨论一个由于日常使用的编程语言日益成熟和复杂度提升从而让开发者忽略的话题——内存管理。我们还将提供一些有关如何处理 SessionStack 中 JavaScript 内存泄漏的建议,因为我们需要确保 SessionStack 不会导致内存泄漏,也不会增加我们集成的 Web 应用程序的内存消耗。

    概述

    像 C 语言,拥有底层原始的内存管理方法,例如:malloc()  free()。这些原始的方法被开发者用来从操作系统中分配内存和释放内存。

    然而,JavaScript 当一些东西(objects,strings,etc.)被创建的时候分配内存并且当它们不再被使用的时候“自动”释放它们,这个过程被称为垃圾回收。

    释放资源的这种看似“自动”的性质是造成困扰的根源。它给 JavaScript (和其它高级语言)开发者一个错误的印象——他们可以选择不关心内存管理。这是一个很大的错误。

    即使是使用高级语言,开发者对内存管理也应该有所了解(至少要有基础的了解)。有时,开发者必须理解自动内存管理会遇到问题(例如:垃圾回收中的错误或者性能问题等),以便能够正确处理它们。(或者是找到适当的解决方法,用最小的代价去解决。)

    内存的生命周期

    无论你使用那种语言,内存的生命周期基本是都差不多:

    image

    一下是生命周期中每一步发生了什么的一个概述:

    • Allocate memory —— 操作系统分配内存,允许你的程序使用它。在基础语言中(例如 C ),这是一个开发者自己处理的明确操作。然而,在高级语言中,它已经为你处理了。
    • Use memory —— 现在你就可以使用之前分配好的内存了。当你在代码中使用变量时,   的操作正在发生。
    • Release memory —— 现在该释放你不再需要的内存了,以便它们能够被再次使用。与分配内存的操作一样,这种操作在基础语言中是明确执行的。

    要快速了解调用栈和内存堆的概念,你可以阅读我们该主题的第一篇文章。

    什么时内存

    在直接讨论 JavaScript 中的内存之前,我们先简略地讨论下一般内存是什么以及它的工作原理。

    在硬件层面,计算机内存是有大量触发电路组成的。每个触发电路包含了一些晶体管并且能够存储一个位(bit)。单个触发器可通过唯一的标识符寻址,因此我们可以读取并重写它们。因此,在概念上,我们可以将整个计算机内存看作时我们可以读和写的一个由位组成的大数组。

    因为作为人类,我们不是很善于在  上进行思考和计算,所以,我们将位组成更大的组,它们可以一起用来表示数字。8位称为1字节(byte)。除了字节之外,还有字(有时是 16 位,有时是 32 位)。

    很多东西都存储在内存中:

    1. 所有程序使用的所有变量和其他数据。
    2. 程序的代码,包括操作系统的。

    编译器和操作系统共同合作,帮助你管理内存,但我们建议你查看一下引擎盖下的内容。

    编译代码时,编译器可以检查原始数据类型,并提前计算出需要多少内存。然后将所需的数量分配给调用堆栈中的程序。这些变量分配的空间称为堆栈空间,因为随着函数被调用,它们的内存被添加到现有存储器的顶部。当它们终止时,它们以 LIFO (last-in,first-out)顺序被移除。例如,考虑以下声明:

    int n; // 4 bytes
    int x[4]; // array of 4 elements, each 4 bytes
    double m; // 8 bytes

    编译器立即会看到这段代码需要:
    4 + 4x4 + 8 = 28 bytes。

    这是现在整数和双精度的工作原理。大约 20 年前,整数通常为 2 字节,双精度为 4 字节。你的代码应该永远不依赖于此时基本数据类型的大小。

    编译器将插入与操作系统交互的代码,以便在堆栈中请求要存储的变量所需的字节数。

    在上面的示例中,编译器知道每个变量的精确内存地址。事实上,每当我们写入变量 n 时,在系统内部都会被翻译为“内存地址 4127963” 这样的东西。

    注意,如果我们这里尝试访问 x[4] ,我们可能会访问到和 m 相关联的数据。这是因为我们访问的元素在数组中并不存在——这 4 个字节比数组中最后一个元素 x[3] 还要远,可能会读取(或重写) m 的位。这肯定会对程序产生难以理解的不良影响。

    image

    当函数调用其他函数时,每个函数在调用时都会获得自己的堆栈块。它保存所有的局部变量,还有一个程序计数器,可以记住函数在执行中的位置。当函数执行完成时,其内存块可以再次用于其他目的。

    动态分配

    不幸的是,当我们在编译一个不知道多少内存的变量时,事情就变的不那么容易了。假设我们要做如下的事情:

    int n = readInput(); // reads input from the user
    
    ...
    
    // create an array with "n" elements

    这样,在编译的时候,编译器不知道数组需要多少内存,因为它依靠用户提供的输入。

    因此,它不能为堆栈上的变量分配空间。相反,我们的程序需要在运行时明确地要求操作系统获得适当的空间量。这个内存时从堆空间分配的。静态和动态内存分配的区别如下表所示:

    Static allocation Dynamic allocation
    编译时内存大小确定 编译时内存大小不确定
    编译阶段执行 运行时执行
    分配给栈 分配给堆
    FILO 没有特定的顺序

    为了充分了解动态内存分配的工作原理,我们需要花更多的时间在指针上,这可能与这篇文章的主题有些偏离了。如果你有兴趣了解的话,可以在评论中通知我们,我们可以在未来的文章中详细介绍指针。

    JavaScript 中的分配

    现在,我们将要解释 JavaScript 中第一步(分配内存)是如何工作的。

    JavaScript 解放了开发者处理内存分配的责任——JavaScript 自己在声明 values 时就做了这件事。

    var n = 374; // allocates memory for a number
    var s = 'sessionstack'; // allocates memory for a string 
    var o = {
      a: 1,
      b: null
    }; // allocates memory for an object and its contained values
    var a = [1, null, 'str'];  // (like object) allocates memory for the
                               // array and its contained values
    function f(a) {
      return a + 3;
    } // allocates a function (which is a callable object)
    
    // function expressions also allocate an object
    someElement.addEventListener('click', function() {
      someElement.style.backgroundColor = 'blue';
    }, false);

    一些函数调用也会导致对象分配:

    var d = new Date(); // allocates a Date object
    var e = document.createElement('div'); // allocates a DOM element

    方法可以分配新值或对象:

    var s1 = 'sessionstack';
    var s2 = s1.substr(0, 3); // s2 is a new string
    // Since strings are immutable, 
    // JavaScript may decide to not allocate memory, 
    // but just store the [0, 3] range.
    var a1 = ['str1', 'str2'];
    var a2 = ['str3', 'str4'];
    var a3 = a1.concat(a2); 
    // new array with 4 elements being
    // the concatenation of a1 and a2 elements

    JavaScript 中使用内存

    基本上在 JavaScript 中使用内存的意思就是在内存在进行   

    这个操作可能是一个变量值的读取或写入,一个对象属性的读取或写入,甚至时向函数中传递参数。

    当内存不再需要时释放它

    大多数的内存管理问题发生在这个阶段。

    这里最困难的任务就是确定内存何时就不再被需要了。它通常需要开发人员确定程序中哪里不再需要这样的内存,并释放它。

    高级语言拥有垃圾回收器,它的职责就是追踪内存分配和使用情况,找到不再被使用的内存,然后自动地释放它。

    不幸的是,这个过程只能得到一个近视的值,因为内存是否被需要是不可判定的(不能用算法求解)。

    大多数垃圾回收器通过判断内存是否能够被再次访问来工作的,例如:指向它的所有变量都超出了作用域。然而,这只能得到一个近似值。因为在任何位置,存储器位置可能仍然具有指向其范围的变量,但是它可能将永远不会被再次访问了。

    垃圾回收

    由于事实上发现内存“不再被需要”是不可判定的,因此垃圾收集的通常解决方案都存在局限性。本节将介绍理解主要垃圾收集算法及其局限性的必要概念。

    内存引用

    垃圾回收算法依靠的主要概念就是引用

    在内存管理的上下文中,如果前者具有对后者的访问权限(可以是隐式的或者显式的),则一个对象被成为引用另一个对象。例如:JavaScript 对象具有对其原型(隐式引用)及其属性值(显式引用)的引用。

    在上下文中,“对象”的定义扩展到比常规 JavaScript 对象更广泛的东西,并且还包含了函数的作用域(或者全局的词法作用域)。

    词法作用域定义了如何在嵌套函数中解析变量名称:即使父函数已返回,内部函数也可以包含父函数的范围。

    引用计数——垃圾回收

    这是最简单的垃圾回收算法。如果一个对象指向它的引用数为 0,那么它就应该被“垃圾回收”了。

    看一下下面的代码:

    var o1 = {
      o2: {
        x: 1
      }
    };
    // 2 objects are created. 
    // 'o2' is referenced by 'o1' object as one of its properties.
    // None can be garbage-collected
    
    var o3 = o1; // the 'o3' variable is the second thing that 
                // has a reference to the object pointed by 'o1'. 
                                                           
    o1 = 1;      // now, the object that was originally in 'o1' has a         
                // single reference, embodied by the 'o3' variable
    
    var o4 = o3.o2; // reference to 'o2' property of the object.
                    // This object has now 2 references: one as
                    // a property. 
                    // The other as the 'o4' variable
    
    o3 = '374'; // The object that was originally in 'o1' has now zero
                // references to it. 
                // It can be garbage-collected.
                // However, what was its 'o2' property is still
                // referenced by the 'o4' variable, so it cannot be
                // freed.
    
    o4 = null; // what was the 'o2' property of the object originally in
               // 'o1' has zero references to it. 
               // It can be garbage collected.

    循环依赖造成的问题

    出现循环依赖就会产生限制。在以下的示例中,将创建两个对象并引用彼此,从而创建了一个循环。在函数调用之后,它们离开了作用域,因此它们实际上已经无用了,可以被释放了。然而,引用计数算法认为,由于两个对象中的每一个至少被引用了一次,所以也不能被垃圾回收。

    function f() {
      var o1 = {};
      var o2 = {};
      o1.p = o2; // o1 references o2
      o2.p = o1; // o2 references o1. This creates a cycle.
    }
    
    f();

    image

    标记扫描算法

    为了确定一个对象是否被需要,这个算法会确定对象是否可以访问。

    该算法由以下步骤组成:

    1. 垃圾回收器构建“roots”列表。Roots 通常是代码中保留引用的全局变量。在 JavaScript 中,“window” 对象可以作为 root 全局变量示例。
    2. 所有的 roots 被检查并标记为 active(即不是垃圾)。所有的 children 也被递归检查。从 root 能够到达的一切都不被认为是垃圾。
    3. 所有为被标记为 active 的内存可以被认为是垃圾了。收集器限制可以释放这些内存并将其返回到操作系统。

    image

    这个算法优于前一个,因为“一个对象零引用”会让这个对象不是可达的。反过来就不一定对了,因为存在循环引用。

    截至 2012 年,所有现代浏览器都配备了 mark-and-sweep 机制的垃圾回收器。过去几年,JavaScript 垃圾回收(代数、增量、并行、并行垃圾收集)领域的所有改进都是对该算法(mark-and-sweep)的实现进行改进,但并没有对垃圾回收算法本身进行改进,其目标是确定一个对象是否可达。

    在本文中,你可以阅读到更多关于垃圾回收的详细信息,当然也包含对其的优化。

    循环引用不再是问题

    在上面的第一个例子中,在函数调用后,两个对象不再被从全局对象可访问的东西所引用。因此,垃圾回收器将发现它们是不可达的。

    image

    尽管两个对象还是存在引用,但是他们从 root 出发已经是不可达的了。

    反垃圾收集器的直接行为

    虽然垃圾收集器很方便,但他们也有自己的一系列决策。其中一个是非确定论。换句话说,GCs 是不可预测的。你不能真正的知道回收是什么时候执行的。这意味着在某些情况下,程序使用的内存要比实际需要的还多。在另外一些情况下,如果程序特别敏感,那么一些短暂的暂停会显得特别明显。虽然不确定性意味着回收执行的时间不能被确定,但是大多数 GCs 的实现是共享模式——在分配内存期间执行回收遍历。如果没有分配执行,大多数 GCs 保持空闲状态。考虑一下情况:

    1. 相当大的一组分配被执行。
    2. 大多数元素(或全部)被标记为不可达(假设我们将指向不再需要的缓存的引用置空)。
    3. 不再进行分配。

    在这种情况下,大多数 GC 将不会再运行任何进一步的收集。换句话说,即使有不可达的引用变量可以被收集,但是收集器并没有被声明。这些不是严格的内存泄漏,但仍然会导致使用内存比通常的内存要高。

    什么是内存泄漏

    实质上,内存泄漏可以被定义为应用程序不再需要的内存,但由于某种原因,内存不会返回到操作系统或可用内存池中。

    编程语言支持多种管理内存的方法。然而,某块内存是否被使用实际上是一个不确定的问题。换句话说,只有开发人员可以清楚一块内存是否可以释放到操作系统又或者不该被释放。

    某些编程语言提供了一些特性,帮助开发者处理这些事情。另外一些语言期望开发者能够完全自己去明确地控制内存。维基百科有关于手动和自动内存管理的好文章。

    四种常见的 JavaScript 内存泄漏

    1:Global variables

    JavaScript 以有趣的方式处理未声明的变量:对未声明的变量的引用在全局对象内创建一个新变量。在浏览器中,全局对象就是 window。换种说法:

    function foo(arg) {
        bar = "some text";
    }

    等价于:

    function foo(arg) {
        window.bar = "some text";
    }

    如果 bar 被假定为仅仅在函数 foo 的作用域范围内持有对变量的引用,但是你却忘记了使用 var 来声明它,那么就会创建一个意外的全局变量。

    在这个例子中,泄漏一个简单的字符串不会有太大的伤害,但肯定会变得更糟的。

    可以通过另一种方式创建意外的全局变量:

    function foo() {
        this.var1 = "potential accidental global";
    }
    // Foo called on its own, this points to the global object (window)
    // rather than being undefined.
    foo();

    为了防止这些错误的发生,可以在 JavaScript 文件开头添加 “use strict”,使用严格模式。这样在严格模式下解析 JavaScript 可以防止意外的全局变量。

    即使我们讨论了如何预防意外全局变量的产生,但是仍然会有很多代码用显示的方式去使用全局变量。这些全局变量是无法进行垃圾回收的(除非将它们赋值为 null 或重新进行分配)。特别是用来临时存储和处理大量信息的全局变量非常值得关注。如果你必须使用全局变量来存储大量数据,那么,请确保在使用完之后,对其赋值为 null 或者重新分配。

    2:被忘记的 Timers 或者 callbacks

    在 JavaScript 中使用 setInterval 非常常见。

    大多数库都会提供观察者或者其它工具来处理回调函数,在他们自己的实例变为不可达时,会让回调函数也变为不可达的。对于 setInterval,下面这样的代码是非常常见的:

    var serverData = loadData();
    setInterval(function() {
        var renderer = document.getElementById('renderer');
        if(renderer) {
            renderer.innerHTML = JSON.stringify(serverData);
        }
    }, 5000); //This will be executed every ~5 seconds.

    这个例子阐述着 timers 可能发生的情况:计时器会引用不再需要的节点或数据。

    renderer 可能在将来会被移除,使得 interval 内的整个块都不再被需要。但是,interval handler 因为 interval 的存活,所以无法被回收(需要停止 interval,才能回收)。如果 interval handler 无法被回收,则它的依赖也不能被回收。这意味着 serverData——可能存储了大量数据,也不能被回收。在观察者模式下,重要的是在他们不再被需要的时候显式地去删除它们(或者让相关对象变为不可达)。

    过去,特别是某些浏览器(IE6)无法管理循环引用。如今,大多数浏览器会在被观察的对象不可达时对 observer handlers 进行回收,即使 listener 没有被显式的移除。但是,明确地删除这些 observers 仍然是一个很好的做法。例如:

    var element = document.getElementById('launch-button');
    var counter = 0;
    function onClick(event) {
       counter++;
       element.innerHtml = 'text ' + counter;
    }
    element.addEventListener('click', onClick);
    // Do stuff
    element.removeEventListener('click', onClick);
    element.parentNode.removeChild(element);
    // Now when element goes out of scope,
    // both element and onClick will be collected even in old browsers // that don't handle cycles well.

    如今,现代浏览器(包括 IE 和 Edge)都使用的是现代垃圾回收算法,可以检测这些循环依赖并正确的处理它们。换句话说,让一个节点不可达,可以不必而在调用 removeEventListener。

    框架和库,例如 jQuery ,在处理掉节点之前会删除 listeners (使用它们特定的 API)。这些由库的内部进了处理,确保泄漏不会发生。即使是在有问题的浏览器下运行,如。。。。IE6。

    3:闭包

    JavaScript 开发的一个关键方面就是闭包:一个可以访问外部(封闭)函数变量的内部函数。由于 JavaScript 运行时的实现细节,可以通过以下方式泄漏内存:

    var theThing = null;
    var replaceThing = function () {
      var originalThing = theThing;
      var unused = function () {
        if (originalThing) // a reference to 'originalThing'
          console.log("hi");
      };
      theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
          console.log("message");
        }
      };
    };
    setInterval(replaceThing, 1000);

    这个代码片段做了一件事:每次调用 replaceThing 时,theThing 都会获得一个新对象,它包含一个大的数组和一个新的闭包(someMethod)。同时,变量 unused 保留了一个拥有 originalThing 引用的闭包(前一次调用 theThing 赋值给了 originalThing)。已经有点混乱了吗?重要的是,一旦一个作用域被创建为闭包,那么它的父作用域将被共享

    在这个例子中,创建闭包 someMethod 的作用域是于 unused 共享的。unused 拥有 originalThing 的引用。尽管 unused 从来都没有使用,但是 someMethod 能够通过 theThing 在 replaceThing 之外的作用域使用(例如全局范围)。并且由于 someMethod 和 unused 共享 闭包范围,unused 的引用将强制保持 originalThing 处于活动状态(两个闭包之间共享整个作用域)。这样防止了垃圾回收。
    当这段代码重复执行时,可以观察到内存使用量的稳定增长。当 GC 运行时,也没有变小。实质上,引擎创建了一个闭包的链接列表(root 就是变量 theThing),并且这些闭包的作用域中每一个都有对大数组的间接引用,导致了相当大的内存泄漏,如下图:

    image

    这个问题由 Meteor 团队发现的,他们有一篇伟大的文章,详细描述了这个问题。

    4:DOM 引用

    有时候,在数据结构中存储 DOM 结构是有用的。假设要快速更新表中的几行内容。将每行 DOM 的引用存储在字典或数组中可能是有意义的。当这种情况发生时,就会保留同一 DOM 元素的两份引用:一个在 DOM 树种,另一个在字典中。如果将来某个时候你决定要删除这些行,则需要让两个引用都不可达。

    var elements = {
        button: document.getElementById('button'),
        image: document.getElementById('image')
    };
    function doStuff() {
        elements.image.src = 'http://example.com/image_name.png';
    }
    function removeImage() {
        // The image is a direct child of the body element.
        document.body.removeChild(document.getElementById('image'));
        // At this point, we still have a reference to #button in the
        //global elements object. In other words, the button element is
        //still in memory and cannot be collected by the GC.
    }

    还有一个额外的考虑,当涉及 DOM 树内部或叶子节点的引用时,必须考虑这一点。假设你在 JavaScript 代码中保留了对 table 特定单元格(<td>)的引用。有一天,你决定从 DOM 中删除该 table,但扔保留着对该单元格的引用。直观地来看,可以假设 GC 将收集除了该单元格之外所有的内容。实际上,这不会发生的:该单元格是该 table 的子节点,并且 children 保持着对它们 parents 的引用。也就是说,在 JavaScript 代码中对单元格的引用会导致整个表都保留在内存中的。保留 DOM 元素的引用时,需要仔细考虑。

    ==============================

    原文地址:传送门
    作者:Alexander Zlatkov

    展开全文
  • 原文:How JavaScript works: memory management + how...【译者注】本文介绍了JavaScript在内存管理方面的工作原理,同时列举了4种常见内存泄漏和处理方式。以下为译文:几个星期前,我们开始编写深入研究JavaScr...
  • 常见C++内存池技术

    千次阅读 2014-04-15 20:05:57
    总结下常见的C++内存池,以备以后查询。 应该说没有一个内存池适合所有的情况, 根据不同的需求选择正确的内存池才是正道. (1)最简单的固定大小缓冲池  适用于频繁分配和释放固定大小对象的情况, 关于这...
  • 内存动态管理视图(DMV): 从sys.dm_os_memory_clerks开始。 SELECT [type] , SUM(virtual_memory_reserved_kb) AS [VM Reserved] , SUM(virtual_memory_committed_kb) AS [VM Committed] , SUM(awe_al
  • Android性能优化(2):常见内存泄漏与优化 作者:无名之辈FTER Dalvik是Google特别设计专门用于Android平台的虚拟机,它位于Android系统架构的Android的运行环境(Android Runtime)中,是Android移动设备平台的核心...
  • 嵌入式linux内核中常见内存分配

    千次阅读 2013-04-16 13:25:22
     slab分配器为每种使用的内核对象建立单独的缓冲区Linux 内核已经采用了伙伴系统管理物理内存页框,因此 slab分配器直接工作于伙伴系 统之上每种缓冲区多个 slab 组成,每个 slab就是一组连续的物理内存页框,被...
  • OC内存管理常见面试题整理

    千次阅读 2016-03-31 19:06:57
    1:简述OC中内存管理机制。与retain配对使用的方法是dealloc还是release,为什么?需要与alloc配对使用的方法是dealloc还是release,为什么?readwrite,readonly,assign,retain,copy,nonatomic,atomic,strong,weak...
  • JavaScript 中 4 种常见内存泄露陷阱

    千次阅读 2017-08-24 21:44:50
    在这篇文章中我们将要探索客户端 JavaScript 代码中常见的一些内存泄漏的情况,并且学习如何使用 Chrome 的开发工具来发现他们。读一读吧!介绍内存泄露是每个开发者最终都不得不面对的问题。即便使用自动内存管理的...
  • CPU中常见寄存器及与内存的交互

    千次阅读 2020-04-20 22:27:40
    对于每个CPU,其都有一套自己可以执行的专门的指令集(这部分指令CPU提供)。 正式因为不同CPU架构的指令集不同,使得X86处理器不能执行ARM程序,ARM程序也不能执行X86程序(Inter和AMD都使用x...
  • 该算法能够判断出某个对象是否可以访问,从而知道该对象是否有用,该算法以下步骤组成: 垃圾收集器构建一个“根”列表,用于保存引用的全局变量。在JavaScript中,“window”对象是一个可作为根节点的全局变量。 ...
  • 内存泄漏是指 一些对象我们不在...它两部分构成:函数,以及创建该函数的环境。环境闭包创建时在作用域中的任何局部变量组成。 这种官方的概念是比较难理解的,在面试的时候说出来也不是很专业,因为没办法有...
  • 常见的JAVA内存泄漏及解决办法

    千次阅读 2009-03-19 23:53:00
    这篇文章就是要介绍一些常见的缺陷,然后提供一些非常好的实践例子来指导你写出没有内存泄漏的代码。一旦你的程序存在内存泄漏,要查明代码中引起泄漏的原因是很困难的。同时这篇文章也要介绍一个新的工具来查找内存...
  • 什么内存BANK

    千次阅读 2016-03-18 10:29:09
    其实这种观念是不对的,内存的Bank(指物理Bank)数和内存颗粒的面无关,它们之间有什么联系呢? 要讲清这个问题,就要提到内存的逻辑Bank,下面就给大家介绍一下物理Bank和逻辑Bank的概念。在介绍之前
  • 本文已经收录自笔者开源的 JavaGuide: https://github.com/Snailclimb (【Java学习 面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识)如果...写在前面 (常见面试题) 基本问题 介绍下 Java 内存区域(运...
  • 常见的Linux内核中内存分配

    千次阅读 2018-08-07 21:56:09
    每种缓冲区多个 slab 组成,每个 slab就是一组连续的物理内存页框,被划分成了固定数目的对象 根据对象大小的不同,缺省情况下一个 slab 最多可以 1024个页框构成 出于对齐 等其它方面的要求,slab 中分配给对象...
  • 先初步介绍一下内存组成: java进程占用内存 约等于 Java永久代 + Java堆(新生代和老年代) + 线程栈+ Java NIO,其它部分占用内存较小, 详细可以参考这篇文章 ...
  • C++中避免内存泄露常见的解决方案

    千次阅读 2012-07-27 09:59:10
    常见内存泄露及解决方案-选自ood启示录 new/delete, array new/arrray delete匹配 case 1: 在类的构造函数与析构函数中没有匹配地调用 new/delete!  解决方法:检查构造函数,在出现new的情况下,按相反的顺序...
  • 对C/C++程序员来说,内存管理是个不小的挑战,绝对值得慎之又慎,否则让上万行代码构成的模块跑起来后才出现内存崩溃,是很让人痛苦的。因为崩溃的位置在时间和空间上,通常是在距真正的错误源一段距离之后才表现...
  • 这是普通程序员心目中的内存印象,一个个的字节组成,而CPU并不是这么看待的。   图二: CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的...
  • DDRMC 系列总结(2)zz常见硬件术语之内存术语解释标签 : DDRMC 内存相关术语 内存模块 (Memory Module):提到内存模块是指一个印刷电路板表面上有镶嵌数个记忆体芯片chips,而这内存芯片通常是DRAM芯片,但近来...
  • 这个问题,我们需要知道 GC 在什么时候回收内存对象,什么样的内存对象会被 GC 认为是“不再使用”的。 Java中对内存对象的访问,使用的是引用的方式。在 Java 代码中我们维护一个内存对象的引用变量,通过这个引用...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 264,582
精华内容 105,832
关键字:

常见的内存由什么构成