内存管理基础
Java内存分配模型
Java的对象生命周期
1.创建
为对象分配内存空间 构造对象
2.应用
此时 对象至少被一个强引用持有
3.不可见阶段
没有强引用,没有任何引用了, 对象此时还是存在内存里面的,如果被gc扫描到了,就会被后面的可达性分析,如果不可达了就会被回收
4.不可达
5.收集
6.终结 垃圾回收
7.对象空间重新分配
对象的内存布局
可回收对象的判定
Java的四种引用
- 1.强引用(Strong Reference)
在代码中普遍使用的,类似Person person = new Person();如果一个对象具有强引用,则无论在什么情况下,GC都不会回收被引用的对象。当内存空间不足时,JAVA虚拟机宁可抛出OutOfMemoryError终止应用程序也不会回收具有强引用的对象。
2.软引用(Soft Reference)
表示一个对象处在有用但非必须的状态。如果一个对象具有软引用,在内存空间充足时,GC就不会回收该对象,当内存空间不足时,GC会回收该对象的内存(回收发生在OutOfMemoryError之前)
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被GC回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中,以便在恰当的时候将该软引用回收,但是由于GC线程的优先级较低,通常手动调用System.gc()并不能立即执行GC,因此弱引用所引用的对象并不一定会被马上回收。
3.弱引用(Weak Reference)
用来描述非必须的对象,它类似软引用,但是强度比软引用更弱一些,弱引用具有更短的生命,GC在扫描的过程中,一旦发现只具有被弱引用关联的对象,都会回收掉被弱引用关联的对象,无论当前内存是否紧缺,GC都将回收被弱引用关联的对象。
4.虚引用(Phantom Reference)
虚引用等同于没有引用,这意味着在任何时候都可能被GC回收,设置虚引用的目的是为了被虚引用关联的对象在被垃圾回收器时,能够收到一个系统通知。(被用来跟踪对象被GC回收的活动)虚引用和弱引用的区别在于,虚引用在使用时必须和引用队列(ReferenceQueue)联合使用,其在GC回收期间的活动如下:
ReferenceQueue queue = new ReferenceQueue();
PhantomReference pr = new PhantomReference(object.quque);
也即是GC在回收一个对象时,如果发现该对象具有虚引用,,那么在回收之前会首先该对象的虚引用加入到与之关联的引用队列中,程序可以通过判断引用队列中是否已经加入虚引用来了解被引用的对象是否被GC回收。
GC垃圾回收算法
-
标记清除算法
特点
位置不连续,产生碎片
效率略低
两遍扫描
会扫描两遍,第一遍哪些对象是存活的,哪些是没使用的内存空间,哪些是可回收的,标记出来,第二次扫描之后就把可回收的全部给回收掉
缺点:多次清除后,内存就会千疮百孔,产生大量的内存碎片
- 标记整理算法
特点
没有内存碎片
效率偏低
两遍扫描、指针需要调整
- 复制算法
特点
位置不连续,产生碎片
效率略低
利用率只有一半
分代收集算法 综合运用
新生代 采用复制算法
老年代 对象存活久 不容易产生垃圾 标记整理算法,虽然效率慢点,回收频率不需要那么高。
App内存组成以及限制
Android 给每个App分配一个VM,让App运行在dalvik上,这样即使App崩溃也不会影响到系统,系统给VM分配了一定的内存大小,App可以申请使用的内存大小不能超过此硬性逻辑限制,就算物理内存富余,如果应用超出VM最大内存,就会出现内存溢出crash。
由程序控制操作的内存空间在heap上,分 java heapsize 和native heapsize
Java申请的内存在vm heap上,所以如果java申请的内存大小超过VM的逻辑内存限制,就会出现内存溢出的异常。
native层内存申请不受其限制,native层收native process 对内存大小的限制
通过代码获取内存大小的限制
ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE) activityManager.getMemoryClass();//以m为单位
Android内存分配与回收机制
- 内存分配
Android的Heap空间是一个 Generational Heap Memory 的模型,最近分配的对象会存放在Young Generation区域,当一个对象在这个区域停留的时间达到一定程度,它会被移动到old Generation,最后累积一定时间再移动到Permanent Generation区域。
1、Young Generation
由一个Eden区和两个Survivor区组成,程序中生成的大部分新的对象都在Eden区中,当Eden区满时,还存活的对象将被复制到其中一个Survivor区,当Survivor区满时,此区存活的对象又被复制到另一个Survivor区,当这个Survivor区也满时,会将其中存活的对象复制到老年代。
2、Old Generation
一般情况下,老年代中的对象生命周期都比较长。
3、Permanent Generation
用于存放静态的类和方法,持久代对垃圾回收没有显著影响。
总结:内存对象的处理过程如下
- 1.对象创建后在Eden区
2.执行GC后,如果对象仍然存活,则复制到SO区
3.当SO区满时,该区域存活对象将复制到S1区,然后SO清空,接下来SO和S1角色互换。
4.当第3步达到一定次数(系统版本不同会有差异)后,存活对象将被复制到Old Generation。
5.当这个对象再Old Generation区域停留的时间达到一定程度时,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域
系统在Young Generation、Old Generation上采用不同的回收机制,每一个Generation的内存区域都有固定的大小,随着新的对象陆续被分配到此区域,当对象总的大小临近这一级别内存区域的阀值时,会触发GC操作,以便腾出空间来存放其它新的对象。
执行GC占用的时间与Generation和Generation中的对象数量有关:
Young Generation < Old Generation < Permanent Generation
Gener中的对象数量与执行时间成正比
4、Young Generation GC
由于其对象存活时间短,因此基于Copying算法(扫描出存活的对象,并复制到一块新的完全未使用的控件中)来回收,新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在Young Generation区间的位置,当有新的对象要分配内存时,用于检测空间是否足够,不够就触发GC。
5、Old Generation GC
由于其对象存活时间较长,比较稳定,因此采用Mart(标记)算法(扫描出存活的对象,然后再回收未标记的对象,回收后对空出的空间要么合并,要么标记出来便于下次分配,以减少内存碎片带来的效率损耗)来回收。
GC类型
在Android系统中,GC有三种类型:
kGcCauseForAlloc:分配内存不够引起的GC,会Stop World,由于是并发GC,其它线程都会停止,知道GC完成。
kGcCauseBackground:内存达到一定阈值触发的GC,由于是一个后台GC,所以不会引起Stop World。
kGcCauseExplicit:显示调用时进行的GC,当ART打开这个选项时,使用System.gc时会进行GC。
可达性分析与GCRoots
在Java,可作为GC Roots的对象包括:
1.方法区:类静态属性引用的对象;
2.方法区:常量引用的对象
3.虚拟机栈(本地变量表)中引用的对象
4.本地方法栈JNI(Native方法)中引用的对象
Android低内存杀进程机制
低内存终止守护进程(LMK)
LMK 使用一个名为 oom_adj_score 的“内存不足”分值来确定正在运行的进程的优先级,以此决定要终止的进程。最高得分的进程最先被终止。后台应用最先被终止,系统进程最后被终止。下表列出了从高到低的 LMK 评分类别。评分最高的类别,即第一行中的项目将最先被终止
-
Android 进程,高分在上,低分在下
后台应用:之前运行过且当前不处于活动状态的应用,LMK将首先从具有最高oom_adj_score的应用终止后台应用。
上一个应用:最近用过的后台应用,上一个应用比后台应用具有更高的优先级(得分更低),因为相比某个后台应用,用户更有可能切换到上一个应用。
主屏幕应用:这是启动器应用,终止该应用会使壁纸消失。
服务:服务由应用启动,可能包括同步或上传到云端。
可觉察的应用,用户可通过某种方式察觉到的非前台应用,例如运行一个显示小界面的搜索进程或听音乐。
前台应用:当前正在使用的应用,终止前台应用看起来就像应用崩溃了,可能会向用户提示设备出了问题。
持久性(服务):这些是设备的核心服务,手机可能看起来即将重新启动。
原生:系统使用的极低级别的进程
设备制造商可以更改LMK的行为。
系统需要进行内存回收时最先回收空进程,然后是后台进程,以此类推最后才会回收前台进程(一般情况 下前台进程就是与用户交互的进程了,如果连前台进程都需要回收那么此时系统几乎不可用了
ActivityManagerService会对所有进程进行评分(存放在变量adj中),然后再讲这个评分更新到内核,由内核去完成真正的内存回收(lowmemorykiller,oom_killer),这里只是大概的流程,中间过程还是很复杂的。
什么是OOM
OOM(OutOfMemoryError)内存溢出错误,在常见的Crash疑难排行榜上,OOM绝对可以名列前茅并且经久不衰,因为它发生时的Crash堆栈信息往往不是导致问题的根本原因,而只是压死骆驼的最后一根稻草。
Android 2.x系统 GC LOG中的dalvik allocated + external allocated + 新分配的大小 >= getMemoryClass()值的时候就会发生OOM。 例如,假设有这么一段Dalvik输出的GC LOG: GC_FOR_MALLOC free 2K, 13% free 32586K/37455K, external 8989K/10356K, paused 20ms, 那么32586+8989+(新分配23975)=65550>64M时,就会发生OOM。
Android 4.x系统 Android 4.x的系统废除了external的计数器,类似bitmap的分配改到dalvik的 java heap中申请,只要allocated + 新分配的内存 >= getMemoryClass()的时候就会发生OOM
OOM原因分类
OOM代码分析
Android虚拟机最终抛出OutOfMemoryError的地方
/art/runtime/thread.cc
void Thread::ThrowOutOfMemoryError(const char* msg) { LOG(WARNING) << StringPrintf("Throwing OutOfMemoryError "%s"%s",
msg, (tls32_.throwing_OutOfMemoryError ? " (recursive case)" : "")); if (!tls32_.throwing_OutOfMemoryError) {
tls32_.throwing_OutOfMemoryError = true; ThrowNewException("Ljava/lang/OutOfMemoryError;", msg); tls32_.throwing_OutOfMemoryError = false;
} else {
Dump(LOG_STREAM(WARNING)); // The pre-allocated OOME has no stack, so
help out and log one.
SetException(Runtime::Current()->GetPreAllocatedOutOfMemoryError()); }
}
堆内存分配失败
/art/runtime/gc/heap.cc
void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type) {
// If we're in a stack overflow, do not create a new exception. It would require running the
// constructor, which will of course still be in a stack overflow.
if (self->IsHandlingStackOverflow()) { self->SetException(
Runtime::Current()- >GetPreAllocatedOutOfMemoryErrorWhenHandlingStackOverflow());
return; }
std::ostringstream oss; size_t total_bytes_free = GetFreeMemory();
//为对象分配内存时达到进程的内存上限
oss << "Failed to allocate a " << byte_count << " byte allocation with "
<< total_bytes_free << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << "
until OOM," << " target footprint " <<
target_footprint_.load(std::memory_order_relaxed) << ", growth limit "
<< growth_limit_;
//没有足够大小的连续地址空间
// There is no fragmentation info to log for large-object space.
if (allocator_type != kAllocatorTypeLOS) { CHECK(space != nullptr) << "allocator_type:" << allocator_type
<< " byte_count:" << byte_count
<< " total_bytes_free:" << total_bytes_free; space->LogFragmentationAllocFailure(oss, byte_count);
} }
创建线程失败
/art/runtime/thread.cc
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
CHECK(java_peer != nullptr);
Thread* self = static_cast<JNIEnvExt*>(env)->GetSelf();
// TODO: remove from thread group?
env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, 0);
{
std::string msg(child_jni_env_ext.get() == nullptr ?
StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) : StringPrintf("pthread_create (%s stack) failed: %s",
PrettySize(stack_size).c_str(), strerror(pthread_create_result)));
ScopedObjectAccess soa(env);
soa.Self()->ThrowOutOfMemoryError(msg.c_str()); }
内存三大问题
- 1.内存抖动
短时间内有大量对象创建和销毁,它伴随着频繁的GC,内存波动图形呈齿装、GC导致卡顿
例如在自定义view里面onDraw方法中多次创建对象
如何预防?
1)避免在循环中创建对象
2)避免在频繁调用的方法中创建对象,如view的onDraw方法
3)允许复用的情况下,使用对象池进行缓存,如:Handler的Message单链表(obtain),
Message.obtain();
2.内存溢出
内存不够用了,垃圾回收又回收不了
3.内存泄漏
这个对象已经不再使用了,但是还被GC Roots引用,导致不能回收。
程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。
长生命周期对象持有短生命周期对象的强引用,从而导致短生命周期对象无法被回收
Android内存泄漏常见场景以及解决方案
- 1.资源性对象未关闭
对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null,例如Bitmap等资源未关闭会造成内存泄漏,此时我们应该在Activity销毁时及时关闭
2.注册对象未注销
例如BroadcastReceiver、EventBus未注销造成的内存泄漏,我们应该在Activity销毁时及时注销
3.类的静态变量持有大数据对象
尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储
4.单例造成的内存泄漏
优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可。
5.非静态内部类的静态实例
该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源不能正常回收。此时,我们可以将该内部类设为静态内部类或将内部类抽取出来封装成一个单例,如果需要使用Context,尽量使用Application Context,如果需要使用Activity Context,就记得用完后置空让GC可以回收,否则还是会内存泄漏。
6、Handler临时性内存泄漏
Message发出之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的,则会导致Activity或者Service不会被回收,并且消息队列是在一个Looper线程中不断地轮询处理消息,当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message持有Handler实例的引用,Handler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏,解决方案如下所示:
1.使用一个静态Handler内部类,然后对Handler持有的对象(一般是Activity)使用弱引用,这样在回收时,也可以回收Handler持有的对象。
2.在Activity的Destory或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中有待处理的消息需要处理。
7、容器中的对象没清理造成的内存泄漏
在退出程序之前,将集合里的东西clear,然后置为null,再退出程序
8、WebView
WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉,我们可以为WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。
9、使用ListView时造成的内存泄漏
在构造Adapter时,使用缓存的convertView
另外可参考android官网管理应用内存的部分https://developer.android.com/topic/performance/memory?hl=zh_cn