【万字总结】Android 内存优化知识盘点

在人生最艰难的日子里,不要想太过遥远的未来,认真过好当下每一天,就好了。------ 杨绛

内存优化的意义

Android性能优化三把板斧:稳定性、内存、启动速度、包体积。这几个方向的知识其实是相互关联的网状结构,例如内存控制得好,就不容易出现OOM导致的稳定性问题。同样,经过精简的包体积,也会大大提升应用启动速度。

相比于C/C++,JVM很大的一个跨越是实现了内存的自动分配与回收。然而,这并不意味着作为开发者可以肆无忌惮地使用内存,作为一种有限的资源,内存再大也有耗尽的时候。我们讲"内存优化",主要是出于稳定性、流畅度、进程存活率三个维度的考虑。

  1. 稳定性:减少OOM,提高应用稳定性
  2. 流畅度:减少卡顿,提高应用流畅度
  3. 进程存活率:减少内存占用,提高应用后台运行时的存活率

内存理论知识

这一部分讲解内存优化相关的理论知识,为实践环节提供理论支持。首先从虚拟机配置角度说明Android系统里应用可用的内存上限;然后分析在这个上限之内,应用程序内存主要分配为哪几个部分;最后用比较多的篇幅讲解开发中关系最紧密的对象内存分配与回收

应用可用内存:dalvik.vm配置

现在市面上主流旗舰机型的内存已经达到了16G的级别,比起笔记本电脑也不遑多让,然而在极限的使用条件下,仍然会不可避免地出现卡顿。对于应用程序而言,它能够使用的最大内存是在虚拟机配置里写死的,可以通过命令adb shell getprop dalvik.vm.heapgrowthlimit来查看,在我的手机(vivo X Note)上是256m

bash 复制代码
> adb shell getprop dalvik.vm.heapgrowthlimit
256m

此外虚拟机配置中还有heapstartsizeheapsize等参数,它们都是定义在/system/build.prop文件当中的,含义说明如下:

参数 含义 vivo X Note
heapstartsize 堆分配的初始大小,影响操作系统对RAM的分配和首次使用应用的流畅性 8m
heapgrowthlimit 单个应用可用的最大内存,若超出则OOM 256m
heapsize 单个进程可以用最大内存,仅当声明android:largeHeap=true时生效,此时会覆盖heapgrowthlimit的值 512m

Native虚拟内存

我们知道Android 8.0以后,Bitmap不再存放于Java堆内存,而是位于Native的堆内存,它的上限也就是Native虚拟内存的上限,只与CPU架构相关。Native堆内存不受JVM管控。

CPU架构 Native堆上限
32位 3G(4G减去1G内核空间)
64位 128T

进程内存指标:USS, PSS, RSS, VSS

dumpsys meminfo可以看到整个系统中不同进程所占据的内存情况,最重要的有以下4个缩写指标,它们之间的关系是:VSS >= RSS >= PSS >= USS。在Android系统中推荐使用PSS曲线来衡量应用的物理内存占用情况。

内存指标 英文全称 含义 等价
USS Unique Set Size 物理内存 进程独占的内存
PSS Proportional Set Size 物理内存 PSS = USS + 按比例包含共享库
RSS Resident Set Size 物理内存 RSS= USS+ 包含共享库
VSS Virtual Set Size 虚拟内存 VSS= RSS+ 未分配实际物理内存

可以通过dumpsys命令查看以上内存信息。

bash 复制代码
dumpsys meminfo <pid> // 指定pid
dumpsys meminfo --package <packagename> // 指定包名,可能存在多个进程
dumpsys meminfo // 系统中所有进程的内存占用,有排序

按照范围从大到小,Android系统管理内存可以分为进程、对象、变量三个层级。

进程内存分配与回收

进程内存分配和回收受到Linux内核的管控,Android将进程分为5个优先级,当进程空间紧张时,按照优先级从低到高的顺序进行回收。

对象/变量内存分配与回收

对象/变量内存分配和回收受到JVM虚拟机的管控,在JVM中,内存按区域分配如下,其中方法区、堆区是供所有线程共享的。

  • 方法区:被虚拟机加载的类信息、常量、静态变量;存活于程序运行整个周期,不回收
  • 堆区:存储Java对象实例,在线程区-Java函数栈帧会定义实例指针(or引用),指向堆内的实例;GC时判断是否回收
  • 线程区 :每一个线程单独的区域
    • 程序计数器:当前线程运行到的指令位置行数
    • Java函数栈帧:存储方法执行时的局部变量、操作数;执行结束后释放
    • Native函数栈帧:为Native函数服务;执行结束后释放

Java对象生命周期

ClassLoader装载.class文件开始,一个Java对象的生命周期包含以下阶段:

JVM内存分代

Java虚拟机采用分代策略管理堆内存中的对象,分代的目的是优化GC性能 ,将具有不同生命周期 的对象归属于不同的年代,采取最适合它们的内存回收方式

JVM运行时内存 可以分为堆(Heap)非堆(Non-heap) 两大部分。堆在JVM启动时创建,是运行时数据区域 ,所有类实例数组 内存从堆分配。堆以外的内存称为非堆内存,方法区、类结构 等数据保存在非堆内存。简单说,堆是开发者可以触及的内存部分,非堆是JVM自留的部分

对象分代的角度,JVM内部将对象分为3类:

  • New Generation,新生代,位于堆内存,内部分为3块,其中EdenSurvivor的默认比例为8:1
    • Eden,新创建的对象都位于该分区,满时执行GC,并将仍存活的对象复制到From,GC后此区域被清空
    • From Survivor (S1),GC时,将仍存活的对象复制到To
    • To Survivor (S2),满时,将仍存活的对象复制到Old。GC之后会交换FromTo区,从而使新的To(也就是老的From)永远是空的
  • Old Generation,老年代,位于堆内存
  • Permanent Generation,永生代,位于非堆内存

GC Roots

定义:通过一系列名为GCRoots的对象作为起始点,从这个节点向下搜索,搜索走过的路径称为ReferenceChain,当一个对象到GCRoots没有任何ReferenceChain相连时,(图论:这个对象不可到达),则证明这个对象不可用。

共有4GC Roots:

  • 方法区
    • 静态引用的对象
    • 常量引用的对象
  • 线程区
    • Java函数栈帧中引用的对象
    • Native函数栈帧中JNI引用的对象

垃圾回收算法

算法 解释 特点 应用场景
复制 将内存分为2份,使用其中一份进行存储,每次执行清除时将仍然存活的对象复制到另一半,然后清除原来的内存 舍弃空间,换取时间上的高效率 对象存活率低的场景------新生代
标记-清除 先扫描一遍,将不存活的对象打上标记。第二遍扫描时清理掉这些对象 会产生不连续的内存碎片 对象存活率高的场景------老年代
标记-整理 先扫描一遍,将不存活的对象打上标记。第二遍扫描时清理掉这些对象,并将内存向一侧聚集收拢 防止产生内存碎片 对象存活率高的场景------老年代

内存问题归类

理解了JVM内存的分配与回收原理,我们看一下在Android开发中,发生内存问题的三个表现,它们分别是内存抖动内存泄漏内存溢出。其中内存抖动与泄漏是引发内存溢出的重要因素。

内存抖动

什么是内存抖动

在一段时间内,频繁地发生内存的分配和释放,在曲线图上展现为锯齿状的内存使用曲线。

内存抖动的原因

  1. 选择了不恰当的数据类型:如使用加号+进行字符串拼接,正确的做法是用StringBuilder
  2. 在循环里面创建对象:则循环执行时会频繁地创建销毁
  3. onDraw中创建对象:一次绘制会多次调用onDraw

如何避免内存抖动

  1. 选择恰当的数据类型(String -> StringBuilder
  2. 在循环体、onDraw函数外创建对象并复用,减少不合理的对象创建
  3. 使用对象池进行缓存复用

内存泄漏

直接原因是长生命周期的对象引用了短生命周期的对象,导致短生命周期的对象无法回收。主要是指Activity的泄漏,即持有Activity的变量生命周期长于Activity的生命周期,导致Activity虽然结束但其对象并未释放,其内部的成员变量也由于被持有而无法释放。反复进入退出页面,会看到内存曲线不断上升,即使手动GC也无济于事,继续下去会导致OOM。

常见泄漏场景

在Android开发中经常会有将Activity作为Context类型参数传递的场景,用到的时候必须谨慎,防止泄漏。

1.非静态内部类

非静态内部类会持有外部类对象的引用,典型的有Handler、Runnable等。内部类在编译后,会生成一个名为this$0的成员变量,就是外部类实例。

1.1 Handler等待运行

在使用Handler处理消息时,会构造一个Message对象并调用Handler.post(),此时Message对象中的target成员变量指向该Handler。由于Message会在MessageQueue中按时序处理,在处理到它之前,是无法释放Handler的。也就导致了Handler所持有的外部类(通常是Activity)发生泄漏。

要解决这个问题,需要从两点入手:

  1. 使用Handler静态内部类(或者直接新建一个类文件),Handler通过弱引用持有Activity
  2. Activity.onDestroy()中清空消息队列
1.2 Runnable长时间运行

与非静态内部类相类似,通过new Runnable() {...}创建一个匿名内部类的对象时,也会持有外部对象的引用。如果这个Runnable在线程池中排队等待处理,同样会导致Activity无法释放。解决方法与Handler类似,由于Activity与任务队列不强相关,此处不建议清空线程池中的任务队列。

  1. 创建一个实现了Runnable的静态内部类,内部通过弱引用持有Activity,使用Activity时判空
java 复制代码
// Runnable错误用法
private Runnable mRunnable = new Runnable() {
    @Override
    public void run() {

    }
};

// Runnable正确用法
private static class MyRunnable implements Runnable {
    WeakReference<MainActivity> reference;
    public MyRunnable(MainActivity activity){
        reference = new WeakReference<>(activity);
    }
    @Override
    public void run() {
        MainActivity activity = reference.get();
    }
}

2.方法区持有对象引用

方法区包含类结构、常量和静态变量,是GC Roots之一,与整个应用的生命周期相同,由于长期存活即拿即用,比较方便,但也因此容易导致内存泄漏。

2.1 静态成员变量

静态成员变量在类加载的时候就会赋值,如果其中持有Activity对象,会直接导致无法回收。

2.2 单例

双重检查是单例的常规写法,其实现中也用到了静态变量,因此同样有泄漏的风险。

3.资源未关闭

当Activity销毁时,资源性对象不再使用,应当关闭该对象,然后将对象置为null,常见的有BitmapInputStreamOutputStream等实现了Closable接口的类。

java 复制代码
// Bitmap
Bitmap bitmap = new Bitmap();
...
bitmap.recycle();
bitmap = null;

// InputStream
InputStream inp = new FileInputStream("foo.txt");
...
inp.close();
inp = null;

4.注册对象未注销

切记需要成对调用注册-注销,如BroadcastReceiverEventBus,否则会导致Activity一直被持有,从而发生泄漏。在开发总线类框架的时候,也要注意提供相应的注销接口。

5.WebView必然泄漏

这个是Android的痼疾了,一旦创建过WebView对象,就无法回收,与Chromium内核实现有关,网上有建议用如下方法清空WebView资源,但据网友描述在一些机型让仍然会泄露。

kotlin 复制代码
override fun onDestroy() {
    val parent = webView?.parent
    if (parent is ViewGroup) {
        parent.removeView(webView)
    }
    webView?.destroy()
    super.onDestroy()
}

既然如此,就得避免创建过多的WebView对象,或者干脆另外起一个进程用作WebView展示。

  1. 构建WebView对象池,实现复用
  2. 将WebView单独开辟一个进程使用,与主进程之间跨进程通信传递数据

6.集合未清空

将对象添加进集合后,集合实例会持有该对象的引用,因此在销毁时应当将集合中对应元素移除。

java 复制代码
// 问题场景
// 通过 循环申请Object 对象 & 将申请的对象逐个放入到集合List
List<Object> objectList = new ArrayList<>();        
for (int i = 0; i < 10; i++) {
    Object o = new Object();
    objectList.add(o);
    o = null; // 虽释放了集合元素引用的本身,但仍然被集合持有
}

// 正确写法
objectList.clear();
objectList = null;

内存溢出

当应用在为新的数据结构申请内存时,如果当前可用内存不足以提供给新对象,就会发生内存溢出(OOM,即Out of Memory)。我们面临的大部分都是Java堆内存超限问题。

当发生OOM时,并不一定是当前执行的代码发生了问题,有可能是由于之前的不正确调用,当前已经处于一个内存异常的状态,在申请开辟新内存时导致问题爆发。

优化思路

这里列出一些内存优化的建议,由于在应用运行的整个生命周期里都存在内存的申请和释放,因此内存优化存在于应用开发的方方面面。

1.精简代码

在JVM中类的数据结构位于方法区,伴随应用的整个生命周期都不会销毁,因此应当尽量精简类的结构。

  • 减少不用的类,以及类中的成员变量
  • 减少不必要的三方依赖包
  • 开启应用混淆,精简类名方法名变量名,自动清理无用的类

2.内存复用

通过复用,减少频繁创建销毁对象的行为,防止产生大量离散的内存碎片。

  • 资源复用:归拢通用的字符串、颜色、度量值等,进行基础布局的复用
  • 视图复用:借助ViewHolder减少视图布局开销
  • 对象复用:建立对象池,实现复用逻辑

3.选用性能更高的数据结构

3.1 慎用SharedPreferences

读取SP时会将整个xml加载到内存里,占据几十K、上百K的内存。

3.2 使用SparseArray、ArrayMap代替原生的HashMap

HashMap最初是为JVM设计的,为了减少哈希冲突,选用了一个较大的数组作为容器来降低冲突,这是一种空间换时间的思路。在Android中提供了不同的替换结构。

  • KeyInt时,选用SparseArray
  • Key是其他类型时,选用ArrayMap

它们的思路是时间换空间,内部是两个数组,节约了HashMap中空置的元素位。从性能角度看,当元素数量在1000以下时,性能几乎与HashMap持平。

4.选用性能更高的数据类型

4.1 避免频繁拆箱装箱(AutoBoxing)

在自动装箱时,会创建一个新的对象,从而产生更多的内存和性能开销。int只占4字节,而Integer对象占据16字节,尤其是HashMap此类容器,当用Integer作为Key/Value,进行添加、删除、查找操作时,会产生大量自动装箱操作。

排查方法:通过TraceView查看耗时,如果发现调用了大量的Integer.valueOf()函数,说明存在装箱行为。

4.2 避免使用枚举类型,用IntDef、StringDef等注解代替

枚举的优点是提供强制的类型检查,但缺点是对于dex构建、运行时内存都会造成更大的消耗(相比于IntegerString)。为了满足编译期间的类型检查,可以改用IntDefStringDef,需要引入依赖:

bash 复制代码
compile 'com.android.support:support-annotations:22.0.0'

5.实现onLowMemory()、onTrimMemory()方法

它们都是在Application、Activity、Fragment、Service、ContentProvider中提供的,用于应用内存紧张时的通知,在发生这些回调时,应当主动清除缓存,释放次要资源。通过Context.registerComponentCallbacks()进行注册监听。

  • onLowMemory:发生时,所有后台进程已经被杀死
  • onTrimMemory:Android 4.1引入,存在多个不同级别的回调

6.声明largeHeap以增大可用内存

在AndroidManifest.xml中声明android:largeHeap=true可以将应用的可用内存扩展到dalvik.vm.heapsize。但这是一种治标不治本的方法,并不推荐使用。

7.建立LruCache

对于可能存在复用的对象,建立LruCache,内部由LinkedHashMap双向链表实现(也可以自定义双向链表)。支持get/set/sizeOf函数。当收到低内存警告(onLowMemoryonTrimMemory)时,清空LruCache。

LruCache的实现可以参考Glide源码。

8.排查内存泄漏

针对上文中提出的内存泄漏风险点,使用Memory Profiler、LeakCanary、MAT等工具,逐页面进行排查。

典型问题:Bitmap优化

Bitmap是内存消耗的大头,在Android 8.0及以上版本,Bitmap内存位于Native层,可以利用虚拟内存提升存储量,同时避免JVM堆内存耗尽。即使这样,在创建Bitmap时仍然会先占用JVM的堆空间,只不过在创建后会将其复制到Native堆进行维护。

ARGB_8888格式为例,一个像素占用4bytes,所以整个Bitmap所占据的字节数=长*宽*4 bytes,假设一张图片长宽各为500px,那么它需要占用500*500*4=1,000,000B=1MB的内存,对于长列表而言,如果使用不当,一次就可能导致几百MB的内存泄漏。

因此,Bitmap优化是内存优化的重中之重,通常有以下几个思路:

  1. 缩放减小宽高
  2. 减少每个像素占用的内存
  3. 内存复用,避免重复分配
  4. 大图局部加载
  5. 使用完毕后释放图片资源
  6. 设置图片缓存
  7. 大图监控

1.缩放减小宽高

图片尺寸不应大于View的尺寸,例如图片是200*200,而View是100*100的,此时应当对图片进行缩放,通过inSampleSize实现。

java 复制代码
BitmapFactory.Options options = new BitmapFactory.Options();
// 宽高都变为原来的1/2
options.inSampleSize = 2;
BitmapFactory.decodeStream(is, null, options);

2.减少每个像素占用的内存

这一条适用于那些些不需要展示高清图的场景,以及低配机器的图片显示。在API 29中,将Bitmap分为6个等级,如果图片不含透明通道,可以考虑用RGB_565代替ARGB_8888,能够节约一半的内存。

  • ALPHA_8:不存储颜色信息,每个像素占1个字节;
  • RGB_565:仅存储RGB通道,每个像素占2个字节,对Bitmap色彩没有高要求,可以使用该模式;
  • ARGB_4444:已弃用,用ARGB_8888代替;
  • ARGB_8888:每个像素占用4个字节,保持高质量的色彩保真度,默认使用该模式;
  • RGBA_F16:每个像素占用8个字节,适合宽色域和HDR
  • HARDWARE:一种特殊的配置,减少了内存占用同时也加快了Bitmap的绘制。

3.内存复用,避免重复分配

通过设置BitmapFactory.Options.inBitmap,可以在创建新的Bitmap时尝试复用inBitmap的内存,在4.4之前需要inBitmap与目标Bitmap同样大小,在4.4之后只要inBitmap比目标Bitmap大即可。

4.大图局部加载

有些场景下需要展示巨大的一张图片,比如地铁路线图、微博长图、对高像素图片进行编辑等,如果按照原图尺寸进行加载,一张4800万像素的照片会占据48M的内存,非常可怕。

因此,在大图展示的场景下,通常采用BitmapRegionDecoder来实现。

java 复制代码
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(inputStream, false);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
// 加载图片中央200*200的局部区域
Bitmap bitmap = decoder.decodeRegion(new Rect(width/2-100, height/2-100, width/2+100, height/2+100), options);
mImageView.setImageBitmap(bitmap);

5.使用完毕后释放图片资源

Bitmap使用完毕后应当及时释放。

5.1 常规释放

java 复制代码
Bitmap bitmap = ...
// 使用该bitmap后,进行释放
bitmap.recycle();
bitmap = null;

5.2 非常规,泄漏兜底

如果Activity、Fragment发生泄漏,会导致它们持有的View无法释放,进而使View中展示的Bitmap对象无法被系统回收。

此时,可以通过覆写View.onDetatchedFromWindow进行监控兜底,当View从Window中被移除,且Activity处于destroy状态时,延迟2s判断,如果2s后图片仍然没有被移除,说明发生了泄漏。此时应当主动将Bitmap释放。

6.设置图片缓存

可以参考Glide的四级缓存策略,我之前写过文章《Glide源码解析》

7.进行大图监控

前面说过,如果Bitmap的尺寸大于目标View的尺寸,是没有意义的,这里提供一些技术手段协助进行大图监控,我们声明一个checkBitmapFromView的函数,用于扫描当前Activity、Fragment的全部View,并取出Bitmap进行尺寸比较,如果发现大图case则报警。

java 复制代码
/**
 * 1. 遍历Activity中每个View
 * 2. 获取View加载的Bitmap
 * 3. 对比View尺寸和Bitmap尺寸
 */
fun checkBitmapFromView() {
    // 实现略
}

如何将这段检查的逻辑插入到代码当中?有以下几种方式。

方案 实现思路 优点 缺点
BaseActivity 在基类的onDestroy()中检查 接入简单 侵入性强,对全部Activity产生影响,所有Activity需要继承BaseActivity
ArtHook 是针对ART虚拟机的Hook方案,通过Hook ImageView的setImageBitmap函数,在其中进行检测 无侵入性,一次配置全局生效;可以获取代码调用堆栈,便于定位问题 Hook方案存在兼容性风险,需要全面覆盖测试
ASM 在编译过程中对setImageBitmap进行插桩 无侵入性 增加编译耗时,ASM代码维护成本高
registerActivityLifecycleCallback 在Application中注册 无侵入性,接入简单 暂无

分析工具

Memory Profiler

MAT(Memory Analyzer Tool)

MAt是用来分析内存快照,分析对象引用链,从而找出导致内存泄漏根因的工具。

MAT的使用步骤

  1. 下载 eclipse.org/mat/downloa...
  2. 在Android Studio中进入Memory Profiler功能,操作应用关键页面
  3. 点击GC以清理掉那些没有被泄露的对象,此时剩余的是发生泄漏的
  4. 将内存快照dump到本地
  5. 使用Android SDK自带的转换工具(位于platform-tools),将Dalvik/ART格式的.hprof文件转换为MAT能识别的J2SE格式,最后用MAT打开文件
bash 复制代码
./hprof-conv aaa.hprof aaa-converted.hprof

MAT中的术语说明

  • Dominator:支配者,如果B引用了A,那么B是A的支配者
  • Dominator Tree:支配者引用链
  • Shallow Heap:当前对象自身的内存占用
  • Retained Heap:当前对象及其成员变量加在一起的总内存占用
  • Outgoing References:当前对象引用了哪些对象
  • Incoming References:当前对象被哪些对象引用
  • Top Consumers:通过饼图方式列出内存占用最多的对象

LeakCanary

在开发环境中使用,反复进出同一页面,查看是否有泄漏通知。LeakCanary可以保存.hprof文件,供进一步分析。关于其原理部分主要有三个考察点:怎样监听Activity销毁时机、如何判断Activity是否被释放、如何自启动,可以参考我之前写的一篇文章《LeakCanary源码解析》

线上OOM监控:美团Probe与快手KOOM

当线上发生OOM时,我们需要立即采集现场数据,并报警给服务器进行收集。对此,不同公司都提出了各自的方案,例如美团的Probe(未开源),快手的KOOM。这些方案主要解决了以下几个问题:

  • 监控OOM:当发生OOM时触发采集上报流程
  • 堆转储:即采集内存快照,dump hprof
  • 解析快照:把对分析问题无用的数据裁剪去掉,仅保留有用的信息。进行可达性分析,生成到GC Roots的调用链,找出泄漏根因
  • 生成报告并上传:KOOM支持在端侧生成json格式报告,并进行上传,将解析工作交由端侧进行,能有效减少云端的分析压力

面试常见提问

Q:说说你对内存优化的理解

问题比较大,采用总-分-总方式作答。

  • (总)内存管理是应用性能优化中非常重要的一环,在我以往的工作中也从事过一些内存优化的工作,业余也查阅积累了一些内存优化的知识。接下来我从重要性、问题分类、常见问题场景、优化思路几个方面讲一下我的理解
  • (分)不要讲的太细,一是占用时间太多,过犹不及,容易让面试官感到啰嗦,二是会留下背答案的印象
  • (总)略

Q:开发过程中你是怎样判断有内存泄漏的

在开发过程中,我通常使用以下几种方法,对内存泄漏可能存在的场景进行快速检测和定位。

  1. 内存泄漏-应用全生命周期分析shell命令 + LeakCanary + MAT,运行程序并将主要链路页面都打开一遍,完全退出程序并手动触发GC。然后使用adb shell dumpsys meminfo <packagename> -d打印应用当前进程信息,如果存活的View和Activity、Fragment数量不是0,说明它们发生了泄漏。接着借助LeakCanary查看哪些对象发生了泄漏,最后用MAT找出这些泄漏对象的引用关系,定位问题根因
  2. 内存泄漏-单一页面分析:使用Memory Profiler工具,对于目标页面,反复进出5次,在最后一次退出后手动触发GC。如果此时内存曲线没有回到进入页面之前的状态,说明发生内存泄漏。然后,将内存快照保存下来,并在Memory Profiler中查看该Activity,如果存在多个实例,说明没有被回收,即发生泄漏
  3. 内存上涨-单一页面分析 :利用Memory Profiler观察进入每个页面的内存变化情况,对于上升幅度大的页面,下载进入页面前后的两个内存快照并用MAT的对比分析功能,找出新页面内存的分配情况,被哪些大的对象所占用

参考资料

相关推荐
测试老哥4 小时前
外包干了两年,技术退步明显。。。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
ThisIsClark6 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
测试19987 小时前
外包干了2年,技术退步明显....
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
Aphasia3118 小时前
一次搞懂 JS 对象转换,从此告别类型错误!
javascript·面试
GISer_Jing10 小时前
2025年前端面试热门题目——HTML|CSS|Javascript|TS知识
前端·javascript·面试·html
上海运维Q先生11 小时前
面试题整理14----kube-proxy有什么作用
运维·面试·kubernetes
开发者每周简报12 小时前
求职市场变化
人工智能·面试·职场和发展
贵州晓智信息科技16 小时前
如何优化求职简历从模板选择到面试准备
面试·职场和发展
百罹鸟16 小时前
【vue高频面试题—场景篇】:实现一个实时更新的倒计时组件,如何确保倒计时在页面切换时能够正常暂停和恢复?
vue.js·后端·面试
古木20191 天前
前端面试宝典
前端·面试·职场和发展