目录:
1.为什么要优化内存,什么原因导致你优化内存问题
2.内存分析的思想 ,内存优化实例
3.内存泄漏产生的原因, leaycanery的原理
4. 内存抖动的分析和案例
5. 内存OOM分析和案例
6. 实战内存分析工具:profile ,hprof文件分析。MAT用法,****Allocation Tracker(用的少),Heap Viewer
1).profile是如何检查内存泄露,又是如何检测大内存
2).MAT是如何精准定位的,hprof文件需要做什么处理,MAT分析内存泄露,有享学的视频
3).Allocation Tracker在哪个位置,DDMS怎么打开,工作原理
7.快手的KOOM工具, 内存如何线上监控?(重点 )
8. 给你一个APP, 如何快速进行内存优化(重点)
9.腾讯的Matrix原理
1.为什么要优化内存,什么原因导致你优化内存问题
1.) 公司定的内存指标,峰值,静态,前台和后台
- .后台被杀死 ,因为跑步在后台运行,内存越高越容易被系统杀死。就像进程有个优先级,adj-oom的值越低,越不容易被杀死!!!
优化指标: 整个系统内存占用比较高, 系统策略会把你杀死! LMK 机制 ,需要统计内存的峰值, 平均值! 静态内存,动态内存! 提高应用后台运行时的存活率。
3).bugly上的oom
4).bugly上TimeOut问题
5). 卡顿, 内存抖动导致GC
2.内存分析的思想 ,内存优化实例
思路一:
第一步:计算,和优化指标对比, 为了看优化的效果如何
第二步 3大内存杀手, 需要用工具分析才行
2.1)内存泄漏-------->看详细博客
2.2). OOM------------>B**itmap: **分配及回收追踪 这样总体可用减少OOM,内存溢出的问题
OOM主要是大内存分析 静态内存分析和动态内存分析,主要是图片优化
2.3)内存抖动-------->看详细博客(线上一般监控不出来! )
第三步). 优化之后可以总结:
1>.减少内存
2>.复用内存
3>.回收内存
第四步: >最重要的: 预防和线上监控
5大监测:
1).监测泄漏
2).监测大图
3).监测GC事件
4).监控线程
5). IO监控
内存泄漏 解决方案分析 产生原因 工具分析 实例 leakcanary 原理和Profier分析
内存OOM 解决方案分析 产生原因 工具分析 实例 KOOM
内存抖动 解决方案分析 产生原因 工具分析 实例 profile工具
思路二: KOOM
3.内存泄漏产生的原因, leaycanery的原理
3.1 什么是内存泄露
Java内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到gc roots导致无法被GC回收。
3.2 内存泄露分析工具: leaycanery原理
LeakCanary 的核心也是解决内存问题常见的三板斧:监控泄漏、采集镜像、分析镜像。
简单原理
:
第一步: 监控泄露:
1). LeakCanary通过ApplicationContext统一注册监听的方式,通过application.registerActivityLifecycleCallbacks来绑定Activity生命周期的监听,从而监控所有Activity; (监控生命周期)
2).在Activity执行onDestroy时,开始检测当前页面是否存在内存泄漏,并分析结果。(触发入口)
3).WeakReference与ReferenceQueue联合使用,在弱引用关联的对象被回收后,会将引用添加到ReferenceQueue;清空后,可以根据是否继续含有该引用来判定是否被回收;判定回收 (真正的原理)
4).手动GC, 再次判定回收,采用双重判定来确保当前引用是否被回收的状态正确性;如果两次都未回收,则确定为泄漏对象。(确保一直性)
第二步: 采集镜像
5).dump信息,得到hprof文件
第三步: 采集镜像
6).开进程分析文件,得到引用关系链
检测内存泄露的核心原理: ****引用队列可以配合弱引用使用,引用的对象将要被JVM回收时,会将其加入到引用队列中。
监测机制利用了Java的WeakReference和ReferenceQueue,弱引用和引用队列
ReferenceQueue referenceQueue = new ReferenceQueue<Object>();
Object object = new Object();
WeakReference<Object> weakReference = new WeakReference<Object>(object, referenceQueue);
如果当GC过后引用对象仍然不被加入ReferenceQueue中,不存在说明内存泄漏了。 存在说明没有泄漏
3.3 leaycanery优化: ResourceCanary
LeakCanary缺点
1). LeakCanary也有一定的不确定性,一般同一个地方反复泄漏5次,算是一个泄漏,
2). 不能线上操作, 主动触发 GC 造成 App 卡顿
3). 没有聚类, 不够自动化
3.4 内存泄露的案例
非静态内部类创建静态实例造成
public class MainActivity extends AppCompatActivity {
public void onCreate() {
// ...
MyListener listener = new MyListener() {
// ...
};
// ...
}
}
// 解决方案:
public class MainActivity extends AppCompatActivity {
private MyListener listener;
public void onCreate() {
// ...
listener = new MyListener() {
// ...
};
// ...
}
protected void onDestroy() {
super.onDestroy();
// 在合适的时机,及时将listener置空,释放外部类引用
listener = null;
}
}
图片:
4. 内存抖动的分析和案例
4.1 内存抖动:指的是在短时间内大量的新对象被实例化,运行时无法承载这样的内存分配,在这种情况下垃圾回收事件被大量调用
4.2 内存抖动分析工具: profile
点击按钮时频繁的创建对象。在真机上运行上面的程序也许不会出现锯齿状的内存波动,但是会有非常频繁的 GC 回收
按照上面的步骤操作:
- 位置①:在程序处于运行时,点击 Record 按钮录制内存情况,然后点击 Stop 停止录制,会显示上图
- 位置②:我们可以点击 Allocations 按降序从大到小或升序从小到大查看分配对象的数量,一般我们会选择降序从大到小看数量最多的对象。上图对象数量最多的是 String 对象
- 位置③:在 Instance View 随便选择一个 String 对象,会显示下面的 Allocation Call Stack,它会显示这个对象的调用栈位置
- 位置④: 从 Allocation Call Stack 可以看到,String 对象是在 MainActivity 的第 18 行 handleMessage() 创建的,从而也定位到内存抖动的位置
4.3 内存抖动案例.
1). recyclerView的bindview方法里面创建大量对象
2). 地图的接口是会频繁回调的, 100ms回调一次数据, 但是我们频繁的刷新!
public class MainActivity extends AppCompatActivity {
@SuppressWarnings("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 模拟内存抖动
for (int i = 0; i < 100; i++) {
String[] args = new String[100000];
}
mHandler.sendEmptyMessageDelayed(0, 30);
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mHandler.sendEmptyMessage(0);
}
});
}
}
4.4 内存抖动产生的原因
问题: 内存抖动为什么会引起OOM呢?
内存不连续,导致无法分配,系统直接就返回OOM了。**
问题: 为什么说频繁的GC 会导致卡顿呢?
因为GC会停止所有线程的事件,等待GC 操作完成之后,其他操作才能够继续运行,
4.4 内存抖动解决方案
1). (new 对象)一般来说瞬间大量产生对象一般是因为我们在代码的循环中new 对象, 或是在onDraw 中创建对象等。
2). (对象复用)享元模式,通过节省对象所共享的状态,以减少内存的量(和对象池是有区别的)
5. 内存OOM分析和案例
TOP级别的OOM问题:
5.1.pthread_create问题( 线程数太多. 50%)
源码: c++
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
...
pthread_create_result = pthread_create(...)
//创建线程成功
if (pthread_create_result == 0) {
return;
}
//创建线程失败
...
{
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). 用线程池替代 new thread
2). Hook 线程,线程监控: 通过hook, ASM字节码插桩, 替换对应的线程
3).线程泄漏监控: KOOM的源码分析
主要监控native线程的几个生命周期方法:pthread_create、pthread_detach、pthread_join、pthread_exit
。
-
)hook 以上几个方法,用于记录线程的生命周期和堆栈,名称等信息;
2.)当发现一个joinable的线程在没有detach或者join的情况下,执行了pthread_exit,则记录下泄露线程信息;
5.2.文件描述符超限问题(打开太多文件. 10%)
上面2个, pthread_create
和 fd 数量不足均为 native 内存限制导致的 Java 层崩溃
不知道文件在哪创建,是谁创建的,这个就涉及到IO监控~
**文件太多监控方案: **
监控完整的IO操作,包括open、read、write、close
open:获取文件名、fd、文件大小、堆栈、线程
read/write:获取文件类型、读写次数、总大小,使用buffer大小、读写总耗时
close:打开文件总耗时、最大连续读写时间
Native监控方案: native hook 框架目前使用比较广泛的是爱奇艺的[xhook]
int open(const char *pathname, int flags, mode_t mode);
ssize_t read(int fd, void *buf, size_t size);
ssize_t write(int fd, const void *buf, size_t size); write_cuk
int close(int fd);
5.3. java 堆内存超限(内存不足. 40%)
5.3.1 堆内存超限原因 (大图片, 内存泄露)
1).单次配过大
2).累计使用过大
5.3.1 监控方案: KOOM,
具体措施: 和leakcanary优化版本一样三步!
5.3.2 具体的方案: 监控大突破和内存泄露
5.3.2.1 内存泄露: 如上
native内存泄漏监控
首先要了解native层
申请内存的函数:malloc、realloc、calloc、memalign、posix_memalign
释放内存的函数:free
`问题: 那怎么判断native内存泄漏呢?
- 周期性的使用
mark-and-sweep
分析整个进程 Native Heap,获取不可达的内存块信息「地址、大小」 - 获取到不可达的内存块的地址后,可以从我们的Map中获取其堆栈、内存大小、地址、线程等信息。
可以参考KOOM的源码:`
5.3.2.2 图片优化
图片监控设计方案:
Android Bitmap Monitor:
https://github.com/shixinzhang/AndroidBitmapMonitor
建立全局的线 Bitmap 监控
为了建立全局的 Bitmap 监控,我们必须 对 Bitmap 的分配和回收 进行追踪。我们先来看看 Bitmap 有哪些特点:
- 1)、创建场景比较单一:在 Java 层调用 Bitmap.create 或 BitmapFactory 等方法创建,可以封装一层对 Bitmap 创建的接口,注意要 包含调用第三方库产生的 Bitmap,这里我们具体可以使用 ASM 编译插桩 + Gradle Transform 的方式来高效地实现。
- 2)、创建频率比较低。
- 3)、和 Java 对象的生命周期一样服从 GC,可以使用 WeakReference 来追踪 Bitmap 的销毁。
根据以上特点,我们可以建立一套 Bitmap 的高性价比监控组件:
- 1)、首先,在接口层将所有创建出来的 Bitmap 放入一个 WeakHashMap 中,并记录创建 Bitmap 的数据、堆栈等信息。
- 2)、然后,每隔一定时间查看 WeakHashMap 中有哪些 Bitmap 仍然存活来判断是否出现 Bitmap 滥用或泄漏。
- 3)、最后,如果发生了 Bitmap 滥用或泄露,则将相关的数据与堆栈等信息打印出来或上报至 APM 后台。
这个方案的 性能消耗很低,可以在 正式环境 中进行。但是,需要注意的一点是,正式与测试环境需要采用不同程度的监控。
图片缩放,占用内存问题。
1). 大图片检测
方案一: 从图片框架侧监控: 熟读源码,找到hook点。
对于Glide,可以通过hook SingleRequest
,它里面有个requestListeners
,我们可以注册一个自己的监听,图片加载完做一个大图检测。
参考:
方案二: ARTHook优雅检测大图
ARTHook,即 挂钩,用额外的代码勾住原有的方法,以修改执行逻辑,主要可以用于以下四个方面:
- 1)、AOP编程
- 2)、运行时插桩
- 3)、性能分析
- 4)、安全审计
具体我们是使用 Epic 来进行 Hook,Epic 是 一个虚拟机层面,以 Java 方法为粒度的运行时 Hook 框架。简单来说,它就是 ART 上的 Dexposed,并且它目前 支持 Android 4.0~10.
方案三: BitmapCanary 诞生
2). 图片的加载原理
allocateJavaPixelRef,是 8.0 之前版本为 Bitmap 像素从 Java heap 申请内存
-
allocateHeapBitmap,是 8.0 版本为 Bitmap 像素从 Native heap 申请内存
图片回收原理: bitmap.recycle()
- 通过源码可以了解到,加载Bitmap到内存里以后,是包含两部分内存区域的。简单的说,一部分是Java部分的,一部分是C部分的。这个Bitmap对象是由Java部分分配的,不用的时候系统就会自动回收了
- 但是那个对应的C可用的内存区域,虚拟机是不能直接回收的,这个只能调用底层的功能释放。所以需要调用recycle()方法来释放C部分的内存
- bitmap.recycle()方法用于回收该Bitmap所占用的内存,接着将bitmap置空,最后使用System.gc()调用一下系统的垃圾回收器进行回收,调用System.gc()并不能保证立即开始进行回收过程,而只是为了加快回收的到来。
3). 大图片加载原理
方案一: 通过分区加载。BitmapRegionDecoder。加载图片的某一块区域
方案二: 可用于线上的大图加载方案
介绍一个开源库:subsampling-scale-image-view
SubsamplingScaleImageView
将大图切片,再判断是否可见,如果可见则加入内存中,否则回收,减少了内存占用与抖动 同时根据不同的缩放比例选择合适的采样率,进一步减少内存占用 同时在子线程进行decodeRegion操作,解码成功后回调至主线程,减少UI卡顿.
之前我也做BitmapRegionDecoder
与SubsamplingScaleImageView
的内存分析
有兴趣的同学也可以了解下:Android性能优化之UI卡顿优化实例分析
4). 图片跨进程传输
Android在Linux的基础上进行了改造,并借助Binder+fd文件描述符实现了共享内存的传递。
封装了android特有的内存共享机制Ashmem匿名共享内存,简单来说,Ashmem在Android内核中是被注册成一个特殊的字符设备,Ashmem驱动通过在内核的一个自定义slab缓冲区中初始化一段内存区域,然后通过mmap把申请的内存映射到用户的进程空间中(通过tmpfs),这样子就可以在用户进程中使用这里申请的内存了,另外,Ashmem的一个特性就是可以在系统内存不足的时候,回收掉被标记为"unpin"的内存,这个后面会讲到,另外,MemoryFile也可以通过Binder跨进程调用来让两个进程共享一段内存区域。由于整个申请内存的过程并不再Java层上,可以很明显的看出使用MemoryFile申请的内存实际上是并不会占用Java堆内存的。
5). 三级缓存原理
弱引用---->LruCache-->磁盘缓存
LruCache: 最近最少使用原则, LiskHashMap: 双向链表
5). 图片优化总结, Glide是如何优化图片的?
可以通过一个公式, 总结出图片优化的内容,
Bitamp 所占内存大小 = 宽度像素 x (inTargetDensity / inDensity) x 高度像素 x (inTargetDensity / inDensity)x 一个像素所占的内存字节大小
1. 图片缓存
2. 图片压缩
3. 图片缩放
4. 图片算法
5. 图片复用
**6. 实战内存分析工具:profile **
通过对静态内存数据的分析,主要发现了以下几个问题:
问题1: App首页的主图有两张(一张是保底图,一张是动态加载的图),都比较大,而且动态加载的图回来后,保底图并没有及时被释放
优化:首先是对首页的主图进行颜色通道的改变以及压缩,可以大大降低这两张图所占的内存,然后在动态加载图回来后及时释放掉保底图 -5M
问题2: 首页底部的轮播背景图占用内存1.6M,且在图片加载回来后,背景图一直没有置空
优化:首先一般来说对背景图的质量并没有很高的要求,所以这张背景图是可以被成倍压缩的,并且在图片加载回来后,背景图要及时的释放掉。同时首页的多张轮播图以及其他图片都可以进行颜色模式的改变以及质量压缩。 -1.6M -4M
问题3: 发现几个lottie动画一直没有被回收,并且同一个lottie动画会有几个不同的实例存在,总共占用内存450K
优化:首先要确定几个lottie动画为什么在页面退出后没有被回收,并且同一个动画有几个不同的实例,很容易就联想到内存泄漏,由于页面没有被销毁,所以导致几个lottie动画也没有被回收,排查下来是项目里的RN页面存在内存泄漏,解决后大概可以节省3-5M内存
问题4: SharePreference在内存里占用了700K的内存
优化:由于SP中的东西是会一次性加载到内存里并且保存为静态的,直到App进程结束才会被销毁,所以SP中千万别放大的对象,别图一时方便把对象序列化成json后保存到SP里,优化点就是把已经保存在SP中的一些较大的json字符串或者对象迁移到文件或者数据库缓存。 -400K
**问题5 **:列表item被回收时还持有图片的引用
**优化**:应该在item被回收不可见时释放掉对图片的引用,这里注意RecyclerView与ListView的区别,如果是ListView,因为每次item被回收后再次利用都会重新绑定数据,只需在ImageView onDetchFromWindow的时候释放掉图片引用即可。而对于RecyclerView来说,因为被回收不可见时第一选择是放进mCacheView中,而这里面的item被复用时并不会执行bindViewHolder来重新绑定数据,只有被回收进mRecyclePool中后拿出来复用才会重新绑定数据,所以如果是RecyclerView,我们释放图片引用的时机应该是item被回收进RecyclePool的时候,只要重写Adapter中的
onViewRecycled方法即可:
7.快手的KOOM工具, ****内存如何线上监控?(重点)
KOOM(Kwai OOM, Kill OOM)是快手性能优化团队在处理移动端OOM问题的过程中沉淀出的一套完整解决方案。
解决内存问题最有效的办法就是通过内存镜像(Hprof文件),KOOM主要是生成Hprof文件
KOOM的3大功能点:
1).Java Heap 泄漏监控
2).Native Heap 泄漏监控
3).Thread 泄漏监控
7.1 KOOM的原理:
其核心流程为三部分:
1.监控OOM,发生问题时触发内存镜像的采集,以便进一步分析问题(监控)
2.采集内存镜像,学名堆转储,将内存数据拷贝到文件中,以下简称dump hprof(采集)
3.解析镜像文件,对泄漏、超大对象等我们关注的对象进行可达性分析,解析出其到GC root的引用链以解决问题(分析)
总流程: 监控------------->采集(裁剪)------------>解析-------------------->上传
7.2 5种检查类型
mOOMTrackers中有五种类型,分别为:
1).HeapOOMTracker(APP内存使用检查)
2).ThreadOOMTracker(线程数检查)
3). FdOOMTracker(文件检查)
4).PhysicalMemoryOOMTracker(内存监控)
5). FastHugeMemoryOOMTracker (快速的增长, 图片)
7.3 KOOM解决GC卡顿
LeakCanary
通过多次GC
的方式来判断对象是否被回收!
KOOM通过将泄漏判断延迟至解析时,即可解决GC
卡顿的问题
7.4 KOOM解决Dump hprof冻结app
[图片上传失败...(image-983a87-1713338850421)]
问题: 子线程中不行吗?
虽然是在子线程内,但是还是会产生内存垃圾(一边采集数据,一边申请内存也不合理)
更本原因: 子线程也被暂停,不工作
void DumpHeap(const char* filename, int fd, bool direct_to_ddms) {
CHECK(filename != nullptr);
Thread* self = Thread::Current();
// Need to take a heap dump while GC isn't running. See the comment in Heap::VisitObjects().
// Also we need the critical section to avoid visiting the same object twice. See b/34967844
gc::ScopedGCCriticalSection gcs(self,
1607 gc::kGcCauseHprof,
1608 gc::kCollectorTypeHprof);
ScopedSuspendAll ssa(__FUNCTION__, true /* long suspend */);
Hprof hprof(filename, fd, direct_to_ddms);
hprof.Dump();
}
异步 dump
1). COW, dump 之前先 fork App 进程,这样子进程就获得了父进程所有的内存镜像((copy-on-write 写时复制))
2). 绕过暂停线程(suspend), 父进程立刻恢复虚拟机运行
先在主进程中执行 SuspendAll 方法,使 ThreadList 中保存所有线程状态为 suspend,之后再fork,这样子进程共享父进程的 ThreadList 全局变量 _list,可以欺骗虚拟机,使其认为所有的线程完成了暂停操作,接下来就可以在子线程执行 hprof.dump 操作了。而主进程在 fork 之后调用 ResumAll 恢复运行。
问题: 主进程中依然执行了 SuspendAll()方法, 不会冻结APP么! (虽然有一个短暂的挂起时间,但是相对于GC的频繁STW)
7.5. KOOM解决hprof解析的耗时与OOM
客户端解析后上传报告, 裁剪有用的信息