作为运行在Linux内核上的移动设备,手机受限于内存的大小,给每一个app都只能分配有限的可用内存,如果开发者的代码编写不当,可能导致应用的内存泄漏,进而导致OOM内存溢出(程序申请内存,但是系统无更多可用空间)。
为了避免内存泄漏,需要我们在平时的代码编写上多加注意,避免产生内存泄漏的情况。可用LeakCanary监控你的应用,当发生内存泄漏的时候他会在适当的时间给你发出警告。当然,这只是一种监控工具,我们还是需要知道如何去处理这个泄漏,并且要知道为什么会产生泄漏。
在java里,内存的释放虽然不需要我们手动去管理,但是对于存在引用的对象,java并不会去释放它,在这块java有两种机制去判断对象是否存在引用:
1.计数器,对象每引用一次就+1,当对象的引用计数为0时,判断该对象为垃辣鸡,赶快把它解决掉吧。但是这种方法对应对象间的相互引用可不好用了。
2.可达性判断,从GCRoot根部判断是否有能到达该对象的路径,如果有,则判断为该对象有引用,不能回收了。
可作为GC Roots的对象有:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象;
2.方法区中的类静态属性引用的对象;
3.方法区中常量引用的对象;
4.本地方法栈中JNI(即一般说的Native方法)中引用的对象
Java内存分配策略
对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次 Minor GC
大对象直接进入老年代
所谓大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代。为做到这一点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可通过参数-XX:MaxTenuringThreshold设置
动态对象年龄判定
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
Java 程序运行时的内存分配策略有三种,分别是静态存储区(也称方法区)、栈区和堆区。
静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。
Java垃圾回收算法
Mark-Sweep Collector(标记-清除收集器)
标记清除收集器停止所有的工作,从根扫描每个活跃的对象,然后标记扫描过的对象,标记完成以后,清除那些没有被标记的对象。
缺点是耗时长,内存碎片多
Copying Collector(复制收集器)
将内存分为两块,标记完成开始回收时,将一块内存中保留的对象全部复制到另一块空闲内存中。
缺点是需要额外的空间消耗
Mark-Compact Collector(标记-整理收集器)
标记整理收集器汲取了标记清除和复制收集器的优点,它分两个阶段执行,在第一个阶段,首先扫描所有活跃的对象,并标记所有活跃的对象,第二个阶段首先清除未标记的对象,然后将活跃的对象复制到堆的底部。
分代收集算法
当代商业虚拟机的垃圾收集都采用“分代收集(Generational Collection)”算法,该算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。
Java四种引用
强引用(Strong Reference)最常用的引用类型,如Object obj = new Object();。只要强引用存在则GC时必定不被回收。
软引用(Soft Reference)用于描述还有用但非必需对象,当堆将发生OOM(Out Of Memory)时则会回收软引用所指向的内存空间,若回收后依然空间不足才会抛出OOM。
弱引用(Weak Reference)发生GC时必定回收弱引用指向的内存空间。和软引用加入队列的时机相同
虚引用(Phantom Reference)又称为幽灵引用或幻影引用,虚引用既不会影响对象的生命周期,也无法通过虚引用来获取对象实例,仅用于在发生GC时接收一个系统通知。
GC的各种触发
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法去空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
一般来说,我们需要关注的只是堆这块空间,因为堆存放的都是对象的引用,这一块是很容易造成对象释放不及时而导致的泄漏。
Android常见的内存泄漏
1.匿名类跟非静态内部类默认持有外部类的引用,导致无法释放。解决办法是把它改为静态内部类,并使用弱引用,比如Handler内部类。
2.Handler因为其Message会默认持有Handler的引用,如果在对象释放的时候,MessageQueue还在不断的循环使用,那么将造成无法释放对象。
3.各种资源使用后未释放,比如IO的操作。
4.static使用不当,造成其生命周期伴随整个Application。常见的为单例模式中的Context,最好将其改为Application的Context。
另外,内存抖动也是影响性能的重要因数,在短时间内大量的创建和销毁对象就会导致这种情况的出现。所以在Ondraw方法中不要创建对象,因为android系统在每隔16ms会发出一次刷新界面的信号。对于线程的使用最好通过线程池来进行管理。