你所写的每一句代码,在内存里是怎么分布的,搞清楚这个问题,你对编程的理解又上升到一个高度了
前言
首先,如果对于java虚拟机的内存划分不清楚的同学,可以先去了解一下java虚拟机把java程序加载到内存以及内存的分布是怎样的,因为接下来一些知识点比如堆、方法区、虚拟机栈等是需要你对java虚拟机对于java程序运行时的内存划分区域有一定的认识,因此等理解之后再来看本文会有更好的效果。
提示:以下是本篇文章正文内容
一、代码在内存里的呈现是如何的
下面这个图很重要,表现出了一个java文件里的代码最终在内存里是怎么表现的:
把一个java文件编译成.class文件,然后java虚拟机在内存里开辟了一个空间,把.class文件加载进这个内存空间里,然后在这块内存空间里又划分了两个区,一个是线程共享区,一个是线程独占区,线程共享区里又划分为方法区和堆区,线程独占区又划分为java虚拟栈区、本地方法区和程序计数器区。class文件里的指令根据不同的所属就运行在不同的区里面,也可以说你在java文件写的每句代码最终就是根据不同所属,运行在不同的区域里。
下面我们在MainActivity里写个static方法:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}public static void testA() {}}
众所周知这个方法是可以直接通过MainActivity.a()直接调用,因为它是static修饰的,也因为这样它是属于方法区的,而方法区是我们的线程共享区,因此所有线程都可以去对它访问,因此可以直接通过类名点去访问它。
下面我们在代码里再写个代码:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}public void testA() {Person person = new Person();}class Person{private int age = 18;}}
可以看到我们new了一个Person对象,而这个new Person()相当于在堆区new了一个对象,所以它的内存分布是这样的:
person引用是局部变量区里的,因为它在方法testA()里,由它指向堆区里的Person对象。
接下来看图中的线程独占区,当我们每创建一个线程出来,跟随着也会有另一个新的线程区(java虚拟机栈、本地方法栈和程序计数器)被创建出来,比如因此当线程间来回切换时,CPU就是从这两个线程独占区里根据各自的线程计数器的记录,去运行到哪行代码去运行。线程区里的java虚拟机栈又划分为局部变量表、操作数栈、动态链接和返回地址,这些区域组成了栈帧,每个方法表示一个栈帧。此时我们的代码中的testA()方法就运行在java虚拟机栈的栈帧里。
所以一个方法对应一个栈帧,而当在执行testA方法的时候,又有一个方法testB()要执行:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);testA();}public void testA() {Person person = new Person();testB();}public void testB() {}class Person{private int age;}}
那么此时方法testB(栈帧)入虚拟java虚拟机栈里,排在方法testA(栈帧)上面,而当方法testB(栈帧)执行完之后,就会出栈,然后就是方法testA(栈帧)继续执行,执行完之后,方法testA(栈帧)出栈:
这时,如果在testA方法里创建一个新的线程,并在它的run方法里定义一个变量a:
public void testA() {Person person1 = new Person();new Thread(){@Overridepublic void run() {int a = 1;}}.start();}
相当于一个新的线程区被创建出来,也就是此时有两个java虚拟机栈了,一个是主线程的,另一个则是现在这个Thread线程的,Thread线程的这个栈帧里的局部变量表就有变量a,然后操作数栈里有1。这里要注意的是,如果我在testA方法里也就是Thread线程外定义多一个变量person1:
public void testA() {Person person = new Person();new Thread(){@Overridepublic void run() {int a = 1;Person person1 = person;}}.start();}
可以看到,在Thread线程里的run方法里,让主线程的变量person赋值给person1,这行代码其实在JDK1.8之前是会报错的,因为线程之间是不能访问的,所以我们在Thread线程里是无法访问到person的,所以此时是要用final去修饰person:
final Person person = new Person();
这时候person就会放到方法区里,变成共享了,这样其实就是通过一个第三者(公共者)来存值,从而实现传值,而两个栈没发生过共享数据的,而JDK1.8之后当在方法内匿名类引用局部变量(上面这种情况时),虚拟机默认已经替我们去修饰了,所以我们可以不用final也不会报错。
接下来再看回栈帧里的结构,它里面划分为局部变量表、操作数栈、动态链接和返回地址这四个区域:
现在可以来分析一下代码里这个testA方法,把它改动一下:
public int testA() {int a = 1;int b = 2;int c = a + b;return c;
// Person person = new Person();
}
可以看到,方法里定义了变量a、b、c,因此局部变量表里就分别压入变量a、b、c,然后操作数栈里也分别压入1,然后将它赋值给变量表里的a,然后操作数栈里压入2,又将它赋值给变量表里的b,然后操作数栈里压入从变量表里拿出的a和b的值,进行相加之后,得到的值3,然后将它赋值给变量表里的c,所以一些算术的操作其实是在操作数栈里进行的,变量表里记录变量及其存储值,当某个局部变量需要赋值的时候,就会从操作数栈里去取值赋值到局部变量表里的这个变量里:
过程一目了然,相信大家都不难理解。
动态链接
动态链接又是什么呢?首先要明白什么是链接,链接的官方概念是:链接是获取类或者接口并将其组合到java虚拟机的运行时状态以便可以执行的过程,它有三个步骤:验证、准备(初始化)和解析。听起来有点抽象,但其实很好理解,当你要调用某个对象的方法或者要访问这个对象的属性时,都是person.testA()或者person.age这样调用和访问,那么为什么能这样通过对象.就能调用,是因为我们知道了这个对象它的类在虚拟机的运行状态以及相关数据和地址,所以就可以这样方便地去访问和调用,而这个链接过程,实则就是帮我们去获取这个类的这些数据的,所以它里面也有这么一个步骤,就是:解析(将类的符号引用转换为直接引用),那这一步过后,就能让我们能通过对象.这样的形式去调用对象方法和访问对象属性了。先在代码里定义的内部类A用static修饰:
private static class Person{private int age = 18;
}
我们之所以不用通过new去构造Person对象,因为现在可以直接Person.testA()去调用方法,不用new对象,因为当你用static去修饰Person时,虚拟机在加载MainActivity时已经把static修饰的Person也去加载了,然后加载到方法区里,因此链接这个过程已经在这个时候做好了。
那么如果我们不加static去修饰Person类,那此时就不能通过Person.testA()这种形式去调用了,因为我们不知道person对象的链接情况啊,它没有被static修饰,那么类加载器也就没有加载该类,也就不知道它在虚拟机的运行状态以及相关数据和地址,自然就不能直接通过类.去访问和调用它了,所以这种情况,我们要写这句代码:
public void testB() {Person person = new Person();person.age = 20;}class Person{private int age = 18;}
没错,就是Person person = new Person(),我们要去new这个对象,完成一次链接过程,这种在我们new对象的时候去链接的做法,就是动态链接,而我们的栈帧里的动态链接区,记录的就是对象的动态链接情况。
返回地址
当程序要跳转到另一个方法执行时,执行完毕后,就要根据这个返回地址来返回到当前代码:
程序是从上往下执行的,当执行到int d = testA()代码时,很明显此时要去执行testA()方法,而返回地址区此时就会存储这句代码的地址,然后当testA()方法执行完毕后,CPU则要根据返回地址区里的这个地址返回到第14行代码继续往下执行,所以此时CPU就把testA()方法返回的结果赋值给这行代码里的d,然后继续执行下去。
总结
现在再来看Person person = new Person()这行代码,这个变量person也就是局部变量存在java虚拟机栈的局部变量表里,然后它的值便是对象Person()的地址,因此也就是person指向的是堆区里的Person()对象:
二、对象的生命周期
定义一个App类:
public class App extends Application {public static MainActivity.Person person;@Overridepublic void onCreate() {super.onCreate();}
}
App类里定义了一个MainActivity.Person的变量,而且是static修饰的,然后改动MainActivity里的testA()方法:
public void testA() {
// int a = 1;
// int b = 2;
// int c = a + b;Person person1 = new Person();App.person = person1;}
可以看到,把person1变量的值赋值给App的静态变量person,那么也就是相当于此时有两个变量都指向堆区里Person()这个对象,所以当testA()方法执行完毕后,指向Person()对象的person1引用这条线会被抹掉,然后Person()对象会被回收,但此时App生命周期还没结束的,而它的静态变量person仍然指向Person()对象,因此Person()对象就不会被回收,这样就导致内存泄漏了。
所以,关注内存有三方面需要搞清楚:
1)代码在JVM中是怎么存在的(上面已经讲清楚,栈帧、堆等各区之间关系)
2)某个对象在内存中到底占用多少内存
3)某个对象的生命周期
对象在内存中到底占用多少内存
先来看这张图:
一个java对象分为三部分,对象头、实例数据和对齐补充部分,各自所占的大小如图已经说得很清楚,其中实例数据就是我们定义的属性,其他一些比如对象头里的锁状态这些是一些配置信息数据,对齐补充里的填充数据主要作用是根据该对象在算完实例数据加对象头之后的大小如果不是8字节的整数倍,则会另外填充适当的大小,把整个对象大小变成8字节的整数倍。至于为什么是8字节的整数倍,这跟内存的最小单位以及进制计算有关,这个不是本文的着重点,感兴趣的可自行去搜索研究。
Class Pointer就是记录new对象的时候赋值给哪个引用的,比如当初 A a = new A()时,那么A()对象里此时这个Class Pointer记录的就是a,表示a引用指向该对象,可以理解为指针。
现在来看这段代码:
class Person{private int age = 1;private String name = "abc";private Person person1 = new Person();
}
现在看着这段代码,再根据我们上面分析的对象内存分布,就可以知道实例数据里就是int类型的成员变量age,占四字节,然后是一个Person类型的person1变量,它作为reference引用占8字节,它此时指向另一个对象Person(),不过这里要记住这个Person()是新的Person()对象了,它是堆区的另一个开辟的对象Person。还有一个成员变量是String类型的name,它也是一个引用,占用8个字节大小,它指向的是“abc”对象,也就是堆里另一个对象。
相信经历前面所讲的内存以及对象里的划分,现在应该对你所写的代码有了一个更深的理解了吧,所以从现在开始,可以对自己所写的代码想象它在内存是怎样的,这样你去内存优化的时候,就会知道该怎么去写代码,才是最合理的。
对象的生命周期
private void createList() {for (int i = 0; i < 100; i++) {String a[] = new String[100];}
}
在方法createList()里new的是一个String类型的数组,其实就是等于new了一个连续内存空间,100个对象内存块连在一起组成的一个内存块。而此时a[]变量实质上在局部变量表里也是100块,然后各自指向堆区里这块连续内存空间(100块小块),而每块小块里则有个成员变量String,各自对应又指向的是堆区里另一个对象“XXXX”,这就是数组类型的内存呈现:
而且又是循环100次,那么这个方法一下子创建这么多个对象,根据之前讲解的对象的内存大小,我们可以知道这个方法一下子创建了很大一块内存空间来装载这些对象,所以其实这样在一个方法里new这么多对象是很危险的,因为容易引起内存抖动(不断地开辟对象与回收对象)。容易引起内存不够用,因为连续开辟这么多块连续的内存空间,内存里并不都是随时会有一块连续个空间空出来的内存空间的,这样就一直得重新另起一行或者另起一块区域,来建立这么一大块连续内存空间,很浪费内存。
所以这就是一个很好的例子,在写代码的过程中,根据所写的代码,来想象内存中到底发生了怎样的变化与分布,然后根据对象的内存大小来估计有没有引起内存泄漏或者内存溢出的情况,然后来优化你的代码。
GcRoot
一个对象什么时候会被认为可以回收?
当某个对象始终是在一条有GcRoot连着的引用链上面,它就不会被回收,要让它能被回收,就要切断它的引用GcRoot,让它没有GcRoot,这样它就会被回收,而生命周期很长的变量或者对象就可以作为GcRoot。
如图,如果我想要让Person对象被回收,那么就要切断GcRoot引用,让它不再指向后面的对象,这样整条引用链就没有GcRoot了,因此Person对象也可以被认为是可以回收了。
静态变量、常量、局部变量表(也就是方法里的变量)、本地变量表(C/C++方法里的变量)都可以作为GCRoort。方法里的变量之所以可以是因为它如果中途就被回收掉,那么后面如果要用到它不就不能执行了吗,这应该很好理解。
既然知道了什么对象能被回收,我们再来一个例子,定义一个管理观察者的管理类:
public class ManagerObservable implements TestObserver{private static ManagerObservable instance;private List<TestObserver> list = new ArrayList<>();private ManagerObservable() {}public static ManagerObservable getInstance() {if (instance == null) {instance = new ManagerObservable();}return instance;}public void register(TestObserver managerObserver) {list.add(managerObserver);}public void unRegister(TestObserver managerObserver) {list.remove(managerObserver);}@Overridepublic void runObserve() {//可以遍历所有订阅者managerObserver,然后调用它们各自的runObserve方法}
}
代码很好理解,这是一个管理注册了TestTestObserver的观察者的管理类,这个TestTestObserver是个接口:
public interface TestObserver {void runObserve();
}
回到ManagerObservable,它里面是用单例模式去构造自己,也就是getInstance()方法,然后就是注册观察者register()和解注册方法unRegister(),解注册方法很重要,就是用来防止内存泄漏的。runObserve()方法则是遍历每个观察者,然后调用它们的具体实现方法。这就是一个简单的仿照观察者模式所写的例子,观察者模式是一对多,而普通的接口模式则是一对一。
然后在Activity里可以这样注册调用:
public class TestActivity extends AppCompatActivity implements TestObserver{@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_test);setButtonToClick();}private void setButtonToClick() {findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {ManagerObservable.getInstance().register(TestActivity.this);}});}@Overridepublic void runObserve() {}
}
这样TestActivity就已经作为观察者被注册到ManagerObservable里的list集合里被ManagerObservable管理,但此时别忘了ManagerObservable对象也就是instance它是static修饰的,它是在方法区里的,所以此时不妨想象一下内存分布:方法区里的instance变量指向堆区里的ManagerObservable()对象,而ManagerObservable()对象里的结构含有list(实例数据),因为我们调用完ManagerObservable.getInstance()方法后构造出ManagerObservable()对象后,又接着调用register(TestActivity.this)方法,而register()方法是这样的:
public void register(TestObserver managerObserver) {list.add(managerObserver);
}
所以ManagerObservable()对象里的list指向了堆区里的另一个对象new ArrayList<>,然后new ArrayList<>对象里填充的第一个元素对象的值就是TestActivity类型的managerObserver对象,整个内存分布如图所示:
当TestActivity销毁时,因为instance是static修饰的,它的生命周期是跟app一样的,因此它还在,而它的list依然保存着managerObserver对象,也就是TestActivity对象,因此就造成内存泄漏。
此时的引用链就是这样:
根据上文我们说到的GcRoot,我们要让TestActivity对象得以回收,就必须断掉它的GcRoot,因此我们ManagerObservable的解注册方法unRegister()就派上用场了:
public void unRegister(TestObserver managerObserver) {list.remove(managerObserver);
}
它就是让list清空掉managerObserver对象,在TestActivity的onDestroy()里调用它:
@Overrideprotected void onDestroy() {super.onDestroy();ManagerObservable.getInstance().unRegister(TestActivity.this);
}
这样就等于断了activity对象跟GcRoot的联系,让TestActivity对象没有GcRoot,从而可以被回收,不再内存泄漏。
垃圾回收
有看过内存条的同学应该都知道它是一格一格的,每一格便存储一个位的数据,而java虚拟机之所以要那样抽象地把内存划分不同的区域,是为了方便管理和跟踪不同的数据,这样也便于去回收数据,而垃圾回收机制便是根据虚拟机对每个数据的状态的记录去决定要不要回收的一套机制。
前面我们已经知道,当对象被虚拟机认为没有GcRoot引用他们的时候,他们就是垃圾对象,虚拟机就会对他们进行回收,而回收的方法就是垃圾回收算法。
1)标记-清除法
分两个阶段:标记和清除。标记所有可以回收的对象,然后统一进行回收。但是这样会产生很多不连续的内存碎片,这样碎片过多时,此时又需要较大的内存分配,这样就会因为内存不够又再次触发GC:
2)标记-复制法
将内存区域划分为两块大小一致的区域(比如A和B),每次只使用其中一块(比如A),当A块的内存用完了之后就将还存活的对象复制到B区域上去,然后将当前块A的内存空间一次清理掉:
这样也有缺点,将内存区域划分为两块,只能使用另一块,这相当于只能使用原来的一半空间,对内存浪费太大,而且每次都要复制大量的存活对象,这需要消耗性能,而且效率也必然下降。
3)标记-整理法
先标记所有可以回收的对象,然后将存活的对象向内存空间的一端进行移动,将所有存活的对象都连续的放在一起,然后直接清理掉边界以外的内存:
当然,标记整理法还是不可避免的造成性能开销大,但它解决了内存空间不浪费的问题。
至于以上三种方法要什么情况下使用,这就要引申出另一个知识点,那就是根据垃圾回收机制,我们可以把堆区又分为以下这些区,然后根据不同的区,则使用对应的垃圾回收算法去回收垃圾对象,如图:
可以看到,堆区分为新生代和老年代。
老年代占据堆空间的2/3,主要存放应用中生命周期长的对象,老年代不会频繁进行垃圾回收,在经历了年轻代也没有空间存储对象,然后就会进入老年代,而当老年代的空间也不足的时候,才会触发GC进行垃圾回收。
新生代占据堆空间的1/3,又细分为三个区:Eden区、SurvivorFrom、ServivorTo区,三个区的默认比例为:8:1:1。新生代保存着大量的刚刚创建的对象,而新生代中会频繁进行垃圾回收,所以新生代里的对象一般在经历被创建后,当被标记为垃圾对象后又会在里面很快被回收。首先大部分对象会先分配在Eden区,当Eden区内存不够的时候,就会触发GC进行一次垃圾回收,如果还是不够则会把对象移动到SurvivorFrom区,重复以上的步骤,最后对象移动到SurvivorTo区中,如果经历了好几次后SurvivorTo区也还是不够内存时,那么就把对象移动到老年代里去。
根据以上这些区域的特性,可以知道老年代中的对象大部分都不会被清理掉,只存在于少部分的对象会被清理,所以在老年代中不会存在标记大量对象并清除的情况,因此使用标记清除算法来回收老年代的垃圾对象是比较合适的。当然,如果还是在老年代里出了很多需要清除的垃圾对象,那么使用标记清除法还是会造成大量内存碎片,那么此时使用标记整理算法也是不错的选择。
而新生代区域中,因为其频繁进行GC,所以它里面存活的对象是很少的,所以要复制的对象也就少,也就不会消耗很大性能,而至于浪费内存空间这事,因为它是分为了Eden区(占8/10)而Survivor区(占2/10),这就比原来只能使用的1/2空间高了许多,也就减少了浪费内存空间这个缺点。
三、内存溢出与内存泄漏
经过前面对内存分布的讲解之后,相信大家在写代码时又多了一层理解,那么接下来可以继续讲解内存溢出与内存泄漏是怎么一回事。
内存溢出
程序在申请内存时,没有足够的内存空间使用就会出现out of memory,即内存不够用。Android系统一般为每个应用申请的内存为64M-128M,可以在清单文件中设置android:largeheap=”true”,从而给App申请更大的内存。
内存溢出分为堆溢出和栈溢出,本地方法栈属于native层,所以不用管,而程序计数器不会发生内存溢出,方法区尽管会发生,但几率很小,所以也可以忽略不计。所以也就是堆和虚拟机栈才是发生内存泄露和内存溢出的高发区。
下面是经典的发生内存溢出的情况:
1)生产者与消费者模型,注册回调,忘记注销,然后将某对象添加到队列时忘记控制队列大小等:
private void addObject() {ArrayList<Person> list = new ArrayList<>();while (true) {list.add(new Person());}
}
这里不断在堆区里创建对象,堆区最后会爆掉,也就是内存溢出。
2)常见的把服务器返回的json数据解析成java bean时,因为循环引用而内存溢出,比如fastjson对象转json string时因为内层对象的某个属性正好是外层对象,那这样就会循环引用,导致解析时死循环而内存溢出报错:
public class Person {private People people;private String name;
}public class People {private Person person;private int age;
}
如上面所示,Person类里有People属性,而People类里也有Person属性,这就容易在解析时造成死循环,从而内存溢出。
3)递归使用不恰当会造成栈溢出,调用方法的过程是在栈区进行,上文已经讲过,因此递归使用不恰当就会造成栈区爆掉,也就是栈溢出。
内存泄漏
我们都知道,创建一个对象,也就是在堆区里申请多一块空间给它,如果某个对象一直持有这块空间,该空间无法得到释放。所以如果内存泄露的次数多,最终很容易引起内存溢出的。
下面是一些经典的内存泄漏的情况:
1)单例模式
public class MyApp extends Application {private static MyApp instance;private Context context;private MyApp(Context context) {this.context = context;}public static MyApp getInstance(Context context) {if (instance == null) {instance = new MyApp(context);}return instance;}
}
不考虑锁的情况,以上是一种常见的单例模式的写法,然后在外部调用:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);MyApp.getInstance(this);}
}
这样MainActivity对象就作为context被MyApp的context所引用,而此时MyApp对象也就是instance它是static修饰的,属于方法区的,因此instance的生命周期要比MainActivity长,当MainActivity销毁后(执行完onDestroy),instance生命周期是进程结束才结束,再加上它的context现在持有(指向)了MainActivity的context,那么意味着就算MainActivity销毁了,它还是被instance的context引用指向着,而根据GCroot回收垃圾原则,这条引用不断,context(MainActivity对象)不会被回收,所以内存泄露了。
为了防止这种情况,可以这样:
public class MyApp extends Application {private static MyApp instance;private Context context;private MyApp(Context context) {this.context = context.getApplicationContext();}public static MyApp getInstance(Context context) {if (instance == null) {instance = new MyApp(context);}return instance;}
}
原先的MyApp的context引用指向的是MainActivity的context对象,现在改成MyApp的context引用指向的是Application的context对象,所以MainActivity销毁后就再也没别的引用指向它,也就能被回收,而Application的context对象跟MyApp的context的生命周期都是一样,app结束context就会被回收,这样就解决了内存泄漏问题。
2)静态变量持有的引用导致的内存泄露
来看代码:
public class MainIndexMainActivity extends AppCompatActivity {private static People mPeople;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);if (mPeople == null) {mPeople = new People(this);}}class People{private Activity activity;public People(Activity activity) {this.activity = activity;}}}
定义了一个People类,它有个属性activity,随后在MainIndexMainActivity里定义了静态的mPeople变量,静态变量是存储在方法区,所以People()对象的生命周期是从类加载开始到整个应用程序结束它才会被回收,而它被new的时候,也进行了this.activity = activity,因此想象一下内存结构:
整条引用链如图所示,所以即使MainIndexMainActivity执行完onDestroy()方法被销毁后,但它仍然因为被持有,因为mPeople的生命周期是直到应用结束它才被回收,所以它在MainIndexMainActivity执行完onDestroy()后还会存在,因此一直引用着MainIndexMainActivity()对象,而导致MainIndexMainActivity()不能被回收,从而内存泄漏。
解决方法:
@Overrideprotected void onDestroy() {super.onDestroy();if (mPeople != null) {mPeople = null;}}
很简单,断掉mPeople对它的引用不就好了。所以还是那句话,根据自己所写的代码,想象它们的内存分布,创建的对象会占用多少内存空间,自然就知道会不会造成内存泄漏和溢出。
3)非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象生命周期长时,就会内存泄露,比如常见的Handler,Thread,AsyncTask:
public class MainIndexMainActivity extends AppCompatActivity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);startHandler();}private void doSomething() {//MainIndexMainActivity里某个方法}private void startHandler() {Message message = Message.obtain();message.what = 1;handler.sendMessage(message);}private Handler handler = new Handler(){@Overridepublic void handleMessage(@NonNull Message msg) {if (msg.what == 1) {MainIndexMainActivity.this.doSomething();}}};
}
其实并不是说所有非静态内部类就一定会发生内存泄露,还是回归到本质,就是当这个非静态内部类的生命周期要比外部类对象长,才会内存泄漏,而Handler它是被ThreadLocal绑定的,所以它的生命周期是比activity要长,当发送完消息时activity退出销毁,而此时因为handler还在接收消息,它生命周期还在,还仍然作为GcRoot指向MainIndexMainActivity()对象的,所以MainIndexMainActivity()对象不会被回收的。解决方法是:让该非静态内部类变为静态,然后使用弱引用来引用外部类对象:
private static class MyHandler extends Handler{private WeakReference<MainIndexMainActivity> activityWeakReference;public MyHandler(MainIndexMainActivity activity) {activityWeakReference = new WeakReference<>(activity);}@Overridepublic void handleMessage(@NonNull Message msg) {MainIndexMainActivity activity = activityWeakReference.get();if (activity != null) {if (msg.what == 1) {activity.doSomething();}}}
}
改为静态,为了让它不再持有外部类MainIndexMainActivity引用,但这还不够,因为静态修饰的类生命周期还是比MainIndexMainActivity要长,因此这里还要使用弱引用来引用MainIndexMainActivity,弱引用的作用是让对象拥有更短暂的生命周期,垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存,这就不会导致内存泄漏了。
除了Handler之外,常见的还有创建一个线程去作网络请求时,如果此时退出activity时由于thread还在请求中,它仍然存活,因此继续持有activity引用,导致activity对象没法回收,解决的方法同上。
4)未取消注册或回调导致的内存泄漏
如果在Activity里注册了内部类广播,而当Activity销毁时不注销广播,那么广播一直存在系统中,它作为内部类同样是持有了activity的引用,生命周期又比activity长,从而导致activity对象不能被回收而内存泄漏。解决方法就是在activity的onDestroy方法里注销广播器:
public class MainIndexMainActivity extends AppCompatActivity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);this.registerReceiver(mReceiver, new IntentFilter());}private BroadcastReceiver mReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {//收到广播后的逻辑}};@Overrideprotected void onDestroy() {super.onDestroy();this.unregisterReceiver(mReceiver);}
}
所以现在你是从内存的角度去分析,明白了为什么每次使用广播都要在onDestroy()方法取消注册了吧。
5)集合中的对象未清理造成内存泄漏。
public class MainIndexMainActivity extends AppCompatActivity {private List<Person> list = new ArrayList<>();@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initData();}private void initData() {for (int i = 0; i < 50; i++) {Person person = new Person();list.add(person);person = null;}}}
很常见的集合添加元素对象的写法,此时又要发挥你的想象,内存分布是怎样的:
我们都知道因为对象Person添加到集合中,所以集合本身也是个对象也要被回收的,所以就算令对象person引用指向null之外,仍然有内存泄漏的风险,所以要清理干净,把集合list里的元素变量也要清空,然后断了list的引用链:
@Overrideprotected void onDestroy() {super.onDestroy();if (list != null) {list.clear();list = null;}
}
6)资源未关闭或未释放导致内存泄漏,比如流对象、WebView等使用完后要及时关闭。
WebView是持有Activity的引用的,当Activity已经退出了之后,WebView仍然在请求和加载数据,没有销毁,所以就导致Activity对象无法被回收从而内存泄漏,即使是调用WebView.destroy()仍然不够,要彻底解决还需要在销毁WebView之前先将WebView从父容器中移除后再调用WebView.destroy()销毁WebView,这样就能彻底回收WebView和Activity了:
@Overrideprotected void onDestroy() {super.onDestroy();parentLinearLayout.removeView(webView);webView.stopLoading();webView.removeAllViews();webView.destroy();}
内存抖动
内存抖动就是短时间内有大量的对象被创建或者被回收的现象。那本质其实就是频繁地创建新对象(比如循环内不断new对象),比如在onDraw()方法里创建Paint、Path这些对象:
@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);Paint paint = new Paint();Path path = new Path();canvas.drawBitmap(...);}
众所周知,onDraw()方法在绘制过程中是不断被触发的,如果绘制的控件复杂,时长也变长,那么就相当于这段时间不断在创建很多个Paint()和Path()对象,然后每次绘制完就要回收一次对象,这样显然就会发生内存抖动:
如图所示,短时间内,不断创建对象,不断回收对象,造成了一次波动频繁的内存抖动。
所以在这些方法里最好不要用局部变量来new新对象,能使用全局变量就使用全局变量。当需要大量使用Bitmap对象时,缓存它们来复用,别的对象也尽量缓存起来复用,避免造成大量创建对象和回收对象的情况。
三、补充
说到这里,相信大家对内存空间都有了一个很深的理解了,那么另外再补充一点,看回内存的分布,可以知道有线程共享区和线程独占区,线程共享区里是方法区和堆区,而事实上堆和方法区都是在内存(物理内存)中,而线程独占区里的栈则是在高速缓冲区里,高速缓冲区是手机CPU里的“内存”,可以理解为它相当于CPU的“口袋”,所以这也让它的计算速度比在内存里要快(离CPU近嘛)。
假如是一个四核CPU,每个核处理一个线程,而每个核又有自己私有的高速缓冲区,所以虚拟机栈为什么是私有的,就是因为这个原因。