android内存管理优化哪些事

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
您的访问请求被拒绝 403 Forbidden - ITeye技术社区
您的访问请求被拒绝
亲爱的会员,您的IP地址所在网段被ITeye拒绝服务,这可能是以下两种情况导致:
一、您所在的网段内有网络爬虫大量抓取ITeye网页,为保证其他人流畅的访问ITeye,该网段被ITeye拒绝
二、您通过某个代理服务器访问ITeye网站,该代理服务器被网络爬虫利用,大量抓取ITeye网页
请您点击按钮解除封锁&(科普向)windows 和安卓的内存管理到底哪个好_显卡吧_百度贴吧
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&签到排名:今日本吧第个签到,本吧因你更精彩,明天继续来努力!
本吧签到人数:0成为超级会员,使用一键签到本月漏签0次!成为超级会员,赠送8张补签卡连续签到:天&&累计签到:天超级会员单次开通12个月以上,赠送连续签到卡3张
关注:2,071,470贴子:
(科普向)windows 和安卓的内存管理到底哪个好收藏
今天,我被大神教育了,原因是贬低了安卓优秀的内存管理,吹捧windows sb的内存管理。思索良久决定新开一贴,顺便贴些数据看看windows到底怎么样。。技术讨论,求别删
多种专业显卡维修方案,免费对比挑选,正规公司提供服务!无需反复电话咨询!集合大量资源,货比三家,满足您的需求!
苹果好,此贴终结。
安卓基本和电脑的思想一样 有Service 后台跑着n多进程
问题1:安卓没有必要清理内存,内存清理是windows遗留下来的做法。首先,在内存不足的情况下,再开新程序速度会有所变慢。不过,“大神”告诉我反而会变快。还希望大家试试到底变快还是变慢。
windows phone跟windows更接近 用c#开发的
windows 和 android都差不多把 哪有机制不机制的!要说区别的话 windows不会因为内存不够关后台!
ios 根本不像android 没有service组件 不是标准意义上的后台 确实是这样 只是向系统申请
一个c类写的定制系统 另一个java套虚拟机到处跑的系统 C写的跑不过java那苹果别活了
反正Linux内存管理比win好
在windows系统中,有个叫备用的东西,当你退出软件之后,内存并不会被释放,仍然在内存中,显示为“备用”,当内存不足时,“备用“的内存会被释放。那么安卓有没有功能与之相似呢,当然有。大部分的安卓软件退出后,仍然会保留一个”以缓存“,下次启动速度明显加快。某些文章会写以缓存的东西不会占用cpu,不过嘛,,大家不知道用过re管理器没,解压文件时切出去,内存中会显示为以缓存,但是文件还是会继续解压。既然不用cpu,那么解压是如何完成的?所以说,安卓的缓存机制还有问题
反正我不懂
”比如pc电脑,正常情况下,你100g的内存和200g的内存根本没有区别,因为你200g多出来的100g根本用不到。而你开个虚拟磁盘,把内存虚拟成硬盘,就可以有效利用起来。android好比自带这个缓存系统,把你用不到的内存预载起来&然而了解比较多的都知道,在大内存情况下,空闲的内存并没有浪费,而是拿来缓存了。就算内存很大,用了一段时间,任务管理器的空闲也会变得很小。最重要的是,windows缓存的文件释放是几乎不要时间的,而安卓占了一大半内存的情况下,释放内存浪费的·时间可不少
windows只是显示的内存占用小而已,可不代表内存被浪费了
windows phone 移植微软那套东西非常方便 只是移动平台占有率不高
是这样的,我有一个朋友就在波斯湾开面馆。最早去的伊拉克,现在已经在阿富汗、利比亚、叙利亚都开了分店,前两天打电话很兴奋的和我说,准备去叙利亚开面馆了,就是人手不够,问我有时间没有?其实我这个朋友最早是个混混,工厂不要,公司开除的那种,基本在国内走投无路了,无奈之下在一家川菜馆当小工。2001年吧,美军进攻伊拉克,这兄弟的老板居然疯了,跑到伊拉克开了家川菜馆,这兄弟也就跟着去了。结果生意好的不得了,美军经常坐着直升飞机来点餐,螺旋桨就这么“咣当咣当”的开着,几个黑大兵就这么端着机枪等炒菜,完了用塑料筐装走,大把的美金哗哗的扔,一盘泡菜国内不要钱,卖给美军那就是15美金小半碟子,一个鱼香肉丝要120美金,一碗白米饭20美金。每天老板数钱手都在抖。可惜好景不长啊,2个月不到,他老板就被汽车炸弹轰上了天,这兄弟他命大,那天去美军司令部送外卖,算是捡了一条命。这兄弟,经过这一回也算是想开了。豁出去了,川菜是没法开了,就开面馆了。胆子也大,不要店面(免得美军聚餐的时候基地来放炸弹),只要流动面点车。就是一辆废弃的装甲车,后面割开做成开放式的售货车,遮阳,安全,还防弹。好家伙,生意好的牛上了天。这车就跟着美军走,美军打着打着饿了,坐下来来一碗兰州拉面,热腾腾,吃完嘴一擦,提着步枪上去接着打。有时候和基地打胶着了,分不清南北,也有基地的来吃面的,便吃面还边叫唤:阿拉-阿克吧!不要辣椒!阿拉阿克吧,牛肉少了老子要弄你····。他小子也是大方,经常是一半儿面,一半儿牛肉。最后搞的是装甲车左边坐着美军,右边坐着塔利班,大家都形成默契了,吃中国人的兰州拉面的时候,不许开枪。 这兄弟后来跟着美军又去了阿富汗、巴基斯坦,现在已经跟着几个基地的老客户去了叙利亚。据说在北京最近入手了3环内的别墅,年底还要在上海外滩买两个店面。他在电话里和我说:兄弟,美军上将拉德乌斯将军给他电话了,说美军要在伊拉克和叙利亚动手打IS了,估计出动的美军至少10万人,很想念他的兰州拉面。问我有没有兴趣当个分区经理?装甲车他都看好了,伊拉克美军剩余物资,防地雷装甲车,美军还送7.62机枪一挺和一台移动炊事车,我觉得很不错,但是我不会炒菜,我就想问问学炒菜那家比较强,还有,给我点个标签吧。在线等,急! ?﹏﹏ 出来吧我的小天使们
??我是①号 ?
??我是②号
??我是③号 ?
??我是④号 ?
??我是⑤号 ?
??我是⑥号 ?
??我是⑦号有没有被我强势暖贴吓到!!!(?o??ωo???)
安卓后台还有人吹?脑子让驴踢了?
我读书少,不知道那么多人追求省内存的rom是为了什么。但我知道,一个省内存的,可以开20个软件不杀rom,然后提前开好可能要用的软件,然后就可以一直秒切换的rom,体检绝对好于一个吃内存但是会预载但占内存极多的rom。因为那种rom体验往往是这样:开软件速度很快,开多了就掉。试想,你在玩游戏,开了qq微信音乐浏览器,来了个电话,游戏就没了,体验好吗?如果让你开一堆软件不掉,但是启动速度慢点,你要哪个?
机器人军团逐渐壮大 有本事苹果一挑十 一挑一百
我知道那位大神是软件开发者,懂得比我多。可是理论不等于实际,卡吧都是考虑实际体验的人,看起来完美,技术先进,用起来一般,指的就是安卓
主要取决于软件的内存管理机制比如win用的c和vb,要求程序员自己决定软件什么时候删除无用数据,这时会有优化问题,优化好(程序员写得好)那么吃内存少而安卓用的java,不需要程序员太在意内存管理,jvm会自己判断对象是否为垃圾,并且在需要的时候清理,严格来说,安卓从来就不存在未使用的内存,所谓清内存也不会明显提升性能,比如你打开一个大型应用,内存不足时安卓会开始清垃圾,然后腾出空间。而内存清理无非手动完成这一过程,在你打开的时候会感觉快一点,但是,手动的速度再怎么也比不上自动对于一些软件,使用java,比如游戏我的世界,需要设置jvm最大内存,如果你使用64位java,把内存设置的很大,你会发现它毫不迟疑地吃掉了你所有内存总的来说,这取决于软件而不是系统本身,java本是开发效率很高的语言,运行效率低一点很正常,不过更严重的是现在的程序员倒是很不注重运行效率,在快速开发过程中让应用吃掉了太多内存,说到底还是程序员的问题,现在pc上还不是有一些软件游戏是内存杀手?一个道理
所谓不清内存打开会变快有时候是成立的,如果这个应用还有一些对象残留,再次打开时就不需要重新创建出来了,但是内存不足时需要先清理其它应用的对象,还是会变慢,但是如果算上你手动清内存的时间,不管怎么样都是不清内存更快
我看了半天没看到哪里解释了内存管理机制,神论倒是不少
关于所谓“掉”的说法,其实就是当你把应用切到后台而没有动作时,就会被判定为垃圾内存不够就自动清掉了
登录百度帐号推荐应用
为兴趣而生,贴吧更懂你。或一般来说,程序使用内存的方式遵循先向操作系统申请一块内存,使用内存,使用完毕之后释放内存归还给操作系统。然而在传统的C/C++等要求显式释放内存的编程语言中,记得在合适的时候释放内存是一个很有难度的工作,因此Java等编程语言都提供了基于垃圾回收算法的内存管理机制:
垃圾内存回收算法
常见的垃圾回收算法有引用计数法(Reference Counting)、标注并清理(Mark and Sweep GC)、拷贝(Copying GC)和逐代回收(Generational GC)等算法,其中Android系统采用的是标注并删除和拷贝GC,并不是大多数JVM实现里采用的逐代回收算法。由于几个算法各有优缺点,所以在很多垃圾回收实现中,常常可以看到将几种算法合并使用的场景,本节将一一讲解这几个算法。
引用计数回收法(Reference Counting GC)
引用计数法的原理很简单,即记录每个对象被引用的次数。每当创建一个新的对象,或者将其它指针指向该对象时,引用计数都会累加一次;而每当将指向对象的指针移除时,引用计数都会递减一次,当引用次数降为0时,删除对象并回收内存。采用这种算法的较出名的框架有微软的COM框架,如代码清单14 - 1演示了一个对象引用计数的增减方式。
代码清单14 - 1 引用计数增减方式演示伪码
Object *obj1 = new Object(); // obj1的引用计数为1
Object *obj2 = obj1; // obj1的引用技术为2
Object *obj3 = new Object();
obj2 = NULL; // obj1的引用计数递减1次为1。
obj1 = obj3; // obj1的引用计数递减1次为0,可以回收其内存。
通常对象的引用计数都会跟对象放在一起,系统在分配完对象的内存后,返回的对象指针会跳过引用计数部分,如代码清单14 - 1所示:
图 14 - 1 采用引用计数对象的内存布局示例
然而引用计数回收算法有一个很大的弱点,就是无法有效处理循环引用的问题,由于Android系统没有使用该算法,所以这里不做过多的描述,请有兴趣的读者自行查阅相关文档。
标注并清理回收法(Mark and Sweep GC)
在这个算法中,程序在运行的过程中不停的创建新的对象并消耗内存,直到内存用光,这时再要创建新对象时,系统暂停其它组件的运行,触发GC线程启动垃圾回收过程。内存回收的原理很简单,就是从所谓的"GC Roots"集合开始,将内存整个遍历一次,保留所有可以被GC Roots直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,如代码清单14 - 3:
代码清单14 - 2 标注并清理算法伪码
SuspendAllThreads();
List&Object& roots = GetRoots();
foreach ( Object root : roots ) {
Mark(root);
ResumeAllThreads();
算法通常分为两个主要的步骤:
标注(Mark)阶段:这个过程的伪码如代码清单14 - 2所示,针对GC Roots中的每一个对象,采用递归调用的方式(第8行)处理其直接和间接引用到的所有对象:
代码清单14 - 3 标注并清理的标注阶段伪码
void Mark(Object* pObj) {
if ( !pObj-&IsMarked() ) {
&&&& // 修改对象头的Marked标志
4.&&&& pObj-&Mark();
5.&&&& // 深度优先遍历对象引用到的所有对象
6.&&&& List&Object *& fields = pObj-&GetFields();
7.&&&& foreach ( Object* field : fields ) {
8.&&&& Make(field); // 递归处理引用到的对象
如果对象引用的层次过深,递归调用消耗完虚拟机内GC线程的栈空间,从而导致栈空间溢出(StackOverflow)异常,为了避免这种情况的发生,在具体实现时,通常是用一个叫做标注栈(Mark Stack)的数据结构来分解递归调用。一开始,标注栈(Mark Stack)的大小是固定的,但在一些极端情况下,如果标注栈的空间也不够的话,则会分配一个新的标注栈(Mark Stack),并将新老栈用链表连接起来。
与引用计数法中对象的内存布局类似,对象是否被标注的标志也是保存在对象头里的,如图 14 - 2所示。
图 14 - 2 标注和清理算法中的对象布局
如图 14 - 2是垃圾回收前的对象之间的引用关系;GC线程遍历完整个内存堆之后,标识出所以可以被"GC Roots"引用到的对象-即代码清单14 - 2中的第4行,结果如图 14 - 3中高亮的部分,对于所有未被引用到(即未被标注)的对象,都将其作为垃圾收集。
图 14 - 3 回收内存垃圾之前的对象引用关系
图 14 - 4 GC线程标识出所有不能被回收的对象实例
清理(SWEEP)阶段:即执行垃圾回收过程,留下有用的对象,如图 14 - 4所示,代码清单14 - 3是这个过程的伪码,在这个阶段,GC线程遍历整个内存,将所有没有标注的对象(即垃圾)全部回收,并将保留下来的对象的标志清除掉,以便下次GC过程中使用。
代码清单14 - 4 标注和清理法中的清理过程伪码
void Sweep() {
Object *pIter = GetHeapBegin();
&&&& while ( pIter & GetHeapEnd() ) {
&&&& if ( !pIter-&IsMarked() )
&&&& Free(pIter);
&&&& pIter-&UnMark();
9.&&&& pIter = MoveNext(pIter);
10. &&&& }
图 14 - 5 GC线程执行完垃圾回收过程后的对象图
这个方法的优点是很好地处理了引用计数中的循环引用问题,而且在内存足够的前提下,对程序几乎没有任何额外的性能开支(如不需要维护引用计数的代码等),然而它的一个很大的缺点就是在执行垃圾回收过程中,需要中断进程内其它组件的执行。
标注并整理回收法(Mark and COMPACT GC)
这个是前面标注并清理法的一个变种,系统在长时间运行的过程中,反复分配和释放内存很有可能会导致内存堆里的碎片过多,从而影响分配效率,因此有些采用此算法的实现(Android系统中并没有采用这个做法),在清理(SWEEP)过程中,还会执行内存中移动存活的对象,使其排列的更紧凑。在这种算法中,,虚拟机在内存中依次排列和保存对象,可以想象GC组件在内部保存了一个虚拟的指针 & 下个对象分配的起始位置 ,如图 14 - 6中演示的示例应用,其GC内存堆中已经分配有3个对象,因此"下个对象分配的起始位置"指向已分配对象的末尾,新的对象"object 4"(虚线部分)的起始位置将从这里开始。
这个内存分配机制和C/C++的malloc分配机制有很大的区别,在C/C++中分配一块内存时,通常malloc函数需要遍历一个"可用内存空间"链表,采取"first-first"(即返回第一块大于内存分配请求大小的内存块)或"best-fit"( 即返回大于内存分配请求大小的最小内存块),无论是哪种机制,这个遍历过程相对来说都是一个较为耗时的时间。然而在Java语言中,理论上,为一个对象分配内存的速度甚至可能比C/C++更快一些,这是因为其只需要调整指针"下个对象分配的起始位置"的位置即可,据Sun的工程师估计,这个过程大概只需要执行10个左右的机器指令。
图 14 - 6 在GC中为对象分配内存
由于虚拟机在给对象分配内存时,一直不停地向后递增指针"下个对象分配的起始位置",潜台词就是将GC堆当做一个无限大的内存对待的,为了满足这个要求,GC线程在收集完垃圾内存之后,还需要压缩内存 & 即移动存活的对象,将它们紧凑的排列在GC内存堆中,如图 14 - 7是Java进程内GC前的内存布局,执行回收过程时,GC线程从进程中所有的Java线程对象、各线程堆栈里的局部变量、所有的静态变量和JNI引用等GC Root开始遍历。
图 14 - 7中,可以被GC Root访问到的对象有A、C、D、E、F、H六个对象,为了避免内存碎片问题,和满足快速分配对象的要求,GC线程移动这六个对象,使内存使用更为紧凑,如图 14 - 7所示。由于GC线程移动了存活下来对象的内存位置,其必须更新其他线程中对这些对象的引用,如图 14 - 7中,由于A引用了E,移动之后,就必须更新这个引用,在更新过程中,必须中断正在使用A的线程,防止其访问到错误的内存位置而导致无法预料的错误。
图 14 - 7 垃圾回收前的GC堆上的对象布局及引用关系
图 14 - 8 GC线程移动存活的对象使内存布局更为紧凑
注意现代操作系统中,针对C/C++的内存分配算法已经做了大量的改进,例如在Windows中,堆管理器提供了一个叫做"Look Aside List"的缓存针对大部分程序都是频繁分配小块内存的情形做的优化,具体技术细节请可以参阅笔者的在线付费技术视频:
调试堆溢出问题(上):
调试堆溢出问题(中):
调试堆溢出问题(下):
拷贝回收法(Copying GC)
这也是标注法的一个变种, GC内存堆实际上分成乒(ping)和乓(pong)两部分。一开始,所有的内存分配请求都有乒(ping)部分满足,其维护"下个对象分配的起始位置"指针,分配内存仅仅就是操作下这个指针而已,当乒(ping)的内存快用完时,采用标注(Mark)算法识别出存活的对象,如图 14 - 9所示,并将它们拷贝到乓(pong)部分,后续的内存分配请求都在乓(pong)部分完成,如图 14 - 10。而乓(pong)里的内存用完后,再切换回乒(ping)部分,使用内存就跟打乒乓球一样。
图 14 - 9 拷贝回收法中的乒乓内存块
图 14 - 10 拷贝回收法中的切换乒乓内存块以满足内存分配请求
回收算法的优点在于内存分配速度快,而且还有可能实现低中断,因为在垃圾回收过程中,从一块内存拷贝存活对象到另一块内存的同时,还可以满足新的内存分配请求,但其缺点是需要有额外的一个内存空间。不过对于回收算法的缺点,也可以通过操作系统地虚拟内存提供的地址空间申请和提交分布操作的方式实现优化,因此在一些JVM实现中,其Eden区域内的垃圾回收采用此算法。
逐代回收法(Generational GC)
也是标注法的一个变种,标注法最大的问题就是中断的时间过长,此算法是对标注法的优化基于下面几个发现:
大部分对象创建完很快就没用了 & 即变成垃圾;
每次GC收集的90%的对象都是上次GC后创建的;
如果对象可以活过一个GC周期,那么它在后续几次GC中变成垃圾的几率很小,因此每次在GC过程中反复标注和处理它是浪费时间。
可以将逐代回收法看成拷贝GC算法的一个扩展,一开始所有的对象都是分配在"年轻一代对象池" 中 & 在JVM中其被称为Young,如图 14 - 11:
图 14 - 11 逐代(generational) GC中开始对象都是分配在年轻一代对象池(Young generation)中
第一次垃圾回收过后,垃圾回收算法一般采用标注并清理算法,存活的对象会移动到"老一代对象池"中& 在JVM中其被称为Tenured,如图 14 - 12,而后面新创建的对象仍然在"年轻一代对象池"中创建,这样进程不停地重复前面两个步骤。等到"老一代对象池"也快要被填满时,虚拟机此时再在"老一代对象池"中执行垃圾回收过程释放内存。在逐代GC算法中,由于"年轻一代对象池"中的回收过程很快 & 只有很少的对象会存活,而执行时间较长的"老一代对象池"中的垃圾回收过程执行不频繁,实现了很好的平衡,因此大部分虚拟机,如JVM、.NET的CLR都采用这种算法。
图 14 - 12 逐代GC中将存活的对象挪到老一代对象池
在逐代GC中,有一个较棘手的问题需要处理 & 即如何处理老一代对象引用新一代对象的问题,如图 14 - 13中。由于每次GC都是在单独的对象池中执行的,当GC Root之一R3被释放后,在"年轻一代对象池"中执行GC过程时,R3所引用的对象f、g、h、i和j都会被当做垃圾回收掉,这样就导致"老一代对象池"中的对象c有一个无效引用。
图 14 - 13 逐代GC中老一代对象引用新对象的问题
为了避免这种情况,在"年轻一代对象池"中执行GC过程时,也需要将对象C当做GC Root之一。一个名为"Card Table"的数据结构就是专门设计用来处理这种情况的,"Card Table"是一个位数组,每一个位都表示"老一代对象池"内存中一块4KB的区域 & 之所以取4KB,是因为大部分计算机系统中,内存页大小就是4KB。当用户代码执行一个引用赋值(reference assignment)时,虚拟机(通常是JIT组件)不会直接修改内存,而是先将被赋值的内存地址与"老一代对象池"的地址空间做一次比较,如果要修改的内存地址是"老一代对象池"中的地址,虚拟机会修改"Card Table"对应的位为 1,表示其对应的内存页已经修改过 - 不干净(dirty)了,如图 14 - 14。
图 14 - 14 逐代GC中Card Table数据结构示意图
当需要在 "年轻一代对象池"中执行GC时, GC线程先查看"Card Table"中的位,找到不干净的内存页,将该内存页中的所有对象都加入GC Root。虽然初看起来,有点浪费, 但是据统计,通常从老一代的对象引用新一代对象的几率不超过1%,因此"Card Table"的算法是一小部分的时间损失换取空间。
Android内存管理源码分析
在Android中 ,实现了标注与清理(Mark and Sweep)和拷贝GC,但是具体使用什么算法是在编译期决定的,无法在运行的时候动态更换 & 至少在目前的版本上(4.2)还是这样。在Android的dalvik虚拟机源码的Android.mk文件(路径是/dalvik/vm/Dvm.mk)里,有类似代码清单14 - 5的代码,即如果在编译dalvik虚拟机的命令中指明了"WITH_COPYING_GC"选项,则编译"/dalvik/vm/alloc/Copying.cpp"源码 & 此是Android中拷贝GC算法的实现,否则编译"/dalvik/vm/alloc/HeapSource.cpp" & 其实现了标注与清理GC算法,也就是本节分析的重点。
代码清单14 - 5 编译器指定使用拷贝GC还是标注与清理GC算法
WITH_COPYING_GC := $(strip $(WITH_COPYING_GC))
ifeq ($(WITH_COPYING_GC),true)
LOCAL_CFLAGS += -DWITH_COPYING_GC
LOCAL_SRC_FILES += \
&&&&alloc/Copying.cpp.arm
LOCAL_SRC_FILES += \
&&&&alloc/DlMalloc.cpp \
&&&&alloc/HeapSource.cpp \
&&&&alloc/MarkSweep.cpp.arm
注意本节中分析的Android源码,可以在网址: 中在线浏览。
在Java中,对象是分配在Java内存堆之上的,当Java程序启动后,JVM会向操作系统申请保留一大块连续的内存。
在Android源码中,这个过程分为下面几步:
dvmStartup函数(/dalvik/vm/Init.cpp:1376)解析完传入虚拟机的命令行参数,调用dvmGcStartup函数初始化GC组件。
dvmGcStartup函数(/dalvik/vm/alloc/Alloc.cpp:30)负责初始化几个GC线程同步原语,再调用dvmHeapStartup函数初始化GC内存堆(即Java内存堆)。
dvmHeapStartup函数(/dalvik/vm/alloc/Heap.cpp:75)则根据GC参数设置调用dvmHeapSourceStartup函数向操作系统申请一大块连续的内存空间,这个内存空间会自动增长,在默认设置中(/dalvik/vm/Init.cpp:1237),该内存堆的初始大小是2MB & 由gDvm.heapStartingSize指定,内存堆最大不超过16MB(Java程序用完这16MB内存就会导致OOM异常) & 由gDvm.heapGrowthLimit指定,如果gDvm.heapGrowthLimit的值为0的话(即表示可以无限增长),则将最大值限定为gDvm.heapMaximumSize的值。申请完内存空间之后,初始化一个名为clearedReferences的队列(/dalvik/vm/alloc/Heap.cpp:98),这个队列将用在保存finalizable对象,以在另一个线程中执行它们的finalize函数。最后,dvmHeapStartup函数还要初始化数据结构Card Table(/dalvik/vm/alloc/Heap.cpp:100),如代码清单14 - 6。
代码清单14 - 6 dvmHeapStartup初始化GC内存堆
75 bool dvmHeapStartup()
77 GcHeap *gcH
79 if (gDvm.heapGrowthLimit == 0) {
80 gDvm.heapGrowthLimit = gDvm.heapMaximumS
83 gcHeap = dvmHeapSourceStartup(gDvm.heapStartingSize,
84 gDvm.heapMaximumSize,
85 gDvm.heapGrowthLimit);
86 if (gcHeap == NULL) {
89 gcHeap-&ddmHpifWhen = 0;
90 gcHeap-&ddmHpsgWhen = 0;
91 gcHeap-&ddmHpsgWhat = 0;
92 gcHeap-&ddmNhsgWhen = 0;
93 gcHeap-&ddmNhsgWhat = 0;
94 gDvm.gcHeap = gcH
96 /* Set up the lists we'll use for cleared reference objects.
98 gcHeap-&clearedReferences = NULL;
100 if (!dvmCardTableStartup(gDvm.heapMaximumSize, gDvm.heapGrowthLimit)) {
101 LOGE_HEAP("card table startup failed.");
dvmHeapSourceStartup函数(/dalvik/vm/alloc/HeapSource.cpp:541)通过dvmAllocRegion函数向操作系统申请保留一大块连续的内存地址空间,其大小是内存堆最大可能的大小(/dalvik/vm/alloc/HeapSource.cpp:563),成功后,再根据内存堆的初始大小申请内存。如默认情况下,Java内存堆的初始大小是2MB,而最大能增长到16MB,那么一开始dvmHeapSourceStartup会申请16MB大小的地址空间,但一开始只分配2MB的内存备用。在底层内存实现上,Android系统使用的是dlmalloc实现-又叫msspace,这是一个轻量级的malloc实现。
除了创建和初始化用于存储普通Java对象的内存堆,Android还创建三个额外的内存堆:用来存放堆上内存被占用情况的位图索引"livebits"、在GC时用于标注存活对象的位图索引"markbits",和用来在GC中遍历存活对象引用的标注栈(Mark Stack)。
dvmHeapSourceStartup函数运行完成后,HeapSource、Heap、livebits、markbits以及mark stack等数据结构的关系如图 14 - 15所示。
图 14 - 15 GC堆上HeapSource、Heap等数据结构的关系
其中虚拟机通过一个名为gHs的全局HeapSource变量来操控GC内存堆,而HeapSource里通过heaps数组可以管理多个堆(Heap),以满足动态调整GC内存堆大小的要求。另外HeapSource里还维护一个名为"livebits"的位图索引,以跟踪各个堆(Heap)的内存使用情况。剩下两个数据结构"markstack"和"markbits"都是用在垃圾回收阶段,后面会讲解。
而dvmAllocRegion函数(/dalvik/vm/Misc.cpp:612)则通过ashmem和mmap两个系统调用分配内存地址空间,其中ashmem是Android系统对Linux的一个扩展,而mmap则是Linux系统提供的系统调用,请读者自行搜索参阅相关文档了解其用法。
这些步骤做完之后,一个Android应用的内存情况如图 14 - 16所示,虚线是应用实际申请的地址空间范围,而实线部分则是已经分配的内存:
图 14 - 16 GC向操作系统申请地址空间和内存
当需要应用需要分配内存,即通过"new"关键字创建一个实例时,在Android源码的过程大致如下:
首先虚拟机在执行Java class文件时,遇到"new "或" newarray"指令(所有的Java字节指令码请参考维基百科:),表示要创建一个对象或者数组的实例,这里为了简单起见,我们只看新建一个对象实例的情形。
虚拟机的JIT编译器执行"new"指令,针对不同的CPU架构,"new"指令都有相应的机器码与其对应,如ARM架构,JIT执行/dalvik/vm/mterp/armv5te/OP_NEW_INSTANCE.S中的机器码;而x86架构,则是/dalvik/vm/mterp/x86/OP_NEW_INSTANCE.S中的机器码。"OP_NEW_INSTANCE"函数的工作就是加载"new"指令的对象类型参数,获取对象需要占用的内存大小信息,然后调用"dvmAllocObject"分配必要的内存(/dalvik/vm/mterp/armv5te/OP_NEW_INSTANCE.S:29),当然还会处理必要的异常。
dvmAllocObject函数(/dalvik/vm/alloc/Alloc.cpp:181)调用dvmMalloc根据对象大小分配内存空间,成功后,调用对象的构造函数初始化实例(/dalvik/vm/alloc/Alloc.cpp:191)。
程序在运行的过程中不停的创建新的对象并消耗内存,直到GC内存用光,这时再要创建新对象时,就会触发GC线程启动垃圾回收过程,在Android源码中:
dvmMalloc函数(/dalvik/vm/alloc/Heap.cpp:333)直接将分配内存的工作委托给函数tryMalloc。
tryMalloc函数(/dalvik/vm/alloc/Heap.cpp:178)首先尝试用dvmHeapSourceAlloc函数分配内存,如果失败的话,唤醒或创建GC线程执行垃圾回收过程,并等待其完成后重试dvmHeapSourceAlloc(/dalvik/vm/alloc/Heap.cpp:201);如果dvmHeapSourceAlloc再次失败,说明当前GC堆中大部分对象都是存活的,那么调用dvmHeapSourceAllocAndGrow(/dalvik/vm/alloc/Heap.cpp:222)尝试扩大GC内存堆 & 前面说过,一开始GC堆会根据初始大小向操作系统申请保留一块内存,如果这块内存用完了,GC堆就会再次向操作系统申请一块内存,直到用完限额。
dvmMalloc函数根据内存分配是否成功来执行相应的操作,如内存分配失败时,抛出OOM(Out Of Memory)异常(/dalvik/vm/alloc/Heap.cpp:383)。
Android源码中垃圾回收过程大致如下:
dvmCollectGarbageInternal函数(/dalvik/vm/alloc/Heap.cpp:440)开始垃圾回收过程,其首先调用dvmSuspendAllThreads(/dalvik/vm/Thread.cpp:2539)暂停系统中除与调试器沟通的其他所有线程(/dalvik/vm/alloc/Heap.cpp:462);
如果没有启用并行GC的话,虚拟机会提高GC线程的优先级,以防止GC线程被其它线程占用CPU。
接下来调用dvmHeapMarkRootSet函数(/dalvik/vm/alloc/Heap.cpp:488)来遍历所有可从GC Root访问到的对象列表,dvmHeapMarkRootSet函数(/dalvik/vm/alloc/MarkSweep.cpp:181)的注释中也列出了GC Root列表。其调用dvmVisitRoot遍历GC Roots,代码清单14 - 1是dvmVisitRoot的源码(/dalvik/vm/alloc/Visit.cpp:212),笔者在其中以注释的方式批注关键代码。完整的GC Root列表有兴趣的读者可以参阅链接:。
代码清单14 - 7 在虚拟机中通过dvmVisitRoot遍历GC Roots
// visitor是一个回调函数,dvmHeapMarkRootSet传进来的是rootMarkObjectVisitors
// (位于/dalvik/vm/alloc/MarkSweep.cpp:145),这个回调函数的作用就是标注(Mark)
// 所有的GC root,并将它们的指针压入标注栈(Mark Stack)中。
// 第二个参数arg实际上是GcMarkContext对象,用于找到GC Roots后,回传给回调函数visitor
// 的参数。
void dvmVisitRoots(RootVisitor *visitor, void *arg)
assert(visitor != NULL);
// 所有已加载的类型都是GC Roots,这也意味着类型中所有的静态变量都是GC Roots
visitHashTable(visitor, gDvm.loadedClasses, ROOT_STICKY_CLASS, arg);
// 基本类型也是GC Roots,包括
// void, boolean, byte, short, char, int, long, float, double
visitPrimitiveTypes(visitor, arg);
// 调试器对象注册表中的对象(debugger object registry),这些对象
// 基本上是调试器创建的,因此不能把它们当作垃圾回收了,否则调试器
// 就无法正常工作了。
if (gDvm.dbgRegistry != NULL) {
visitHashTable(visitor, gDvm.dbgRegistry, ROOT_DEBUGGER, arg);
// 所有interned的字符串,interned string是虚拟机中保证的只有唯一一份拷贝的字符串
if (gDvm.literalStrings != NULL) {
visitHashTable(visitor, gDvm.literalStrings, ROOT_INTERNED_STRING, arg);
// 所有的JNI全局引用对象(JNI global references),JNI全局引用对象是
// JNI代码中,通过NewGlobalRef函数创建的对象
dvmLockMutex(&gDvm.jniGlobalRefLock);
visitIndirectRefTable(visitor, &gDvm.jniGlobalRefTable,, ROOT_JNI_GLOBAL, arg);
dvmUnlockMutex(&gDvm.jniGlobalRefLock);
// 所有的JNI局部引用对象(JNI local references)
// 关于JNI局部和全部变量的使用,可以参考下面的网页链接:
// http://journals.ecs.soton.ac.uk/java/tutorial/native1.1/implementing/refs.html
dvmLockMutex(&gDvm.jniPinRefLock);
visitReferenceTable(visitor, &gDvm.jniPinRefTable,, ROOT_VM_INTERNAL, arg);
dvmUnlockMutex(&gDvm.jniPinRefLock);
// 所有线程堆栈上的局部变量和其它对象,如线程本地存储里的对象等等
visitThreads(visitor, arg);
// 特殊的异常对象,如OOM异常对象需要在内存不够的时候创建,为了防止内存不够而无法创建
// OOM对象,因此虚拟机会在启动时事先创建这些对象。
(*visitor)(&gDvm.outOfMemoryObj,, ROOT_VM_INTERNAL, arg);
(*visitor)(&gDvm.internalErrorObj,, ROOT_VM_INTERNAL, arg);
(*visitor)(&gDvm.noClassDefFoundErrorObj,, ROOT_VM_INTERNAL, arg);
dvmHeapMarkRootSet是执行标注过程的主要代码,在前文说过,通常的实现会在对象实例前面放置一个对象头,里面会存放是否标注过的标志,而在Android系统里,采取的是分离式策略,而是将标注用的标志位放到HeapSource里的"markbits"这个位图索引结构,笔者猜测这么做的目的是为了节省内存。图 14 - 17是dvmHeapMarkRootSet函数快要标注完存活对象时(正在标注最后一个对象H),GC内存堆的数据结构。
图 14 - 17 GC执行完标注过程后的HeapSource结构
其中"livebits"位图索引还是维护堆上已用的内存信息;而"markbits"这个位图索引则指向存活的对象,在图 14 - 17中, A、C、F、G、H对象需要保留,因此"markbits"分别指向他们(最后的H对象尚在标注过程中,因此没有指针指向它);而"markstack"就是在标注过程中跟踪当前需要处理的对象要用到的标志栈了,此时其保存了正在处理的对象F、G和H。
在标注(Mark)过程中,调用dvmHeapScanMarkedObjects和dvmHeapProcessReferences函数(/dalvik/vm/alloc/MarkSweep.cpp:776)将实现了finalizer的对象添加到finalizer对象队列中,以便在下次GC中执行这些对象的finalize函数。
标识出所有的垃圾内存之后,调用dvmHeapSweepSystemWeaks和dvmHeapSweepUnmarkedObjects(/dalvik/vm/alloc/MarkSweep.cpp:902)等函数清理内存,但并不压缩内存,这是因为Android的GC是基于dlmalloc之上实现的,GC将所有的内存分配和释放的操作都转交给dlmalloc来处理。在这个过程中, Android系统不做压缩内存处理,据说是为了节省执行的CPU指令,从而达到延长电池寿命的目的,因此dvmCollectGarbageInternal做了一个小技巧,调用dvmHeapSourceSwapBitmaps函数(/dalvik/vm/alloc/Heap.cpp:575)将"livebits"和"markbits"的指针互换,这样就不需要在清理完垃圾对象后再次维护"livebits"位图索引了,如图 14 - 18所示:
图 14 - 18 GC清理完内存后堆上的数据结构
做完上面的操作之后,GC线程再通过dvmResumeAllThreads函数唤醒所有的线程(/dalvik/vm/alloc/Heap.cpp:624)。
虽然GC可以自动回收不再使用的内存,但有很多资源是虚拟机也无法管理的,如进程打开的数据库连接、网络端口以及文件等。针对这些资源,GC线程可以在垃圾回收过程中,标示出其是垃圾,需要释放,但是却不清楚如何释放它们,因此Java对象提供了一个名为finalize的函数,以便对象实现自定义的清除资源的逻辑。
如代码清单14 - 1是一个实现finalize函数的对象,在Java中,finalize对象定义在System.Object类中,即意味着所有对象都有这个函数,当子类重载了这个函数,即向虚拟机表明自己需要与其他类型区别对待。
代码清单14 - 8 实现finalize函数的简单对象
1&&&&class DemoClass {
2&&&& public int X;
4&&&& public void testMethod() {
5&&&&&&&& System.out.println("X: " + new Integer(X).toString());
8&&&& @Override
9&&&& protected void finalize () throws Throwable {
10&&&&&&&& System.out.println("finalize函数被调用了!");
11&&&&&&&& // 实现自定义的资源清除逻辑!
12&&&&&&&& super.finalize();
一些有C++编程经验的读者可能很容易将finalize函数与析构函数对应起来,但是两者是完全不同的东西,在C++中,调用了析构函数之后,对象就被释放了,然而在Java中,如果一个类型实现了finalize函数,其会带来一些不利影响,首先对象的存活周期会更长,至少需要两次垃圾回收才能销毁对象;第二对象同时会延长其所引用到的对象存活周期。如代码清单14 - 2中(示例代码javagc-simple)在第3行创建并使用了DemoClass以在内存中生成一些垃圾,并执行三次GC。
代码清单14 - 9 实现finalize函数的简单对象
1&&&&public class gcdemo {
2&&&& public static void main(String[] args) throws Exception {
3&&&&&&&& generateGarbage();
4&&&&&&&& System.gc();
5&&&&&&&& Thread.sleep(1000);
7&&&&&&&& System.gc();
8&&&&&&&& Thread.sleep(1000);
10&&&&&&&& System.gc();
11&&&&&&&& Thread.sleep(1000);
14&&&& public static void generateGarbage() {
15&&&&&&&& DemoClass g = new DemoClass();
16&&&&&&&& g.X =123;
17&&&&&&&& g.testMethod();
连接好设备,打开logcat日志,并执行示例代码根目录中的run.sh,得到的输出类似图 14 - 8,每一行输出对应代码清单14 - 2中的一次System.gc调用,可以看到第一次GC过程中释放了223个对象,如果运行示例程序javagc,会发现第一次GC之后,DemoClass的finalize函数就会被调用 & 为了避免System.out.println中的字符串对象影响GC的输出,图 14 - 8是javagc-simple的输出结果。第二次GC过程中又释放了34个对象,其中就有DemoClass的实例,以及其所引用到的其它对象。这时所有垃圾对象都被回收了,因此在执行第三次GC过程时,没有回收到任何内存。
图 14 - 19 程序中使用了实现finalize函数对象之后实施三次GC的结果
前文讲到Android源码中通过dvmHeapScanMarkedObjects函数在GC堆上扫描垃圾对象,并将finalizable对象添加到finalize队列中,其具体过程如下:
dvmHeapScanMarkedObjects函数(/dalvik/vm/alloc/MarkSweep.cpp:595)将所有识别出来的可以被GC Root引用的对象放到名为"mark stack"的堆栈中,再调用processMarkStack函数处理需要特殊处理的对象。
processMarkStack函数(/dalvik/vm/alloc/MarkSweep.cpp:471)调用scanObject函数处理"mark stack"中的每个对象。
scanObject函数(/dalvik/vm/alloc/MarkSweep.cpp:454)首先判断对象是保存Java类型信息的类型对象,还是数组对象,还是普通的Java对象,针对这三种对象进行不同的处理。由于finalize对象是普通的Java对象,因此这里我们只看相应的scanDataObject函数。
scanDataObject函数(/dalvik/vm/alloc/MarkSweep.cpp:438)先扫描对象的各个成员,并标记其所有引用到的对象,最后调用delayReferenceReferent函数根据对象的类型,将其放入相应的待释放队列中,如对象是fianlizeable对象的话,则放入finalizerReferences队列中(/dalvik/vm/alloc/MarkSweep.cpp:426);如对象是WeakReference对象的话,则将其放入weakReferences队列中(/dalvik/vm/alloc/MarkSweep.cpp:424)。
dvmHeapProcessReferences函数(/dalvik/vm/alloc/MarkSweep.cpp#776)在垃圾对象收集完毕后,负责将finalize队列从虚拟机的native端传递到Java端。其调用enqueueFinalizerReferences函数通过JNI方式将finalize对象的引用传递到Java端的一个java.lang.ref.ReferenceQueue当中,详细的调用方式请参见enqueueFinalizerReferences函数(/dalvik/vm/alloc/MarkSweep.cpp:729)和enqueueReference函数(/dalvik/vm/alloc/MarkSweep.cpp:653)。
而在JVM虚拟机启动时,dvmStartup函数(/dalvik/vm/Init.cpp:1557)会在准备好Java程序运行所需的所有环境之后,调用dvmGcStartupClasses函数(/dalvik/vm/alloca/Alloc.cpp:71)启动几个与GC相关的后台Java线程 ,这些线程在java.lang.Daemons中定义(/libcore/luni/src/main/java/java/lang/Daemons.java),其中一个线程就是执行java对象finalize函数的HeapWorker线程,之所以要将收集到的java finalize对象引用从虚拟机(native)一端传递到Java端,是因为finalize函数是由java语言编写的,函数里可能会用到很多java对象。这也是为什么如果对象实现了finalize函数,不仅会使其生命周期至少延长一个GC过程,而且也会延长其所引用到的对象的生命周期,从而给内存造成了不必要的压力。
阅读(...) 评论()}

我要回帖

更多关于 android ion内存管理 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信