物理内存&虚拟内存
根据我们的操作系统常识,我们知道,内存分为物理内存和虚拟内存。
物理内存就是我们实际的硬件设备。
而正如国家会超发货币一样,一般我们计算机也会超发内存。比如实际只有32位的存储空间,但是我们会超发比如到40位,而这就是虚拟内存。当然,正如国家超发货币有各种机制兜底以外,我们超发的内存也并非无中生有,超发的这部分内存其实是来自于磁盘。
操作系统会把判定为最没有价值待在内存的数据放到磁盘的这部分空间里,这就是内存交换。
一言以蔽之,虚拟内存是逻辑内存,以为是一整块连续内存,实际上物理内存不一定连续,也不一定是内存。
而访问内存是一个很慢的过程(磁盘就更慢了),为了解决这个问题,CPU会用多级缓存(速度快的贵,多级来平衡性价比)。甚至不惜将芯片大部分空间分给Cache。
手机端和PC端的差异
这当然很合理,但是当来到手机端的时候,芯片的大小就成了问题,本来就没多大,真没空间给你Cache,所以手机端对于这块的优化处理是阉割的状态。
- 既然Cache少,那Cache的命中率就会低
- 手机端因为类似sd卡可读写次数较少,所以不会做内存交换的工作。
- 值得一提的是,IOS会把不用的内存压缩一下腾腾空间,不过Android不会
自从“黑暗降临”(笑)现在大部分的项目会运行在三个端上,IOS,Android,PC。少部分还会兼容鸿蒙和小游戏,所以这里还会简单谈一谈Android端和IOS端的内存管理。鸿蒙是真的不熟,小游戏的话,微信了解一点,抖音小游戏,华为小游戏之类的就更加不熟了。
Android内存管理
Java基础
谈Android的内存,就不能不提Java的内存管理。
我们知道,Java是一种解释型语言。编译型语言需要先将程序编译为机器代码,之后再执行。而解释型语言会在执行过程中动态的把语句逐句解释(Interpret)为机器代码(或机器代码的子程序),之后再执行。解释型语言的出现解决了编译型语言的几个问题,首先应用不需要以机器码的形式存在,因为机器码的程序是不好跨平台的,其次堆空间无法自动GC,这对于操作系统是不优雅的。于是应用得以利用统一的JavaByte实现跨平台。
当然,有得必有失。Java也有不少做不到的事情,必须要需要调用C/C++来解决。为此,Android封装了一个C/C++的底层库。(后文会用到这个知识)将虚拟机的部分和底层库分开管理。调用底层库的方法,用的是本地方法栈(Native Stack),调用Java的是Java栈(Java Stack)。
Android的内存管理
Andriod 是基于Linux的操作系统,最基本的管理单位是一个Page
- 默认情况下,一个Page是4K
- 回收和分配以page为单位
- 区分用户态和内核态(内核态的内存用户态是不可以访问的)
谈Android的内存,就不能不提Java的内存管理。Java程序在运行的过程中会将其管理的内存分为若干个不同的数据区:
- 方法区:方法区存放的是类信息、常量、静态变量,所有线程共享区域。
- 虚拟机栈(Java栈):方法栈,为虚拟机执行Java方法服务。
- 本地方法栈:方法栈,为虚拟机使用到的Native方法服务。
- 堆:JVM管理的内存中最大的一块,所有线程共享;用来存放对象实例,几乎所有的对象实例都在堆上分配内存;此区域也是垃圾回收器(Garbage Collection)主要的作用区域,内存泄漏就发生在这个区域。
- 程序计数器:可看做是当前线程所执行的字节码的行号指示器;如果线程在执行Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的是Native方法,这个计数器的值为空(Undefined)。
看了上文,不熟悉的朋友可能看到方法栈这个名字有点懵,所以以防万一,我们来解释一下什么是方法栈。
Android方法栈运行机制
栈里面存储的数据叫栈帧,它与方法一一对应,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,线程私有区域。栈帧随着方法调用而创建,随方法运行结束而出栈销毁。当方法A调用方法B时,就会为方法B创建栈帧,放在栈顶,把方法A的栈帧压入栈中,当方法B运行完毕就会把B的栈帧从栈顶出栈销毁,继续运行方法A。栈帧中存储着方法的入参、局部变量(基本数据类型直接存储;对象存储在堆中,栈中存对象引用)、运算过程值(过程值中, 也是基本类型存在栈帧,过程对象存堆中, 引用在栈中)、返回值等。
上文有提到,Android采用弹性内存分配机制。一开始每个App都会拥有一个小额的内存,后续若有需要可以申请扩容。Java栈和本地方法栈都可以独立的去动态扩容。如果一个线程在创建和动态扩容的时候无法为栈申请到足够的内存(家里没余粮了),JVM就会抛出Out Of Memory(OOM)错误。如果一个线程请求分配的栈容量超过上限(吃太多了),JVM就会抛出StackOverflow(SOF)错误。
Android应用的内存
Android应用是跑在Android虚拟机里,它对jvm进行了兼容。当启动一个Android程序的时候,会启动一个dalvik vm进程,系统会为它分配固定的虚拟内存空间(物理内存在RAM)。Android采用弹性内存分配机制,一开始每个App都会拥有一个小额的内存,后续可以根据需求去动态申请增加。
Android上,一个进程会对应多个线程。操作系统基础知识告诉我们,每个线程被创建,都会被分配一揽子配套设施。而Android上每个线程被创建的时候都会被JVM分配一个Java栈(Java Stack),本地方法栈(Native Stack)和线程PC寄存器。简单来说,线程PC寄存器是个记录器,会记录线程的运行位置等运行数据。Java栈和本地方法栈都是分配的内存空间。分别对应上文提到的Java方法和本地方法。
Android内存清理
在系统内存减少到达一个阈值时,系统就会开始根据进程的优先级来进行回收机制杀掉一部分进程来释放出内存供后面需要启动的App使用。这套回收内存的机制就叫内存杀手----Low Memory Killer。Linux内核分配给每个系统进程的一个值,叫做oom_adj值。值越大,进程的优先级越低,越容易被回收,普通应用进程的oom_adj值是>=0,而系统进程的oom_adj值是<0的。内存杀手会以oom_adj为第一关键词从大到小,以占用内存大小为第二关键词从大到小排序,决定杀谁。
下图是一些层级划分,从上往下,层级的oom_adj越来越大。杀到System层的时候手机就重启了
Android内存回收
我们毕竟不是Android开发,所以这里不会讲的特别细。主要是罗列一下基本知识和要点。
- Native内存:因为是底层库,分配和回收都是手动进行。
- Java内存:因为Java采用的分代GC,分成新生代和老生代。
- 新生代(标记—复制算法):比较频繁快速,小垃圾回收(Minor GC)。若认为会存在较长时间,那么就会移动到老生代。
- 老生代(标记—整理算法):无法再分配空间的时候,会触发Full GC,即对新生代老生代全部GC。
- Full GC:会触发stop the world (暂停所有线程的),频繁的Full GC会导致应用性能大大下降。
Android内存泄露
Android 采用可达性分析法来确定哪些对象应该存活,具体了来说就是在触发GC时候,只要直接或者间接被GC ROOT(①虚拟机栈中的引用的对象;②仍处于存活状态中的线程对象;③方法区中静态引用指向的对象;④Native 方法中 JNI 引用的对象;)引用,就应该存活(其他对象则会被回收)。 一个对象生命周期应该结束,却依旧处在某一种GC ROOT的引用链上,我们就说就出现了内存泄露。
Android内存指标
下面简单介绍下三种常用安卓内存指标,RSS,PSS,USS。这三者的差异主要在于使用了多少公共服务的Page内存。
- RSS(Resident Set Size):是当前App所应用掉的所有内存,如下Rss图app调用了Google Play Services的4个page内存,在RSS统计的使用这部分内存会统计到App身上
- PSS(Proportional Set Size):App 按比例统计,比如PSS图所示两个进程共享,那就负责一半page。如果三个进程共享,那就负责三分之一
- USS(Unique Set Size):只有统计自己使用的内存,公共使用的内存不算进来
-图1 RSS
图2 PSS
图2 PSS
IOS内存管理
按道理应该也来介绍一下IOS内存管理,但是奈何本人不会,只能双手一摊。事已至此,只能拜托以后的我了。