Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说性能优化:看完这篇文章,至少解决 APP 中 90 % 的内存异常问题,希望能够帮助你!!!。
在 Android 中我们写的 .java 文件,最终会编译成 .class 文件, class 又由类装载器加载后,在 JVM 中会形成一份描述 class 结构的元信息对象,通过该元信息对象可以知道 class 的结构信息 (构造函数、属性、方法)等。JVM 会把描述类的数据从 class 文件加载到内存,Java 有一个很好的管理内存的机制,垃圾回收机制 GC 。为什么 Java 都给我们提供了垃圾回收机制,程序有时还会导致内存泄漏,内存溢出 OOM,甚至导致程序 Crash 。接下来我们就对实际开发中出现的这些内存问题,来进行优化。
我们先来大概了解一下 Java 虚拟机里面运行时的数据区域有哪些,如果想深入了解 Java 虚拟机 建议可以购买<<深入理解 Java 虚拟机>> 或者直接点击我这里的 PDF 版本 密码: jmnf
程序计数器
虚拟机栈
本地方法栈
注意
方法区
java 堆 (本次需要优化的地方)
随着程序的运行,内存中的实例对象、变量等占据的内存越来越多,如果不及时进行回收,会降低程序运行效率,甚至引发系统异常。
目前虚拟机基本都是采用可达性分析算法,为什么不采用引用计数算法呢?下面就说说引用计数法是如果统计所有对象的引用计数的,再对比可达性分析算法是如何解决引用计数算法的不足。下面就来看下这 2 个算法:
每个对象有一个引用计数器,当对象被引用一次则计数器加一,当对象引用一次失效一次则计数器减一,对于计数器为 0 的时候就意味着是垃圾了,可以被 GC 回收。
下面通过一段代码来实际看下
public class GCTest {
private Object instace = null;
public static void onGCtest() {
//step 1
GCTest gcTest1 = new GCTest();
//step 2
GCTest gcTest2 = new GCTest();
//step 3
gcTest1.instace = gcTest2;
//step 4
gcTest2.instace = gcTest1;
//step 5
gcTest1 = null;
//step 6
gcTest2 = null;
}
public static void main(String[] arg) {
onGCtest();
}
}
分析代码
//step 1 gcTest1 引用 + 1 = 1
//step 2 gcTest2 引用 + 1 = 1
//step 3 gcTest1 引用 + 1 = 2
//step 4 gcTest2 引用 + 1 = 2
//step 5 gcTest1 引用 - 1 = 1
//step 6 gcTest2 引用 - 1 = 1
很明显现在 2 个对象都不能用了都为 null 了,但是 GC 确不能回收它们,因为它们本身的引用计数不为 0 。不能满足被回收的条件,尽管调用 System.gc() 也还是不能得到回收, 这就造成了 内存泄漏 。当然,现在虚拟机基本上都不采用此方式。
从 GC Roots 作为起点开始搜索,那么整个连通图中额对象边都是活对象,对于 GC Roots 无法到达的对象便成了垃圾回收的对象,随时可能被 GC 回收。
可以作为 GC Roots 的对象
GC 是需要 2 次扫描才回收对象,所以我们可以使用 finalize 去救活丢失的引用
@Override
protected void finalize() throws Throwable {
super.finalize();
instace = this;
}
到了这里,相信大家已经能够弄明白这 2 个算法的区别了吧?反正对于对象之间循环引用的情况,引用计数算法无法回收这 2 个对象,而可达性是从 GC Roots 开始搜索,所以能够正确的回收。
Object strongReference = new Object()
如果一个对象具有强引用,那垃圾回收器绝不会回收它,当内存空间不足, Java 虚拟机宁愿抛出 OOM 错误,使程序异常 Crash ,也不会靠随意回收具有强引用的对象来解决内存不足的问题.如果强引用对象不再使用时,需要弱化从而使 GC 能够回收,需要:
strongReference = null; //等 GC 来回收
还有一种情况,如果:
public void onStrongReference(){
Object strongReference = new Object()
}
在 onStrongReference() 内部有一个强引用,这个引用保存在 java 栈 中,而真正的引用内容 (Object)保存在 java 堆中。当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为 0 ,这个对象会被回收。
但是如果 mStrongReference 引用是全局时,就需要在不用这个对象时赋值为 null ,因为 强引用 不会被 GC 回收。
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存,只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收, java 虚拟机就会把这个软引用加入到与之关联的引用队列中。
注意: 软引用对象是在 jvm 内存不够的时候才会被回收,我们调用 System.gc() 方法只是起通知作用, JVM 什么时候扫描回收对象是 JVM 自己的状态决定的。就算扫描到了 str 这个对象也不会回收,只有内存不足才会回收。
弱引用与软引用的区别在于: 只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
可见 weakReference 对象的生命周期基本由 GC 决定,一旦 GC 线程发现了弱引用就标记下来,第二次扫描到就直接回收了。
注意这里的 referenceQueuee 是装的被回收的对象。
@Test
public void onPhantomReference()throws InterruptedException{
String str = new String("123456");
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);
System.out.println("PhantomReference:" + pr.get());
System.out.printf("ReferenceQueue:" + queue.poll());
}
虚引用顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列 (ReferenceQueue) 联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
引用类型调用方式GC是否内存泄漏强引用直接调用不回收是软引用.get()视内存情况回收否弱引用.get()回收不可能虚引用null任何时候都可能被回收,相当于没有引用一样否
工具很多,掌握原理方法,工具随意挑选使用。
产生的原因: 一个长生命周期的对象持有一个短生命周期对象的引用,通俗点讲就是该回收的对象,因为引用问题没有被回收,最终会产生 OOM。
下面我们来利用 Profile 来检查项目是否有内存泄漏
**Allocations:** 动态分配对象个数
**Deallocation:** 解除分配的对象个数
**Total count:** 对象的总数
**Shalow Size:** 对象本身占用的内存大小
**Retained Size:** GC 回收能收走的内存大小
这里我们得知是一个 ilsLoginListener 引用了 LoginView,我们来看下代码最后怎么解决的。
代码中我们找到了 LoginView 这个类,发现是一个单例中的回调引起的内存泄漏,下面怎么解决勒,请看第七小点。
**示例:**
```
//如果在开发调试中遇见 Android 源码或者 第三方 SDK 持有了我们当前的 Activity 或者其它类,那么现在怎么办了。
```
**解决方法**:
```
//当前是通过 Java 中的反射找到某个类或者成员,来进行手动 = null 的操作。
```
内存频繁的分配与回收,(分配速度大于回收速度时) 最终产生 OOM 。
也许下面的录屏更能解释什么是内存抖动
可以看出当我点击了一下 Button 内存就频繁的创建并回收(注意看垃圾桶)。
那么我们找出代码中具体那一块出现问题了勒,请看下面一段录屏
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
imPrettySureSortingIsFree();
}
});
/**
* 排序后打印二维数组,一行行打印
*/
public void imPrettySureSortingIsFree() {
int dimension = 300;
int[][] lotsOfInts = new int[dimension][dimension];
Random randomGenerator = new Random();
for (int i = 0; i < lotsOfInts.length; i++) {
for (int j = 0; j < lotsOfInts[i].length; j++) {
lotsOfInts[i][j] = randomGenerator.nextInt();
}
}
for (int i = 0; i < lotsOfInts.length; i++) {
String rowAsStr = "";
//排序
int[] sorted = getSorted(lotsOfInts[i]);
//拼接打印
for (int j = 0; j < lotsOfInts[i].length; j++) {
rowAsStr += sorted[j];
if (j < (lotsOfInts[i].length - 1)) {
rowAsStr += ", ";
}
}
Log.i("ricky", "Row " + i + ": " + rowAsStr);
}
}
最后我们之后是 onClick 中的 imPrettySureSortingIsFree() 函数里面的 rowAsStr += sorted[j]; 字符串拼接造成的 内存抖动 ,因为每次拼接一个 String 都会申请一块新的堆内存,那么怎么解决这个频繁开辟内存的问题了。其实在 Java 中有 2 个更好的 API 对 String 的操作很友好,相信应该有人猜到了吧。没错就是将 此处的 String 换成 StringBuffer 或者 StringBuilder,就能很完美的解决字符串拼接造成的内存抖动问题。
修改后
/**
* 打印二维数组,一行行打印
*/
public void imPrettySureSortingIsFree() {
int dimension = 300;
int[][] lotsOfInts = new int[dimension][dimension];
Random randomGenerator = new Random();
for(int i = 0; i < lotsOfInts.length; i++) {
for (int j = 0; j < lotsOfInts[i].length; j++) {
lotsOfInts[i][j] = randomGenerator.nextInt();
}
}
// 使用StringBuilder完成输出,我们只需要创建一个字符串即可, 不需要浪费过多的内存
StringBuilder sb = new StringBuilder();
String rowAsStr = "";
for(int i = 0; i < lotsOfInts.length; i++) {
// 清除上一行
sb.delete(0, rowAsStr.length());
//排序
int[] sorted = getSorted(lotsOfInts[i]);
//拼接打印
for (int j = 0; j < lotsOfInts[i].length; j++) {
sb.append(sorted[j]);
if(j < (lotsOfInts[i].length - 1)){
sb.append(", ");
}
}
rowAsStr = sb.toString();
Log.i("jason", "Row " + i + ": " + rowAsStr);
}
}
这里可以看见没有垃圾桶出现,说明内存抖动解决了。
注意: 实际开发中如果在 LogCat 中发现有这些 Log 说明也发生了 内存抖动 (Log 中出现 concurrent copying GC freed ....)
ps:我觉得这个只是为了应付面试,那么可以参考这里,我也只了解概念这里就不用在多写了,点击看这个帖子吧
也可以参考掘金的这一篇 GC 回收算法
最后思维导图做一个总结:
推荐阅读:2020最新Android大厂高频面试题解析大全(BAT TMD JD 小米)
2020最新BAT Android高端技术面试145题详解
2019年鸿洋大神最新整理一线互联网公司Android中高级面试题总结(附答案解析)
2017-2020历年字节跳动Android面试真题解析(累计下载1082万次,持续更新中)
作者:DevYK
今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
上一篇
已是最后文章
下一篇
已是最新文章