Andorid 知识梳理: 性能优化基本知识

java的内存区域如何划分

有两种说法:
- 从抽象的JVM的角度看: 堆(Heap),栈(Stacks)方法区(MethodArea),运行时常量池(RuntimeConstant Pool),本地方法栈(NativeMethod Stacks),PC Register(PC寄存器)。
- 从操作系统上的进程的角度看:堆(Heap),栈(Stacks),数据段(data segment),代码段(code segment)。

Heap/Stack

  • Heap:Heap内存的分配也叫做动态内存分配,java中运行环境用来分配给对象和JRE类的内存都在堆内存,C/C++有时候可以用malloc或者new来申请分配一个内存。在C/C++可能需要自己负责释放(java里面直接依赖GC机制)。
  • Stack:Stack内存是相对于线程Thread而言的, 在执行函数(方法)时,函数一些内部变量的存储都可以放在栈上面创建,函数执行结束的时候这些存储单元就会自动被释放掉。栈内存包括分配的运算速度很快,因为内置在处理器的里面的。当然容量有限。它保存线程中方法中短期存在的变量值和对Heap中对象的引用等.

区别:堆是不连续的内存区域,堆空间比较灵活也特别大。
栈是一块连续的内存区域,大小是由操作系统觉决定的。堆管理很麻烦,频繁地new/remove会造成大量的内存碎片,这样就会慢慢导致效率低下。对于栈的话,他先进后出,进出完全不会产生碎片,运行效率高且稳定。

我们通常说的内存泄露,GC,是针对Heap内存的. 因为Stack内存在函数出栈的时候就销毁了。
比如说这个类

public class People{
    int a = 1;    //堆
    Student s1 = new Student(); //堆
    public void XXX(){
        int b = 1;  //栈
        Student s2 = new Student();  //new出的对象存在堆中,栈内存中持有s2的引用
    }
}

总结:
- Heap: 存储类对象(成员变量);空间大但不连续;易存在内存碎片
- Stack:存储函数(局部变量);空间小但连续,且运算速度快(内置处理器);栈为先进后出

java四大引用

Java中有四种强度不同的引用,从强到弱依次是:
- 强引用>弱引用>软引用>虚引用。

它们均为java.lang.ref.Reference抽象类的实现类,实现不同则垃圾回收的处理策略不同。

如果指向一个实例的所有引用中最强的引用是强/软/弱引用,则称该实例处于强/软/弱引用可达(strongly/softly/weakly reachable)的状态(下文也用该方式描述)。一个对象的引用可达性状态会影响GC回收的表现。

强引用(Strong Reference)

Book book = new Book();  //最常见的一种创建对象方式

GC不回收强引用可达(strongly reachable)的对象。只有在强引用被置为null时(弱化),才会在被回收。

软引用(SoftReference)

用来描述一些还有用但并非必须的对象,如缓存Cache。 当一次GC后发现内存不足,则会把软引用对象回收。如果回收后仍不足,才会抛出内存溢出异常。所以也可以说:软引用对象在抛出OutOfMemoryError之前已经回收。

弱引用(WeakReference)

被弱引用关联的对象只能生存到下一个垃圾收集之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

WeakReference weakData = new WeakReference(data);
if(weakData.get()!=null){
//do what you like
}

虚引用 (PhantomReference)

虚引用的get()方法永远返回null,这是因为虚引用的设计目的不是获取对象实例,而是作为对象已清除(finalized)、GC准备回收内存(reclaim memory)的信号。 虚引用要配合ReferenceQueue使用,虚引用构造时必须传入一个ReferenceQueue对象。

虚引用在对象从内存中完全移除后插入引用队列。通过ReferenceQueue.remove()可以获得有新引用对象插队的通知,灵活地进行最后的清理工作。(不推荐使用终结器finalize()方法来清理,执行时间不可靠、笨重,在终结器中创建一个强引用指向正在终结的对象会使对象复活,相关文章Finalization and Phantom References、Dalvik虚拟机 Finalize 方法执行分析)。

内存泄漏

内存泄漏
当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用,从而就导致对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏。内存泄露问题,在下篇博客中会详细介绍把内存泄露抓出来。

发生GC(回收弱引用)–> 内存仍然不足(回收软应用)–> 内存仍不足(OutOfMemoryError)

场景:
- 非静态内部类的静态实例
由于内部类默认持有外部类的引用,而静态实例属于类。所以,当外部类被销毁时,内部类仍然持有外部类的引用,致使外部类无法被GC回收。因此造成内存泄露。

public class TestAct extends AppCompatActivity {
    private static InnerClass mInnerClass = null;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.act_test);
        if (mInnerClass == null) {
            mInnerClass = new InnerClass();
        }
    }
    class InnerClass {
    }
}

解决:将该内部类设置为静态内部类(静态内部类不会持有外部类的引用)

  • 类的静态变量持有大数据对象
    静态变量长期维持到大数据对象的引用,阻止垃圾回收。

  • 资源对象未关闭
    资源性对象如Cursor、Stream、Socket,Bitmap,应该在使用后及时关闭。未在finally中关闭,会导致异常情况下资源对象未被释放的隐患。

  • 注册对象未反注册
    我们常常写很多的Listener,未反注册会导致观察者列表里维持着对象的引用,阻止垃圾回收。

  • Handler临时性内存泄露
    Handler通过发送Message与主线程交互,Message发出之后是存储在MessageQueue中的,有些Message也不是马上就被处理的。

  • Context泄露
    这个太多了,不细说,单利模式写的不恰当就属于这种。

Garbage Collector(垃圾回收器)

垃圾回收机制有好几套算法,java语言规范没有明确的说明JVM 使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做两件基本事情:(1)发现无用的信息对象;(2)回收将无用对象占用的内存空间。使该空间可被程序再次使用。

算法

1. 引用计数法(Reference Counting Collector)

当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

2. tracing算法(Tracing Collector) 或 标记-清除算法(mark and sweep)

2.1. 根搜索算法

根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

2.2. 标记-清除算法分析

标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片.

2.3. generation算法(Generational Collector)
- 年轻代(Young Generation)

1.所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收

4.新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)

  • 年老代(Old Generation)

1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

  • 持久代(Permanent Generation)

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

你可能感兴趣的:(安卓学习笔记,内存泄露,四大引用,堆栈,垃圾回收)