一、前言
性能优化 是衡量 我们app质量的一个很大标准
几大影响性能的问题:
UI卡顿 ——主线程耗时操作过多
ANR——主线程耗时操作过多
内存泄漏
OOM 内存溢出——图片处理
启动速度
内存泄漏
特点:
- 不易察觉,不易发现
- 长时间不断累积会导致OOM内存溢出
内存泄漏出现的原因
Java虚拟机 会自动提供一套GC垃圾回收机制。在其内部会自动进行便利无用引用对象而进行内存的清理工作。
其根本原因是:较长生命周期的对象持有了较短生命周期的引用导致较短生命周期对象无法被垃圾回收器回收。
GcRoots
垃圾收集器 (Garbage Collector) 的对象
可达性算法-分析对象是否存活
从GcRoots结点作为起点,向下依次搜索,搜索走过的路径被称为引用链。当一个对象达到没有任何与GcRoots 引用链相连的时候,就说明该对象是不可用的,该对象就是Gc可回收对象。
Gc回收的对象:
没有被GcRoots 引用的对象
Java中的4种引用
- 强引用
如果一个对象是通过一串强引用链相连的 它就不能被回收。JVM即时报错OOM也不会回收强引用对象
-
软引用 -soft
优先级低于强引用,在内存足够的情况下,他和强引用的效果一致。但是如果出现内存不足的情况,Gc垃圾回收就会回收掉软引用对象。
- 弱引用 -weak
不会强制对象保存到内存当中,优先级低于软引用。Gc回收器必回收对象之一。
- 虚引用 -phantom
表明这个引用的存在并无影响,任何情况下都必须被Gc回收器回收。
在Eclipse中 MAT排查引用的方式:
Path To GC Roots ——> exclude all phantom/weak/soft etc.references (排除以上引用链对象进行查找)
保证强引用的引用链上,没有对象可以被回收,表明没有内存泄漏
LeakCanary
特点:
1、手动触发GC 然后分析强引用的GC引用链
2、如果存在GC引用链,说明有内存泄漏问题,会通过对话框提示用户,并自动创建一个LC app
3、会记录每一次内存泄漏的GC引用链,通过它可以直接定位到内存泄漏的未释放的对象,并进行解决
二、 内存泄漏基础
检查代码是否存在内存泄漏的问题的工具
- MAT
- LeakCanary
LeakCanary
Square 开源的一款轻量级第三方内存泄漏检测工具
- 问题由谁产生
- 导致了谁泄漏而不能被回收
核心原理
watch 一个即将被销毁的对象
内存
1、栈内存stack
存放基本类型数据、对象的引用
2、堆内存 heap
存放new 创建出的对象 、数组;在这里分配的内存是通过Gc垃圾回收器管理的
ps:JVM 只有一个堆区,会被所有线程共享
3、方法区(method 静态区)
同2一样也是被所有线程共享的,包含所有的Class对象 和静态变量
内存泄漏产生的原因分析
-
当一个对象已经不需要再使用了
在这个对象该被回收的时候,而这时候正好有另外一个正在使用的对象持有了这个应该被回收对象的引用而导致了这个对象不能被回收。导致本该被回收的对象不能被回收,而把该对象停留在heap 堆内存当中,就会产生内存泄漏
- 有些对象只有优先的生命周期
当生命周很短的对象在任务完成之后,就将被垃圾回收。如果在这类对象在生命周期本该结束的时候,这个对象还被一系列的引用就会导致内存泄漏。随着泄漏的不断累积,App会消耗完整个内存
内存泄漏导致的问题
OOM
造成OOM主要原因之一;应用所需要的内存超过系统分配的内存最大值,造成内存溢出,导致App crash掉。
三、Android 中常见的内存泄漏
静态内部类不持有外部累的引用
1、 单例造成的内存泄漏问题
错误写法:
public class SingletonActivityContext {
//静态对象的生命周期和整个生命周期一样长
private static SingletonActivityContext instance;
private Context context;
//如果这个context 传入的是Activity的context,当Activity退出的时候,这个context的内存并不会被回收
//这个单例对象context 持有了传入Activity对象的引用,所以Activity不能被gc回收
private SingletonActivityContext(Context context) {
this.context = context;
}
public static SingletonActivityContext getInstance(Context context) {
if (instance == null) {
instance = new SingletonActivityContext(context);
}
return instance;
}
}
修改后的正确写法:
/**
*解决办法 ,传入的context 为Application context 就能解决问题错误的单例模式写法
*/
public class SingletionApplicaitonContext {
private static SingletionApplicaitonContext instance;
private Context context;
private SingletionApplicaitonContext(Context context) {
this.context = context.getApplicationContext();//保证单例对象持有整个app的生命周期
}
public static SingletionApplicaitonContext getInstance(Context context) {
if (instance == null) {
instance = new SingletionApplicaitonContext(context);
}
return instance;
}
}
2、非静态内部类创建静态实例造成的内存泄漏问题
/**
* 非静态内部类创建静态实例造成的内存泄漏
*/
public class StaticLeakActivity extends Activity {
//为例避免重复创建相同的数据对象定义一个静态的全局变量,避免资源的重复创建
private static nonStaticClass mResource;
//非静态内部类创建类一个静态的实例mResource,导致该生命周期和app生命周期一样长,
// 会导致mResource一直持有StaticLeakActivity的引用 ,会造成该Activity无法被gc回收
@Override
public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
super.onCreate(savedInstanceState, persistentState);
setContentView(R.layout.activity_main);
if (mResource == null) {
mResource = new nonStaticClass();
}
}
/**
* 非静态的内部类nonStaticClass 默认会持有StaticLeakActivity的引用
*/
private (static)class nonStaticClass {}
}
3、 handler造成内存泄漏问题
为了避免Anr问题 而不在主线程进行耗时操作,处理网络任务或是封装一些请求回调的时候会通过handler 机制来进行处理。
/**
* 3、handler造成内存泄漏
* handler、message、messageQueue
* Message:handler发送消息的时候,这个message没有被处理完成,这时候这个 message和它所发送的handler对象就将被线程一直持有
* TLS变量:handler生命周期和整个Activity生命周期不一致而造成内存泄漏
*/
public class HandlerLeakActivity extends Activity {
//Message持有了Handler引用,而handler又持有了Activity的引用,导致Activity无法被回收造成了内存泄漏,错误的写法:
private final Handler mLeakHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
super.onCreate(savedInstanceState, persistentState);
mLeakHandler.postDelayed(new Runnable() {
@Override
public void run() {
//模拟延时10分钟发送一个消息
}
}, 1000 * 60 * 10);
//结束了Activity,但这时候还持有Activity的handler引用 因为延时操作finish掉的Activity就不会被回收
finish();
}
/**
* 解决办法:
* 1、将handler生命为静态的,使得handler生命周期和Activity无关
* 2、通过弱引用的方式引入Activity
* */
privatefinal static HandlermHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
}
4、线程造成的内存泄漏
/**
* 4、线程造成的内存泄漏
* asyncTask、Thread 的runnable定义成非静态内部类的时候,当Activity想要被回收的时候,由于任务持有了
* Activity的隐式引用,任务没有完成导致当前Activity没办法被回收而导致内存泄漏
* 解决办法:定义为静态内部类,避免Activity内存资源的泄漏
*/
public class ThreadLeakActivity extends Activity {
//静态内部类static MyRunnable
staticclass MyRunnable implements Runnable{
…}
//静态内部类static MyAsyncTask
staticclass MyAsyncTask extends AsyncTask<Void, Void, Void> {
private WeakReference<Context> weakReference;
public MyAsyncTask(Context context) {
weakReference = new WeakReference<>(context);
}
...
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
ThreadLeakActivity activity = weakReference.
if (activity != null) {
//...
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
MyAsyncTask.cancel();//销毁后台可能在执行的任务,节省资源
}
5、WebView造成的内存泄漏
/**
* 5、WebView造成的内存泄漏
* Webview解析网页的时候会申请native堆内存来保存页面的元素。
* 页面越复杂占用的内存越多,当收集加载网页过量的时候就会非常卡顿甚至app闪退 等
*/
public class WebviewLeakActivity extends AppCompatActivity {
private WebView mWebView;
@Override
public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
super.onCreate(savedInstanceState, persistentState);
//...
}
@Override
protected void onDestroy() {
destroyWebView();
//1、将WebView所处的Activity放在一个单独的进程当中,检查到app占用内存过多的时候主动杀掉这个进程,系统就会自动回收这部分内存
android.os.Process.killProcess((android.os.Process.myPid()));
super.onDestroy();
}
private void destroyWebView() {
if (mWebView != null) {
mWebView.pauseTimers();
mWebView.removeAllViews();
mWebView.destroy();
}
}
}
三、 LeakCanary
目标:实时监控Activity
原理
1、Activity Destroy之后将它放在一个WeakReference
2、这个WeakReference关联到一个ReferenceQueue
3、查看ReferenceQueue是否存在Activity的引用
4、如果该Activity泄漏,Dump出heap信息,然后分析泄漏路径和根源
ReferenceQueue
软引用/弱引用
对象被垃圾回收,JVM虚拟机就会把这个引用加入到与之关联的引用队列中。
源码解析:
LeakCanary.install(this); //leakCanary入口
RefWatcher:用于监听activity的内存泄漏
/**
*用于启动activity RefWatcher类,activity RefWatcher类会在onDestroy()类后进行探测activity的内存泄漏
*/
public static RefWatcherinstall(Application application) {
return install(application, DisplayLeakService.class);
}
public static RefWatcher install(Application application,
Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
if (isInAnalyzerProcess(application)) {
return RefWatcher.DISABLED;
}
//开启一个通知内存泄漏信息的Activity
enableDisplayLeakActivity(application);
HeapDump.Listener heapDumpListener =
new ServiceHeapDumpListener(application, listenerServiceClass);
//A、创建一个RefWatcher 启动一个Activity的RefWatcher,通过此监视activity的回收情况
RefWatcher refWatcher = androidWatcher(application, heapDumpListener);
ActivityRefWatcher.installOnIcsPlus(application, refWatcher);
return refWatcher;
}
installOnIcsPlus():
public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
...
ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
activityRefWatcher.watchActivities();
}
watchActivities()():
public void watchActivities() {
// 反注册Activity 生命周期的Callback,保证以前所做的内存泄漏的内容要删除。
stopWatchingActivities();
//重新注册有关生命周期的callback
application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
}
stopWatchingActivities():
public void stopWatchingActivities() {
application.unregisterActivityLifecycleCallbacks
(lifecycleCallbacks);
}
在lifecycleCallbacks中定义了很多的有关生命周期的内部类:
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
new Application.ActivityLifecycleCallbacks() {
@Override public void onActivityDestroyed(Activity activity) {
//B、通过ActivityLifecycleCallbacks把 ActivityRefWatcher与Activity的onDestroy生命周期进行了关联操作
ActivityRefWatcher.this.onActivityDestroyed(activity);
}
};
void onActivityDestroyed(Activity activity) {
refWatcher.watch(activity);
}
在refWatcher中含有的成员变量:
final Executor watchExecutor;//用于执行内存泄漏检测
final DebuggerControl debuggerControl;//查询是否正在调试中
ps:如果在debug调试中就不会进行内存泄漏的检测判断
final GcTrigger gcTrigger;//再一次执行处理垃圾Gc
final HeapDumper heapDumper;//内存泄漏的堆信息文件
final Set<String> retainedKeys;//持有待检侧的,和已经产生内存泄漏的key
final ReferenceQueue<Object> queue;//引用队列
ps:判断弱引用持有的对象是否已经被执行了Gc垃圾回收
final HeapDump.Listener heapdumpListener;//堆信息监听器
watch():
public void watch(Object watchedReference, String referenceName) {
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
if (debuggerControl.isDebuggerAttached()) {
return;
}
final long watchStartNanoTime = System.nanoTime();
//对引用添加一个随机数作为唯一的key值
String key = UUID.randomUUID().toString();
//将获取到的key加入到前面的持有待检测以及产生内存泄漏引用的Set集合当中
retainedKeys.add(key);
//创建一个含有key值的弱引用
final KeyedWeakReferencereference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
//C、开启一个异步线程分析创建好的弱引用的内存泄漏问题
watchExecutor.execute(new Runnable() {
@Override public void run() {
ensureGone(reference, watchStartNanoTime);
}
});
}
ensureGone()://确保Activity已经进入了Gone这个状态(是否被真的回收了)
ps:在dump信息之前希望系统已经充分的进行了Gc垃圾回收而减少误判的可能。
void ensureGone(KeyedWeakReference reference,
long watchStartNanoTime) {
…
//1、计算调用watch()函数到调用Gc垃圾回收总共产生的时间
long watchDurationMs
= NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
//2、清除已经到达引用队列的弱引用,把已经回收的key从set<>集合中移除
removeWeaklyReachableReferences();
//3、如果处于debug状态则不会进行内存泄漏分析
if (gone(reference) || debuggerControl.isDebuggerAttached()) {
return;
}
//4、当前检测对象没有改变它的可达状态,就会手动的调用Gc垃圾回收器进行垃圾回收
gcTrigger.runGc();
//5、再次调用removeWeaklyReachableReferences清楚已经到达队列的弱引用
removeWeaklyReachableReferences();
//6、如果此时对象还没有到达队列那么预期被垃圾回收的队列可能会出现在这个队列当中,如果没有出现表示已经出现了内存泄漏了
if (!gone(reference)) {
…
//创建内存泄漏的信息文件
File heapDumpFile = heapDumper.dumpHeap();
…
//7、开始真正分析 内存泄漏的路径
heapdumpListener.analyze(
new HeapDump(heapDumpFile, reference.key, reference.name, watchDurationMs, gcDurationMs,
heapDumpDurationMs));
heapdumpListener.analyze():
@Override
public void analyze(HeapDump heapDump) {
checkNotNull(heapDump, "heapDump");
HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
}
runAnalysis():
处于IntentService当中,每次调用该方法都会回调到onHandleIntent()函数中
public static void runAnalysis(Context context, HeapDump heapDump,
Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
Intent intent = new Intent(context, HeapAnalyzerService.class);
intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
intent.putExtra(HEAPDUMP_EXTRA, heapDump);
context.startService(intent);
}
@Override protected void onHandleIntent(Intent intent) {
String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);
//堆内存的分析已经排除了系统内存所引起的内存泄漏
AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
}
heapAnalyzer.checkForLeak():进一步分析内存。将前面创建好的.hprof文件输入解析成内存快照snapshot对象输出
public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
…
//根据需要检测的类的key referenceKey的值来查询结果中是否有需要的对象;
获取解析结果中检测的对象
IObject leakingRef = findLeakingReference(referenceKey, snapshot);
//若为空表示对象不存在,表示Gc的时候对象已经被清除了
if (leakingRef == null) {
return noLeak(since(analysisStartNanoTime));
}
//若对象不为空则 进入findLeakTrace() 查询整个路径产生内存泄漏
AnalysisResult result =
findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, true);
if (!result.leakFound) {
result = findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, false);
}
findLeakingReference():如何查找产生内存泄漏的引用,通过集合中的迭代器进行不断的便利
private IObject findLeakingReference(String key, ISnapshot snapshot) throws SnapshotException {
leakCanary 是通过一个弱引用查找所需要的对象的
//1、创建一个class对象,针对需要检测的类 构造了一个含有弱引用的对象,通过查找弱引用就可以找到需要内存泄漏的对象,在snapshot快照中找到的第一个弱引用实际上就是
Collection<IClass> refClasses =
snapshot.getClassesByName(KeyedWeakReference.class.getName(), false);
//2、创建一个迭代器 获取弱引用实例的唯一标识id
IClass refClass = refClasses.iterator().next();
int[] weakRefInstanceIds = refClass.getObjectIds();
18515600025
for (int weakRefInstanceId : weakRefInstanceIds) {
IObject weakRef = snapshot.getObject(weakRefInstanceId);
//3、通过for遍历weakRefInstanceIds 数组得到实例, 找到需要的key值
String keyCandidate=
PrettyPrinter.objectAsString((IObject) weakRef.resolveValue("key"), 100);
//4、当查询到Key值和最开始定义封装的key值相等的时候,说明找到了需要的检测对象,该对象就是内存泄漏的引用并返回该对象
if (keyCandidate.equals(key)) {
return (IObject) weakRef.resolveValue("referent");
}
}
findLeakTrace():根据前面已经好到的内存泄漏的引用,找到最短泄漏内存路径
private AnalysisResult findLeakTrace(long analysisStartNanoTime,
ISnapshot snapshot,
IObject leakingRef,
String className,
boolean excludingKnownLeaks) throws SnapshotException {
ExcludedRefs excludedRefs = excludingKnownLeaks ? this.excludedRefs : baseExcludedRefs;
//不能被垃圾回收器回收的对象gcRootsTree
PathsFromGCRootsTree gcRootsTree = shortestPathToGcRoots(snapshot, leakingRef, excludedRefs);
//找不到内存泄漏时候Gcroot的判断,返回一个neLeak 返回值
// False alarm, no strong reference path to GC Roots.
if (gcRootsTree == null) {
return noLeak(since(analysisStartNanoTime));
}
//生成的内存泄漏的调用栈,发生内存泄漏的时候展示在屏幕上的就是LeakTrace
LeakTrace leakTrace = buildLeakTrace(snapshot, gcRootsTree, excludedRefs);
return leakDetected(!excludingKnownLeaks, className, leakTrace, since(analysisStartNanoTime));
}
小结
1、解析hprof文件,把这个文件封装成snapshot内存快照
2、根据弱引用和前面定义的key值,确定泄漏的对象
3、找到最短泄漏路径,作为结果反馈出来
四、 LeakCanary相关补充知识
4.1 Application
- Application-单例
同四大组件一样(4大组件都能获取到application对象且获取到同一份application对象)也是Android的一个系统组件用于存储系统的一些信息,是生命周期最长的一个组件和app共生死。Application是一个单例对象,每一个程序都只会创建一个单例对象。
启动Application系统会创建一个进程PID,在这个程序中的所有Activity都会在这个进程当中去运行。启动时候会创建并实例化这个对象
- Application是一个全局实例对象
Application 代表了一些全局的信息 ,application 继承自ContextWrapper
- Application持有最长的生命周期
它的生命周期=App程序的生命周期。
Application的应用场景
1、初始化全局对象、环境配置变量
2、获取应用程序当前的内存使用情况,从而更合理的调配内存使用率,避免程序被系统kill掉
3、监听应用程序内所有Activity生命周期(对app activity生命周期进行全局的控制)
4、监听应用程序配置信息的改变
Application源码
//Application的onCreate()方法默认情况下是空实现,可以继承Application类然后重写其onCreate()在其内进行初始化操作,和配置环境变量
public void onCreate() {
}
onTrimMemory():
通知应用程序 ,当前内存使用的情况,同时他的情况会根据内存级别来进行识别,根据内存使用情况进行内存的不同程度的释放
内存级别划分:
TRIM_MEMORY_COMPLETE,:内存不足,该进程在后台进程列表的最后一个,马上要被清理的内存对象
TRIM_MEMORY_MODERATE,:内存不足,该进程在后台进程列表的中部,并不会被立即清理
TRIM_MEMORY_BACKGROUND,:内存不足,同时该线程是后台进程
TRIM_MEMORY_UI_HIDDEN,:内存不足,并且这个进程的Ui已经是不可见状态
TRIM_MEMORY_RUNNING_CRITICAL,:内存不足,这个进程的优先级比较高,可以优先清理该内存
TRIM_MEMORY_RUNNING_LOW,:内存不足,这个进程的优先级比较高,需要清理内存,优先级高于CRITICAL
TRIM_MEMORY_RUNNING_MODERATE,:内存不足,这个进程的优先级是后三个当中最高的,需要及时清理内存
onLowMemory():监听android系统整体内存较低时候的回调方法,android4.0之前检测系统内存情况的函数
onConfigurationChanged():监听应用程序配置信息发生改变时候的操作
onTerminate():应用程序结束的时候会回调这个方法
4.2 Android 性能数据上报
4.2.1 性能接近思路
a、监控性能情况-基础
比如cpu、内存、卡顿、网络流量,页面加载流畅度等,量化数据指标
b、根据上报统计信息 定位问题-方向
c、持续监控并观察-收集
前后观察多个版本查看数据的有效性来进行进一步优化
4.2.2 性能种类
a、资源消耗类
电量、流量
b、流畅度
- 收集UI的流畅度体验
- App启动的时常
4.2.1 各个性能数据指标
思考:从哪几个角度收集数据、统计、上报、定格问题解决
a、网络请求流量测量
通过运营商的网络访问Internet运营商替我妈的手机转发数据报文(上下行,包含ip头),数据报文的总大小字节量即是流量
解决办法:
在日常开发中可以通过tcpdump+Wireshark抓包测试,获取手机root权限的电脑端抓包
TrafficStats类-android SDK 提供的 linux读取文件系统的文本解析
ps:该类中通过一些静态方法获取到需要的一些信息
b、冷启动
冷启动 /热启动 区别: 进程舒服被kill掉
冷启动:没有这个app进程,从0开始第一次启动。或是说系统重启后没有该app的进程
热启动:app从前台切换到后台,再返回就是热启动
命令行获取冷启动时间:
adbshell am start -W packagename/MainActivity(首页Activity)
日志打印获取冷启动时间:
起点->终点——》终点-起点 =冷启动时间
起点: Application的onCreate()方法中
终点:首页Activity的Oncreate()加载完成
c、卡顿
1) Fps 帧率
帧率:页面刷新的频率
在Android中通过 Choreographer() 获取帧率信息,记录每一帧渲染的时间,下一帧再处理的时候不仅可以看是否有掉帧现象,而且整个检测过程都是实时处理的
ps:只要出现掉帧现象就会出现Choreographer这个日志在里面
Choreographer():本身也会占用性能
VSYNC
系统会发出垂直同步信号,可以等同于硬件中断意思。当发出信号后会通知整个界面进行重绘
每次同步的周期是16.6ms。一周的刷新频率。若两次绘制的间隔时间超过16.6ms 说明存在卡顿现象
流畅度:
=实际帧率/理论最优帧率
2)主线程消息处理时长——衡量应用流畅度 非常重要!!而且有效的手段——debug版本或是小范围模块开发版本
原理: 在主线程消息前后都会用log信息打印消息输出日志,监测上下值的差是否小于阀值,小于说明正常,大于说明卡顿现象存在。
根据设备不同设定不同的阀值!!
匹配消息信息本身也会造成性能的损耗,因为通过匹配日志的字符串找到卡顿信息所以这个方法虽然可以拿到用户需要的信息但是无法避免造成一定情况下的损耗(字符串拼接的时候会产生很多临时对象)。
d、内存占用
RAM :一般意义上的总体内存,只需要知道总体内存
PSS:应用占用的实际物理内存(系统会平均的分给每个应用)
heap(最关心):虚拟机堆内存,与代码的好坏有直接关系,heap分配不合理肯定会影响app流畅度,因为GC会阻塞主线程造成卡顿
来源:oschina
链接:https://my.oschina.net/u/4367968/blog/3903682