android 内存泄漏底层可以多给一个应用分配内存吗

android内存溢出 - 移动开发记录 - ITeye技术网站
Android有效解决加载大图片时内存溢出的问题
尽量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource来设置一张大图,
因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存。
因此,改用先通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的 source,
decodeStream最大的秘密在于其直接调用JNI&&nativeDecodeAsset()来完成decode,
无需再使用java层的createBitmap,从而节省了java层的空间。
如果在读取时加上图片的Config参数,可以跟有效减少加载的内存,从而跟有效阻止抛out of Memory异常
另外,decodeStream直接拿的图片来读取字节码了, 不会根据机器的各种分辨率来自动适应,
使用了decodeStream之后,需要在hdpi和mdpi,ldpi中配置相应的图片资源,
否则在不同分辨率机器上都是同样大小(像素点数量),显示出来的大小就不对了。
另外,以下方式也大有帮助:
1. InputStream is = this.getResources().openRawResource(R.drawable.pic1);
BitmapFactory.Options options=new BitmapFactory.Options();
options.inJustDecodeBounds =
options.inSampleSize = 10; //width,hight设为原来的十分一
Bitmap btp =BitmapFactory.decodeStream(is,null,options);
2. if(!bmp.isRecycle() ){
bmp.recycle() //回收图片所占的内存
system.gc()//提醒系统及时回收
以下奉上一个方法:
* 以最省内存的方式读取本地资源的图片
* @param context
* @param resId
public static Bitmap readBitMap(Context context, int resId){
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inPreferredConfig = Bitmap.Config.RGB_565;
opt.inPurgeable =
opt.inInputShareable =
//获取资源图片
InputStream is = context.getResources().openRawResource(resId);
return BitmapFactory.decodeStream(is,null,opt);
================================================================================
Android内存溢出的解决办法
转自:/iuranus/archive//124394.html?opt=admin
昨天在模拟器上给gallery放入图片的时候,出现java.lang.OutOfMemoryError: bitmap size exceeds VM budget 异常,图像大小超过了RAM内存。
模拟器RAM比较小,只有8M内存,当我放入的大量的图片(每个100多K左右),就出现上面的原因。
由于每张图片先前是压缩的情况,放入到Bitmap的时候,大小会变大,导致超出RAM内存,具体解决办法如下:
//解决加载图片 内存溢出的问题
//Options 只保存图片尺寸大小,不保存图片到内存
BitmapFactory.Options opts = new BitmapFactory.Options();
//缩放的比例,缩放是很难按准备的比例进行缩放的,其值表明缩放的倍数,SDK中建议其值是2的指数值,值越大会导致图片不清晰
opts.inSampleSize = 4;
Bitmap bmp =
bmp = BitmapFactory.decodeResource(getResources(), mImageIds[position],opts);
bmp.recycle();
通过上面的方式解决了,但是这并不是最完美的解决方式。
通过一些了解,得知如下:
优化Dalvik虚拟机的堆内存分配
对于Android平台来说,其托管层使用的Dalvik Java VM从目前的表现来看还有很多地方可以优化处理,比如我们在开发一些大型游戏或耗资源的应用中可能考虑手动干涉GC处理,使用 dalvik.system.VMRuntime类提供的setTargetHeapUtilization方法可以增强程序堆内存的处理效率。当然具体原理我们可以参考开源工程,这里我们仅说下使用方法: private final static float TARGET_HEAP_UTILIZATION = 0.75f; 在程序onCreate时就可以调用
VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION); 即可。
Android堆内存也可自己定义大小
对于一些Android项目,影响性能瓶颈的主要是Android自己内存管理机制问题,目前手机厂商对RAM都比较吝啬,对于软件的流畅性来说RAM对性能的影响十分敏感,除了 优化Dalvik虚拟机的堆内存分配外,我们还可以强制定义自己软件的对内存大小,我们使用Dalvik提供的 dalvik.system.VMRuntime类来设置最小堆内存为例:
private final static int CWJ_HEAP_SIZE = 6*
VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE); //设置最小heap内存为6MB大小。当然对于内存吃紧来说还可以通过手动干涉GC去处理
bitmap 设置图片尺寸,避免 内存溢出 OutOfMemoryError的优化方法
★android 中用bitmap 时很容易内存溢出,报如下错误:Java.lang.OutOfMemoryError : bitmap size exceeds VM budget
● 主要是加上这段:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
● eg1:(通过Uri取图片)
private ImageV
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一
Bitmap bitmap = BitmapFactory.decodeStream(cr
.openInputStream(uri), null, options);
preview.setImageBitmap(bitmap);
以上代码可以优化内存溢出,但它只是改变图片大小,并不能彻底解决内存溢出。
● eg2:(通过路径去图片)
private ImageV
private String fileName= "/sdcard/DCIM/Camera/ 16.01.44.jpg";
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一
Bitmap b = BitmapFactory.decodeFile(fileName, options);
preview.setImageBitmap(b);
filePath.setText(fileName);
★Android 还有一些性能优化的方法:
●首先内存方面,可以参考 Android堆内存也可自己定义大小 和 优化Dalvik虚拟机的堆内存分配
●基础类型上,因为Java没有实际的指针,在敏感运算方面还是要借助NDK来完成。Android123提示游戏开发者,这点比较有意思的是Google 推出NDK可能是帮助游戏开发人员,比如OpenGL ES的支持有明显的改观,本地代码操作图形界面是很必要的。
●图形对象优化,这里要说的是Android上的Bitmap对象销毁,可以借助recycle()方法显示让GC回收一个Bitmap对象,通常对一个不用的Bitmap可以使用下面的方式,如
if(bitmapObject.isRecycled()==false) //如果没有回收
bitmapObject.recycle();
●目前系统对动画支持比较弱智对于常规应用的补间过渡效果可以,但是对于游戏而言一般的美工可能习惯了GIF方式的统一处理,目前Android系统仅能预览GIF的第一帧,可以借助J2ME中通过线程和自己写解析器的方式来读取GIF89格式的资源。
● 对于大多数Android手机没有过多的物理按键可能我们需要想象下了做好手势识别 GestureDetector 和重力感应来实现操控。通常我们还要考虑误操作问题的降噪处理。
Android堆内存也可自己定义大小
对于一些大型Android项目或游戏来说在算法处理上没有问题外,影响性能瓶颈的主要是Android自己内存管理机制问题,目前手机厂商对RAM都比较吝啬,对于软件的流畅性来说RAM对性能的影响十分敏感,除了上次Android开发网提到的优化Dalvik虚拟机的堆内存分配外,我们还可以强制定义自己软件的对内存大小,我们使用Dalvik提供的 dalvik.system.VMRuntime类来设置最小堆内存为例:
private final static int CWJ_HEAP_SIZE = 6*
VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE); //设置最小heap内存为6MB大小。当然对于内存吃紧来说还可以通过手动干涉GC去处理,我们将在下次提到具体应用。
优化Dalvik虚拟机的堆内存分配
对于Android平台来说,其托管层使用的Dalvik JavaVM从目前的表现来看还有很多地方可以优化处理,比如我们在开发一些大型游戏或耗资源的应用中可能考虑手动干涉GC处理,使用 dalvik.system.VMRuntime类提供的setTargetHeapUtilization方法可以增强程序堆内存的处理效率。当然具体原理我们可以参考开源工程,这里我们仅说下使用方法: private final static floatTARGET_HEAP_UTILIZATION = 0.75f; 在程序onCreate时就可以调用
VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION); 即可
Android虽然会自动管理内存,JAVA也有garbage collection (GC )内存回收机制。
但是如果程序在一次操作中打开几个M的文件,那么通常会出现下面的错误信息。
02-04 21:46:08.703: ERROR/dalvikvm-heap(2429): 1920000-byte external allocation too large for this process.
02-04 21:52:28.463: ERROR/AndroidRuntime(2429): java.lang.OutOfMemoryError: bitmap size exceeds VM budget
移动终端因为内存有限,往往图片处理经常出现上述的错误。
解决方法:
1.明确调用System.gc();
这种内存回收会有一定的作用,但是请不要太期待。
2.图片处理完成后回收内存。
请在调用BitMap进行图片处理后进行内存回收。
bitmap.recycle();
这样会把刚刚用过的图片占用的内存释放。
3.图片处理时指定大小。
下面这个方法处理几个M的图片时是必须的
1. BitMapgetBitpMap(){
2. ParcelFileD
4. pfd=mCon.getContentResolver().openFileDescriptor(uri,"r");
5. }catch(IOExceptionex){
8. java.io.FileDescriptorfd=pfd.getFileDescriptor();
9. BitmapFactory.Optionsoptions=newBitmapFactory.Options();
10. //先指定原始大小
11. options.inSampleSize=1;
12. //只进行大小判断
13. options.inJustDecodeBounds=
14. //调用此方法得到options得到图片的大小
15. BitmapFactory.decodeFileDescriptor(fd,null,options);
16. //我们的目标是在800pixel的画面上显示。
17. //所以需要调用computeSampleSize得到图片缩放的比例
18. options.inSampleSize=computeSampleSize(options,800);
19. //OK,我们得到了缩放的比例,现在开始正式读入BitMap数据
20. options.inJustDecodeBounds=
21. options.inDither=
22. options.inPreferredConfig=Bitmap.Config.ARGB_8888;
24. //根据options参数,减少所需要的内存
25. BitmapsourceBitmap=BitmapFactory.decodeFileDescriptor(fd,null,options);
26. returnsourceB
28. //这个函数会对图片的大小进行判断,并得到合适的缩放比例,比如2即1/2,3即1/3
29. staticintcomputeSampleSize(BitmapFactory.Optionsoptions,inttarget){
30. intw=options.outW
31. inth=options.outH
32. intcandidateW=w/
33. intcandidateH=h/
34. intcandidate=Math.max(candidateW,candidateH);
35. if(candidate==0)
36. return1;
37. if(candidate&1){
38. if((w&target)&&(w/candidate)&target)
39. candidate-=1;
41. if(candidate&1){
42. if((h&target)&&(h/candidate)&target)
43. candidate-=1;
45. if(VERBOSE)
46. Log.v(TAG,"forw/h"+w+"/"+h+"returning"+candidate+"("+(w/candidate)+"/"+(h/candidate));
///////////////////////////////////////////////////////
///////////////////////////////////////////////////////
android系统的手机在系统底层指定了堆内存的上限值,大部分手机的缺省值是16MB,不过也有些高配置的机型是24MB的,所以我们的程序在申请内存空间时,为了确保能够成功申请到内存空间,应该保证当前已分配的内存加上当前需要分配的内存值的总大小不能超过当前堆的最大内存值,而且内存管理上将外部内存完全当成了当前堆的一部分,也就是说Bitmap对象通过栈上的引用来指向堆上的Bitmap对象,而堆上的Bitmap对象又对应了一个使用了外部存储的native图像,也就是实际上使用的字节数组byte[]来存储的位图信息,因此解码之后的Bitmap的总大小就不能超过8M了。
解决这类问题的最根本的,最有效的办法就是,使用完bitmap之后,调用bitmap对象的recycle()方法释放所占用的内存,以便于下一次使用。
下面是网上找到的一些常用的优化办法,但是基本上都不能从本质上解决问题。
1.设置系统的最小堆大小:
int newSize = 4 * 1024 * 1024 ; //设置最小堆内存大小为4MB
VMRuntime.getRuntime().setMinimumHeapSize(newSize);
VMRuntime.getRuntime().setTargetHeapUtilization(0.75); // 设置堆内存的利用率为75%
补充说明:堆(HEAP)是VM中占用内存最多的部分,通常是动态分配的。堆的大小不是一成不变的,当堆内存实际的利用率偏离设定的值的时候,虚拟机会在GC的时候调整堆内存大小,让实际占用率向个百分比靠拢。比如初始的HEAP是4M大小,当4M的空间被占用超过75%的时候,重新分配堆为8M大;当8M被占用超过75%,分配堆为16M大。倒过来,当16M的堆利用不足30%的时候,缩减它的大小为8M大。重新设置堆的大小,尤其是压缩,一般会涉及到内存的拷贝,所以变更堆的大小对效率有不良影响。
2.对图片的大小进行控制
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2; //图片宽高都为原来的二分之一,即图片为原来的四分之一
Bitmap bitmap = BitmapFactory.decodeFile("/mnt/sdcard/a.jpg",options);
补充说明:这种方法只是对图片做了一个缩放处理,降低了图片的分辨率,在需要保证图片质量的应用中不可取。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inTempStorage = new byte[]; //5MB的临时存储空间
Bitmap bm = BitmapFactory.decodeFile("/mnt/sdcard/a.jpg",options);
补充说明:从创建Bitmap的C++底层代码BitmapFactory.cpp中的处理逻辑来看,如果option不为null的话,那么会优先处理option中设置的各个参数,假设当前你设置option的inTempStorage为(4M)大小的话,而且每次解码图像时均使用该option对象作为参数,那么你的程序极有可能会提前失败,经过测试,如果使用一张大小为1.03M的图片来进行解码,如果不使用option参数来解码,可以正常解码四次,也就是分配了四次内存,而如果使用option的话,就会出现内存溢出错误,只能正常解码两次。Options类有一个预处理参数,当你传入options时,并且指定临时使用内存大小的话,Android将默认先申请你所指定的内存大小,如果申请失败,就会先抛出内存溢出错误。而如果不指定内存大小,系统将会自动计算,如果当前还剩3M空间大小,而解码只需要2M大小,那么在缺省情况下将能解码成功,而在设置inTempStorage大小为4M的情况下就将出现内存溢出错误。所以,通过设置Options的inTempStorage大小也不能从根本上解决大图像解码的内存溢出问题。
总之再做android开发时,出现内存溢出是属于系统底层限制,只要解码需要的内存超过系统可分配的最大内存值,那么内存溢出错误必然会出现.
yidongkaifa
浏览: 415944 次
废话啊啊啊
iOS: 当发生signal 9为 kill的时候,程序直接被 ...
给的地址,没豆子呢,能单独发一份给我吗,andsy2008@1 ...
[b][i][u]引用[list]
[*][flash=200 ...一般来说,程序使用内存的方式遵循先向操作系统申请一块内存,使用内存,使用完毕之后释放内存归还给操作系统。然而在传统的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内存优化 的文章

 

随机推荐