实名反对高赞答案“琴梨梨”的回答,虽然他的答案看起来比很多抖机灵的答案要“专业”许多,但仍然有不少概念含糊、理解错误的地方。最初我是和他在评论里沟通的,可是修订后的答案依然有问题,只是改了几处明显的错误。这样我就不能不亲自下场,以正视听。(想看结论的可以直接拉到结尾)
Android中ART虚拟机的存在是Android需要更大RAM的元凶么?
显然不是。虚拟机是个很容易让人模糊的概念,对于大部分人而言,凡是系统底层弄不懂的一大坨东西,都可以叫做虚拟机。但这个概念对于分析问题而言,百害而无一利。
在Heap初始化的过程中,系统会调用`RegionSpace::CreateMemMap`来为Java堆申请空间。不过需要注意的是,这里申请的是虚拟地址,并不占用物理空间,也不会消耗RAM资源。它会一次性向内核申请1G的虚拟空间(APP进程启动后可能会将后半部分512M释放掉,仅保留512M虚拟内存)。申请的动作最终通过mmap系统调用完成:
void* MemMap::TargetMMap(void* start, size_t len, int prot, int flags, int fd, off_t fd_off) { return mmap(start, len, prot, flags, fd, fd_off); }
这里贴下我一篇博客里写过文字,详述了虚拟地址和物理空间的关系。
虚拟地址和数据的关系
所有的数据都存储在物理内存中,而进程访问内存只能通过虚拟地址。因此,若是想成功访问必须得有个前提:
- 虚拟地址和物理内存之间建立映射关系。若是这层映射关系不建立,则访问会出错。信号11(SIGSEGV)的MAPERR就是专门用来描述这种错误的。
虚拟地址的申请通过mmap完成。这里我们不考虑file-back的mapping,只考虑anonymous mapping。当mmap被调用(flag=MAP_ANONYMOUS)时,实际上会做以下两件事:
1. 分配一块连续的虚拟地址空间。
2. 更新这些虚拟地址对应的PTE(Page Table Entry)。如果细究的话,这个动作其实会在第一次读引发的page fault里完成。
mmap做完这两件事后,就会返回连续虚拟地址空间的起始地址。在mmap调用结束后,其实并不会立即分配物理页。那么如果此时不分配物理页,就会有如下两个问题:
1. 没有新的物理页分配,那么PTE都更新了哪些内容?
2. 如果后续使用mmap返回的虚拟地址访问内存,会有什么情况产生呢?
1. 没有新的物理页分配,那么PTE都更新了些什么内容呢?
PTE也即页表的条目,它的内容反映了一个虚拟地址到物理地址之间的映射关系。如果没有新的物理页分配,这些新的虚拟地址将和和同一个zero page(页内容全为0)建立映射关系。
2. 如果后续使用mmap返回的虚拟地址访问内存,会有什么情况产生呢?
拿到mmap返回的虚拟地址后,并不会有新的物理页分配。此时若是直接读取虚拟地址中的值,则会通过PTE追踪到刚刚建立映射关系的zero page,因此读取出来的值都是0。
如果此时往虚拟地址中写入数据,将会在page fault handler中分配物理页并建立映射关系。需要写多少页,就会新分配多少物理页。所以我们可以看到,真实的物理页是符合lazy(on-demand) allocation原则的。这一点,极大地保证了物理资源的合理分配和使用。
因此,我们能说Heap初始化时mmap出来的1G虚拟空间消耗内存么?这取决你说的是什么内存,如果你说虚拟内存,那当然;可如果你说RAM,也即物理内存,那自然不会。只有当我们往堆里写入实际的数据时,才会产生物理内存的消耗。因此“琴梨梨”下面这段话是完全错误的,他根本没理解申请内存的具体概念。
问题的本质还是在于运行时不知道开发者需要怎么样的内存分配,只能去“猜”或者说是去“适应”,所以就必然只能采取保守的大余量的策略,以免造成一些意外,比如把还有用的东西给回收掉
就比如在cpp中,我知道这个array撑死了也不可能超过512k,那我可以选择就申请512k内存用于存放这个array。但是在java中,当我new了一个array,运行时不知道我这个array会多大,怎么办呢,那就先申请个8m内存吧,等array填充到了6m运行时发现申请的内存快填满了,赶紧再申请一个8m。假如我还是存放一个不到512k的array,在java上却一下子8m内存就这么吃掉了
堆利用率本质上是决定下一次GC触发水位值的一项指标,它和heapmaxfree、heapminfree、foreground_multiplier一起决定了下一次GC触发的水位值。我们每一次通过new分配新的Java对象时,都会将已分配内存的大小和水位值进行比较,一旦超过它,HeapTaskDaemon线程就将发起一次GC(由于是Concurrent Copying GC,只会在FlipThreadRoots阶段非常短暂地对所有线程进行全局暂停,也即大家常常听到的“stop the world”,其他时间都是并发操作,不影响其他线程的运行)。这其中具体的细节,感兴趣的朋友可以阅读本人的拙作:ART虚拟机 | GC的触发时机和条件 - 掘金。
大部分机型的堆利用率都设定为0.75,什么意思?意思就是每个应用占用的内存里,平均只有75%甚至更少是真实使用的,剩下的25%是为了避免频繁扩堆预留的
回到“琴梨梨”回答里论述的观点。首先,GC触发水位值的增长只是一个数值的增长,这里面不会有任何内存新申请的动作产生。而"grow heap"的英文注释不能因为它直译为“扩堆”就简单地认为它会产生新的内存占用。这种浅薄的认识是不对的。大多数人看到“剩下的25%是为了避免频繁扩堆预留的”,都会误认为RAM里有些空间被这25%没有实际使用到的堆给浪费掉了,实际上这完全不会发生。
记住一个概念,物理内存不会提前预留给来自用户空间的请求,它们都是拖到最后一刻,用户空间真的要写数据了才分配出来。
不过即便是GC触发水位值的计算,也并非由堆利用率单独决定,它在很多情况下都会受到maxfree和minfree的钳制。
“琴梨梨”回答里多次提到commit memory的概念,尤其是未修改之前的答案。本人偏重于研究native/ART/framework,对kernel涉足不多,所以一开始讨论时还被他这个概念唬到了。但后来经多方请教(向我司专门负责kernel memory的同事请教)和学习,才发现他说的概念疑点颇多。
首先我们得明白,commit memory是什么意思?我觉得可以理解为申请内存。下面这篇文章介绍得很详细,引用在此。
Linux 理解Linux的memory overcommit 与 OOM Killer
Memory Overcommit的意思是操作系统承诺给进程的内存大小超过了实际可用的内存。一个保守的操作系统不会允许memory overcommit,有多少就分配多少,再申请就没有了,这其实有些浪费内存,因为进程实际使用到的内存往往比申请的内存要少,比如某个进程malloc()了200MB内存,但实际上只用到了100MB,按照UNIX/Linux的算法,物理内存页的分配发生在使用的瞬间,而不是在申请的瞬间,也就是说未用到的100MB内存根本就没有分配,这100MB内存就闲置了。下面这个概念很重要,是理解memory overcommit的关键:commit(或overcommit)针对的是内存申请,内存申请不等于内存分配,内存只在实际用到的时候才分配。
Linux是允许memory overcommit的,只要你来申请内存我就给你,寄希望于进程实际上用不到那么多内存,但万一用到那么多了呢?那就会发生类似“银行挤兑”的危机,现金(内存)不足了。Linux设计了一个OOM killer机制(OOM = out-of-memory)来处理这种危机:挑选一个进程出来杀死,以腾出部分内存,如果还不够就继续杀…也可通过设置内核参数 vm.panic_on_oom 使得发生OOM时自动重启系统。这都是有风险的机制,重启有可能造成业务中断,杀死进程也有可能导致业务中断,我自己的这个小网站就碰到过这种问题,参见前文。所以Linux 2.6之后允许通过内核参数 vm.overcommit_memory 禁止memory overcommit。
因此我们可以知道,禁止memory overcommit可以保证我们申请的内存在后续使用时不会发生物理内存短缺的情况(承诺给你的一定会给你),后续的申请如果超过commit limit,则申请失败。在这种情况下,我们或许可以说,早期commit过多的内存会影响到后续是否能成功commit,进而得出我们需要更大的RAM,也即更大的commit limit来保证后续的commit可以成功。(这是“琴梨梨”之前答案想表达的主要观点)
但这种方式显然不适合Android这种进程很多的场景。它的弊端也很明显,就是如果有些人申请了大量内存却没有使用,那么这些内存也不会允许别人使用。在Android中,init进程在启动阶段设置了 always overcommit,表明完全忽视commit limit的限制。因此所有的虚拟内存申请,只要不超过寻址空间的上限,都是可以成功的。既然虚拟内存的申请不受限制,而物理内存的分配又和申请不在同一时间,那么所有关于commit memory的讨论都对回答Android为什么需要更大的RAM无益,不要再把各种乱七八糟的概念牵扯在一起误人子弟了。
[init.rc]
# Memory management. Basic kernel parameters, and allow the high # level system server to be able to adjust the kernel OOM driver # parameters to match how it is managing things. write /proc/sys/vm/overcommit_memory 1
[overcommit-accounting.rst]
1 Always overcommit. Appropriate for some scientific applications. Classic example is code using sparse arrays and just relying on the virtual memory consisting almost entirely of zero pages.
而与此同时,ART运行时本身也会产生一定额外的内存消耗,将dex2oat产生的AOT编译文件载入内存也需要消耗额外的内存
所有file-back的内存在RAM不够用的时候都可以被释放出来给其他人使用,需要时再通过IO从emmc/ufs设备上读过来。而这些内存占用对解答“Android为什么需要更大的RAM”这个问题没有帮助。
android的内存管理自上到下分为三层,第一层是虚拟机本身的OOM限制,也就是上面的堆大小限制,但只约束dalvik heap,对native heap和显存没有限制,第二层是android framework层的lmk机制,内存不足时杀死占用大内存的进程,第三层是linux内核层的内存管理机制,虽然android默认开启overcommit,从而禁用了linux内核的commit限制,但是内核中的内存碎片处理算法以及zram/swap仍然是开启的
LMK全称"Low Memory Killer",在Android存在一个进程叫LMKD(D是Daemon的意思),它是属于native层的概念,而不是framework层。且kernel中也有自己的OOM Killer,这个不受overcommit控制,如果说第三层应该说这个。
这个问题当然有很多答案,但分析问题我们要抓住主要矛盾。就我个人的看法,它应该主要和后台进程管理及整个生态较为开放有关。早年的各种保活黑科技就不谈了,即便是近期,也有各种突破资源限制的方案出来,譬如字节这篇文章拯救OOM!字节自研 Android 虚拟机内存管理优化黑科技 mSponge提到的mSponge,通过修改虚拟机的管控策略,从而突破512M的堆内存上限,占用更多的内存资源。不过站在系统工程师的角度,我是强烈反对这种黑科技的。
系统的目标是保证公平,最大化使用者的直接体验。而应用的目标是保证自己享用最充足的资源,哪怕我切到后台,我也希望永生,这样下次热启动就会很快,使用者就会认为我这个应用做的不错。所以每个应用都希望自己拥有的更多。而安卓开放的生态(国内无法使用谷歌生态进一步加剧了生态的四分五裂),宽松的限制纵容了应用的胡作非为,所以RAM用起来总感觉不够。另一个方面,苹果和安卓的竞争是多维度的,而安卓厂商间的竞争最后很容易变成纯硬件的比拼,这也导致了安卓手机更喜欢追求硬件上的迭代,不管是RAM,摄像头数目还是分辨率,等等。
(删去原本回答中言辞过激的部分。技术讨论需要百家争鸣,我可以反对别人的回答,但不能贬低。文中所述不敢保证全部正确,但基本都是研究过源码后才给出的结论。如果有网友发现不妥之处,欢迎评论区讨论哈)
因为iphone13是苹果的低端机,4gb不够用,并且是厂商故意让你不够用,为了和6gb的pro区分开来。
6gb的pro内存问题不明显。
都2022年了还搁这儿吹iphone 4gb内存够用呢?根本就不够啊
13和13mini杀后台杀到亲妈都不认识了,真用过的不会不知道吧?
ios再优秀也解决不了国内主流垃圾软件的内存占用吧?
4gb这么好,为啥pro 6gb?为啥ipad pro m1 8gb/16gb?
12gb好!12gb帅!12gb呱呱叫!
-----------------------------------------------------
利益相关:13mini 256、小米12 12+256双持用户,ipad pro m1用户,原果粉,原原生android粉,原s21u、12pro用户,即将购入s22u。android、ios使用时长均超过10年,两个系统的手机均超过10部。
我以为13mini能解决便携问题,用起来最起码能用,出掉了s21u,结果确实挺便携,但根本没法用。
骂我的评论我直接删了,对骂你还真不一定骂得过我,没意思。有话好好说,别搁这儿喷粪。
这个问题实际上是个Java虚拟机的问题
在Java中开发者无法手动分配内存,分配内存的工作都是运行时来完成的
为了避免频繁的Commit内存和频繁的GC带来的性能损失,运行时都会倾向于提前Commit更大的内存,而与此同时,ART运行时本身也会产生一定额外的内存消耗,将dex2oat产生的AOT编译文件载入内存也需要消耗额外的内存
在Android上,运行时的内存管理机制由build.prop配置
就是下面这一段,我用getprop指令读取(Android10开始非root用户无法直接读取build.prop,必须使用getprop指令)
[dalvik.vm.heapgrowthlimit]: [256m]常规应用堆限制
[dalvik.vm.heapmaxfree]: [8m]单次GC的最大调整量
[dalvik.vm.heapminfree]: [2m]单次GC的最小调整量
[dalvik.vm.heapsize]: [512m]设置largeHeap的应用堆限制,一般为游戏等重负载应用,但一些户口本就一页的开发者喜欢滥用
[dalvik.vm.heapstartsize]: [8m]应用启动时的堆大小
[dalvik.vm.heaptargetutilization]: [0.75]堆利用率
大部分机型的堆利用率都设定为0.75,什么意思?意思就是每个应用占用的内存里,平均只有75%甚至更少是真实使用的,剩下的25%是为了避免频繁扩堆预留的
android的内存管理自上到下分为三层,第一层是虚拟机本身的OOM限制,也就是上面的堆大小限制,但只约束dalvik heap,对native heap和显存没有限制,第二层是android framework层的lmk机制,内存不足时杀死占用大内存的进程,第三层是linux内核层的内存管理机制,虽然android默认开启overcommit,从而禁用了linux内核的commit限制,但是内核中的内存碎片处理算法以及zram/swap仍然是开启的
这种复杂的内存管理机制源于android假定所有非系统核心进程都是可随时杀死的,通过杀死进程来及时的释放内存
而且,大部分android设备都没有独立显存,一直是内存即显存,但在8.0之前,甚至bitmap都要在dalvik heap和texture memory里重复储存,直到8.0才引入了hardware bitmap解决了这一问题
可以说android的设计思路一直都是尽可能提升大内存设备的性能表现,以牺牲小内存设备表现为代价
当然,用jni native可以自己管理内存,但应用如果全写native那还搞个锤子Android啊,直接Ubuntu touch不香吗?
其实所有由运行时负责内存管理的语言都有一样的问题,包括但不限于python,js,http://vb.net/ c# 等
问题的本质还是在于运行时不知道开发者需要怎么样的内存分配,只能去“猜”或者说是去“适应”,所以就必然只能采取保守的大余量的策略,以免造成一些意外,比如把还有用的东西给回收掉
就比如在cpp中,我知道这个array撑死了也不可能超过512k,那我可以选择就申请512k内存用于存放这个array。但是在java中,当我new了一个array,运行时不知道我这个array会多大,怎么办呢,那就先申请个8m内存吧,等array填充到了6m运行时发现申请的内存快填满了,赶紧再申请一个8m。假如我还是存放一个不到512k的array,在java上却一下子8m内存就这么吃掉了
就好比停车,cpp是买一个刚刚好的车位,小心的停进去,java这些运行时包裹的语言就是直接买一排车位让你随便停,停满了就再来一排车位
从开发者角度来说,这些语言不用考虑内存申请和释放,降低了开发难度,加快了项目的开发进度,但确实容易对于需要精确管控内存的大型项目来说比较灾难,会出现为了在不能直接管理内存的情况下强行管理内存,就得想着办法去触发运行时进行GC的条件,比如写js的时候把变量定义成false甚至undefined来释放变量的办法
这其实也是方便和精确的取舍平衡,要小项目的开发方便,就要舍弃一些控制上的精确性
大多数笔记本电脑的运存也就8个GB,就能够运行很多庞大的高水平的游戏了。
你手机是有什么软件8个GB都不够用?王者荣耀?王者荣耀的体量和英雄联盟、dota比谁大?
我看等以后迟早手机的运存要比很多电脑还要大,不然永远也满足不了我们如此多软件的需求。
不是安卓阵营手机进入12GB时代,而是被流氓软件占后台内卷到12GB。。
亮多了多说几句。
不少童鞋想问,为什么这些软件要占后台呢?
有些流氓软件通过安卓系统的唤醒机制自启,占用运存资源,甚至还唤醒一大堆关联软件,可以实现不断推送各类消息、广告的目的。
流氓软件占后台的行为存在,不仅让手机变得异常卡顿,耗电严重,还不断启用各种权限,获取用户信息,推送广告。
天下苦流氓软件久矣!
因为Commodore 64都能实现的功能,给某些国内软件开发商做,做出来的软件也能把4G RAM吃的一点不剩。而安卓这边的生态环境刚好由这些开发商说了算,厂家就不得不把内存一提再提。
而iOS生态苹果说了算,内存就那么4G,开发商就得省着用。如果app不加节制使用资源导致设备卡顿,那么苹果根本不会允许上架。就算通过侧载直接装ipa,iOS的资源管理机制有多狠也是有目共睹。导致卡顿直接杀,具体表现就是频繁闪退,软件根本不能用。因此开发商必须从自身下手,软件再复杂,内存也就这么点。要么下功夫优化,要么放弃iOS市场。显然做优化多花的钱比放弃iOS少赚的钱少得多。
与PS4、Switch等主机性能如此孱弱, 却仍然有如此多3A大作的原理相同。
我查看了一下手机的内存占用。
现在的安卓手机,内存占用是这样的。
一个最新的安卓12系统,核心的内存占用是3.3G。
还有个安卓系统进程占了451M
系统界面进程占了400M
系统桌面200多M
浏览器200多M。
微信双开,占用了800多兆。我这还是用了省资源的微信7.015版本。没用最新的。
知乎用了低版本,占了200多M
最后,系统用了8.7G内存。
这还没开大游戏。
就是说安卓系统自己就已经非常臃肿了。
虽然IOS也臃肿,但是臃肿的程度远不及安卓。
以前的安卓不是这样的。
安卓8的时候,1080p屏幕的系统,用1.7G内存就够了。加上当时的应用,只要2G多内存就可以。
当时的手机4G内存就够了。
那么更老的手机呢?
我找到了一台第一代1080p分辨率的手机。
vivo的xplay
这台基于安卓4.4版本的手机。
系统只需要要400M左右的内存,各种自带应用自启动。
也只不过需要700多M就可以正常运行了。
当时的手机2G内存足够用了。
就是说,即使是安卓系统。
如果不臃肿。
4G内存足够用了。
安卓4.4,现在依然可以正常运行很多安卓app。
1080p界面流畅滑动。
现在一些安卓电视,依然在安卓4.4下运行,正常看爱奇艺,腾讯视频。
出于商业目的,厂商要不断臃肿化,让你更新硬件。
同样的功能,以前需要100mhz的处理器,10M内存。
升级以后,就需要4Ghz处理器,2G内存。而功能速度没有变化,为了浪费而浪费。
电脑相对要好一点。
尽管厂商也不给驱动,让新硬件不支持老系统,但是得益于兼容性。
win10依然可以正常运行20多年前的office95。
当你用十二代酷睿,打开几十年前的办公软件,譬如word2003,你就体会到什么叫秒开。
真是双击完了,就可以工作了,没有丝毫延迟。
而你在手机上,10年前5秒开淘宝,现在还是5秒开淘宝,想购物浪费的时间是一样的,丝毫感觉不到科技的进步。
有没有一种可能,这些软件本来只需要4GB内存就够了?
厨子说我一部4G运存的iPhone,流畅度都吊打你们12G运存的安卓,你们进入个寂寞时代?
Android在中国大陆可是没有GMS的,没有统一推送服务,微信和QQ就得常驻内存。
微信内存基本上就是400MB到1GB之间吧,有时候更高,很少情况会低于400MB。
安卓软件常驻内存真的很难禁止,尝试自启动,尝试唤醒别的APP,尝试获取各种隐私信息。