转发Android内存管理机制
这篇文章讲解的很详细,我进行了简单总结。
Java内存分配
线程私有:程序计数器,虚拟机栈,本地方法栈
线程共享:堆,方法区
1.程序计数器 :当前线程所指向的字节码指示器,Java方法存放虚拟机字节码指令地址,Native方法,计数器值为Undefined,唯一不存在OOM
2.虚拟机栈:当线程每个方法执行时都创建一个栈帧用来存放局部变量,方法出口等,并将该栈帧放在JVM栈中。如果栈深度大于虚拟机允许最大深度,抛出StackOverFlowError,不过虚拟机基本都允许动态扩展虚拟机栈大小,这样一直申请会抛出OOM
栈帧主要包含如下:
局部变量表:地址的引用,32位存储空间(约4G)
操作数栈:栈的操作
动态链接:方法多态,JS运行时编译
返回地址:return
3.本地方法栈:存放的栈帧是Native方法调用时产生的
4.堆:对象存储地方,GC重要区域
5.方法区:静态区,用于存储类信息、常量池、静态变量、即时编译期编译后的代码等。方法区可以选择是否开启垃圾回收。JVM内存不足会抛出OOM。有永久代时,方法区被用作有永久代
JAVA内存回收算法
1. 标记-清除算法
标记所有需要回收对象,标记后统一回收。
效率不高,会产生大量不连续内存碎片
2.复制算法
内存划分两块,一块内存用完复制到另一个,清理另一块内存。
只标记2分之一所以简单高效,但是浪费一半空间,存活对象越少,效率越高
3.标记-整理算法
标记需回收内存,存活对象往一端移动,清理剩余内存。
避免内存碎片和空间浪费,比较合理
4.分代回收算法
安卓选取该算法,分为新生代,老年代,永久代(JDK1.8后取消 变为元空间)
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存
新生代:回收频率高,采用标记-复制,mina GC ,eden和To Survior,From Survior 三个区 8:1:1 分配,执行GC时候,eden区存活对象和FromSurvior年轻对象复制到To Survior区,达到分代年龄次数(Mina GC次数用于晋升老年代),或者To Survior达到阈值复制到老年代,然后清空Eden和From Survior,From和To交换空间,保障每次To Survior都是空的
老年代:相对稳定但存活对象多,Full GC ,采用标记-整理
永久代:存放静态类和方法,GC对这没有显著影响,对应方法区
分配原则:对象优先在Eden分配,大对象直接进去老年代,长期存活对象进入老年代,动态对象年龄判定(如果Survior区域满了,或者满足年龄,提前越级进入老年代),空间分配担保(老年代满了进行Full GC)
三级模型中,每个区域都有大小固定的值,当达到某一级内存区域阈值就触发GC
Android系统为每个应用设置了硬性Dalvik Heap Size最大限制阈值,RAM不同阈值设定不同。只有allocated + 新分配内存 >= getMemoryClass 发生OOM。
ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
int value = am.getMemoryClass();
5.JVM对象内存分配
对象优先在Eden分配
GC机制原理
一个线程可以理解为一个根节点,里面是有向图的树,根顶点可到达的对象都是有效对象,反之就说明没有被 引用可以GC,Java使用有向图管理可以消除循环问题,精度高,效率低。另一种常用内存管理技术使用计数器,例如COM模型采用计数器方式,精度低,效率高。
JVM提供GC策略也很多,有平缓执行GC,有中断执行GC,定时执行GC,或者内存到达一定程度GC
Android 进程类型
1.前台进程(正常不会被杀死)
与用户交互Activity,Service绑定正在交互Activity,onReceive方法的BroadcastReceiver,正在执行生命周期的Service
2.可见进程(正常不会被杀死)
前台仍可见的Activity onPause方法,比如弹框时
Service.startForeground启动前台Service ,或者特定Service,动态壁纸
3.服务进程(正常不会杀死)
启动的startService ,例如网络上传,下载数据,除非内存不足一般不会杀死
4.后台进程(随时杀死)
这类进程持有一个多个不可见Activity,可以随时回收,但例如Activity onSaveInstance即使杀死也能正常重启
5.空进程(随时杀死)
不含活动应用组件进程,保留这种进程的的唯一目的是用作缓存,缩短下次运行组件启动时间,
进程销毁策略(ADJ)
Linux内核算法采用打分(oom_score),oom_score_adj -1000到1000 ,数值越大级别越低,越容易杀死。普通app进程 >=0,系统的才可能小于0
那么系统什么时候更新 oom_score_adj ?
启动一个Activity或Activity前台转后台,状态变化时更新。
AMS有两个方法更新:
final boolean updateOomAdjLocked(ProcessRecord app) 针对单进程更新优先级
final void updateOomAdjLocked() 对所有进程更新优先级
系统什么时候根据 oom_score_adj 杀进程?
AMS 调用updateOomAdjLocked() 判断进程是否需要被杀死,若是则调用ProceeRecord::kill()杀死进程
JAVA内存机制
Serial , Serial Old
都是单线程,STW工作线程全部停止后用一个线程清理,适用于小内存,几十M的
缺点:内存大,STW时间很长
Parallel Scavenge,Parallel Old
PSPO 这是java1.8采用的方式,采用并行的多个清理线程进行清理,适用十几G内存
ParNew
年轻代用的,和PS一样的清理方式,专门匹配CMS
CMS
Concurrent mark swap 并发标记清除分为7个阶段,主要是4个
STW->初始标记->STW->并发标记->并发预清理->STW->重新标记->STW->并发清理->STW->并发重置
初始标记:只找到根引用,STW时间很短
并发标记:工作线程可以继续,垃圾回收线程同时继续,时间较长
重新标记:由于并发标记过程中可能标记的垃圾后面又被工作线程引用出现漏标,错标的,于是重新标记,由于错标较少,时间也很快。
并发清理:工作线程也可继续,清理标记的垃圾,工作线程新产生的垃圾叫浮动垃圾
CMS并发标记采用三色标记法找到漏标,Incremental update增量中心
G1采用STAB
ZGC采用color points颜色指针
缺点:由于CMS采用标记算法,碎片化特别严重时会采用单线程的Serial Old进行最后的清理
G1
取消分代,逻辑分代,物理不分,分配成Region,包括Old,Survivor,Eden,Humongous,回收时可以分区域回收,清理内存区域可以随意转换Old,Eden等
Java内存模型
指令重排序
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
1.编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2.指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3.内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
volatile关键字
volatile原理是基于CPU内存屏障指令实现的
内存屏障
内存屏障可以禁止特定类型处理器的重排序,又称内存栅栏,是一个CPU指令,基本上它是一条这样的指令:
1.保证特定操作的执行顺序。
2.影响某些数据(或则是某条指令的执行结果)的内存可见性。
编译器和CPU能够重排序指令,保证最终相同的结果,尝试优化性能。插入一条Memory Barrier会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
Memory Barrier所做的另外一件事是强制刷出各种CPU cache,如一个Write-Barrier(写入屏障)将刷出所有在Barrier之前写入 cache 的数据,因此,任何CPU上的线程都能读取到这些数据的最新版本。
如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。这意味着,如果写入一个volatile变量,就可以保证:
1.一个线程写入变量a后,任何线程访问该变量都会拿到最新值。
2.在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入。