美文网首页内存
Android中缓存理解(一)

Android中缓存理解(一)

作者: 狮_子歌歌 | 来源:发表于2017-04-22 22:00 被阅读213次

    Java GarbageCollection(GC)

    Java不能像C/C++那样直接对内存进行操作(内存分配和垃圾回收)。

    由于JVM会自动回收(GC),Java程序员很难控制JVM的内存回收,只能根据其原理去编写代码,提高程序性能。正是因为这样的特性,导致Java程序员对内存管理方面束手无策:

    • 垃圾回收并不会按照程序员的要求,随时进行GC。
    • 垃圾回收并不会及时的清理内存,尽管有时程序需要额外的内存。
    • 程序员不能对垃圾回收进行控制。

    根据垃圾回收的规律,合理安排内存,提高程序性能,这就要求必须彻底了解JVM的内存管理机制。

    哪些对象需要GC

    JVM运行时数据区主要分为两大块:线程私有区和共享区。线程私有区(程序计数器、虚拟机栈、本地方法区)因其生命周期与线程同步,它们会随着线程的终结而自动释放内存,所以只有共享区需要进行GC。

    如果共享区中的一个对象不存在任何引用时,那么它可以被回收。

    Java GC回收的堆内存的对象,Java对象一般都是在堆中分配内存。

    GC时机

    在共享区内存的对象回收的条件是:没有任何作用时,就需要被回收。而实例回收的时机取决于GC算法。

    曾经的GC算法是引用计数法,每一个对象都有一个引用计数器。每被引用一次,计数器加1,失去引用,计数器减1。当对象的计数器一段时间内保持为0,那么认为可以被回收。

    但是这个算法有个明显的缺陷:当两个对象相互引用,但是二者已经没有作用时,按照常规,应该对其进行垃圾回收,但是其相互引用,又不符合垃圾回收的条件,因此无法完美处理这块内存清理。

    所以采用了根搜索算法,对象之间的引用构建成一个树,根元素是GC Roots对象。从根元素向下搜索,如果一个对象不能达到根元素,说明不再引用,即可被回收。

    Java的引用

    在JDK1.2后,引入了四中引用类型:强引用、软引用、弱引用和虚引用,它们对于GC有着不同的意义。

    强引用
    Object o = new Object();
    

    强引用是指使用关键词new创建一个实例,并将其赋值给引用类型变量引用,就是给该实例添加强引用。

    其特点是当内存不足时,JVM宁可抛出OOM异常终止程序,也不会去回收强引用实例来释放内存。

    如果希望强引用实例在适当的时机被回收需要进行一定的弱化处理。

    o = null;
    

    一旦引用为null或者超出了对象的生命周期,则GC认为该对象的引用不存在,可以回收。

    /**
    * ArrayList.clear()源码,弱化强引用,使其元素被回收。
    * 因为elementData是一个实例变量。
    * 这里不是采用对elementData引用赋值null,是为了后续使用add()可继续添加元素。
    **/
    
    public void clear() {  
            modCount++;  
            // Let gc do its work  
            for (int i = 0; i < size; i++)  
                elementData[i] = null;  
            size = 0;  
    } 
    

    在方法中,如果有一个局部强引用变量去引用堆内存中的对象,当方法执行完退出Java虚拟机栈后,引用也就不存在,该对象就有可能被回收。

    类与对象的生命周期

    Java类的完整生命周期是指一个字节码文件从加载到卸载的全过程:加载->连接->初始化->使用->卸载。

    类的加载统称是类的加载,连接和初始化三个过程。

    1. 加载阶段,找到需要加载的类并把类的信息加载到jvm的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口;
    2. 连接阶段,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证、准备和解析;
    3. 初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息。主动引用触发初始化,主动引用有:
      • 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
      • 通过反射方式执行以上三种行为。
      • 初始化子类的时候,会触发父类的初始化。
      • 作为程序入口直接运行时(也就是直接调用main方法)。
    4. 使用阶段,类的使用包括主动引用和被动引用,主动引用会引起类的初始化,而被动引用不会引起类的初始化。被动引用有:
      • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
      • 定义类数组,不会引起类的初始化。
      • 引用类的常量,不会引起类的初始化。
    5. 卸载阶段,在类使用完毕后,满足以下所有条件,jvm就会在方法区垃圾回收的时候对类进行卸载(类的卸载过程其实就是在方法区中清空类信息),至此java类的整个生命周期就结束了。
      • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
      • 加载该类的ClassLoader已经被回收。
      • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

    Java对象的生命周期是类生命周期一部分-使用阶段的主动引用(实例化对象)。

    对象基本上在jvm的堆区中创建。在创建对象之前,会触发类加载。

    当类初始化完成后,根据类信息在堆区中实例化类对象:初始化非静态变量、非静态代码以及默认构造方法。

    当对象使用完之后会在合适的时候被jvm垃圾收集器回收。

    参考自详解java类的生命周期

    软引用
        String str = new String("abc");
        SoftReference<String> sr = new SoftReference<String>(str);
        str = null;
    

    如果一个对象只具有软引用时,则当内存不够时,垃圾回收器就会回收它。

    一般软引用用于实现内存敏感的高速缓存。

    由于被回收的软引用无法再给程序使用,所以使用前需要进行判断

        if(sr.get() != null) {
            str = sr.get();
        }else {
            str = new String("abc");
        }
    
    弱引用
        String str=new String("abc");      
        WeakReference<String> abcWeakRef = new WeakReference<String>(str);  
        str=null;  
    

    只具有弱引用的对象拥有更短的生命周期,无论当前内存空间是否足够,一旦被垃圾回收线程发现,就会回收它。但是因为垃圾回收线程优先级比较低,所以不一定很快就能发现它。

    如果一个对象引用频率不高且要求引用时快速,但不想控制其生命周期,使其不被回收,可以使用弱引用。

    虚引用

    虚引用有以下几个特点:

    1. 与其他几种引用不同的是,虚引用不会决定对象的生命周期;
    2. 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收;
    3. 虚引用必须和引用队列一起使用。

    虚引用的主要作用是用来跟踪对象垃圾回收的活动。当Java回收一个对象的时候,如果发现他有虚引用,会在回收对象之前将他的虚引用加入到与之关联的引用队列中。可以通过这个特性在一个对象被回收之前采取措施。

    参考Java 7之基础 - 强引用、弱引用、软引用、虚引用

    如何GC

    垃圾回收算法决定了回收方法,而Java内存分为三类:新生代,旧生代和持久代,每一类的回收算法不同。

    内存分类

    1. 新生代,特点生命周期短,频繁的创建和销毁对象。新建的对象都存储在新生代。
    2. 旧生代,特点生命周期长。用于存放新生代中经过多次回收仍然存活的对象,如缓存对象。
    3. 持久代,在Sun的HotSpot中指方法区,可能有些JVM没有这块内存。主要存放常量和类的信息。

    常见的GC算法

    • 标记回收算法(Mark and Sweep GC):从”GC Roots”集合开始,将内存整个遍历一次,保留所有可以被GC Roots直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,这个算法需要中断进程内其它组件的执行并且可能产生内存碎片。
    • 复制算法(Copying):将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
    • 标记-压缩算法(Mark-Compact) :先需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

    根据算法的特点,新生代一般使用复制算法,由于其频繁的回收对象,需要高效率的复制算法;而旧生代则采用标记-压缩算法。

    引用自Android GC 那点事

    常见的问题

    关于内存的问题主要有两类:内存溢出(OOM)和内存泄漏。

    内存溢出

    在编写代码时,声明变量并且要求虚拟机分配内存时,超出了系统限定的容量,不能满足,于是产生内存溢出。

    内存泄漏

    对象可以达到GC Roots,但是程序无法直接使用它,相当于该对象无作用,但是又不符合回收条件无法回收内存空间,导致内存泄漏。

    一旦这类无法分配的内存积累,就会导致内存溢出,程序crash。

    性能优化

    System.gc()

    调用System.gc()表明Java虚拟机为了快速重用不用的对象所占用的内存付出了努力。

    当控制权返回给调用者时,说明Java虚拟机已经做出了最大的努力去回收那些被丢弃的对象所占用的内存。

    System.gc()只是建议JVM执行GC,但是GC的执行与否完全是JVM决定的。

    调用System.gc(),等价于Runtime.getRuntime().gc()

    Object.finalize()

    单纯的Java创建的实例都是在堆中分配内存的。如果使用JNI技术,可能会在栈上分配内存。此时System.gc()无法对该区域无用内存进行回收。

    例如C语言的malloc()分配内存,就需要使用Object.finalize()回收。由于使用了C函数malloc()分配内存,需要使用free()进行释放内存,而子类的需要去覆写Object的finalize(),实现调用free()来进行栈内存回收。

    一旦垃圾回收器确认对象没有引用,就会去调用该对象的finalize()。之后GC就会真正丢弃该对象释放内存。

    但是在调用Object.finalize()之前,对象又存在引用,可以复活(即不被回收)。所以一个对象不存在引用不代表一定会被GC回收。

    注意如果finalize()没有被覆写去执行其他清理或处理系统资源,无引用的对象也会被立刻回收。

    System.runFinalization()

    建议Java虚拟机去调用那些处于Finalizable阶段(失去引用)对象的finalize(),前提是该方法没有被调用过。当控制权返回给调用者时,说明Java虚拟机已经做出了最大的努力去完成调用finalize()

    何时调用
    1. 所有对象被Garbage Collection时自动调用,比如运行System.gc()的时候。
    2. 程序退出时为每个对象调用一次finalize方法。
    3. 显式的调用finalize方法。

    参考finalize() 和 system.gc() 的区别

    编程习惯

    1. 避免在循环体内创建实例,GC线程优先级较低,不及时回收,很容易造成OOM,即使创建的实例占用内存空间不大。
    2. 尽量及时使对象符合垃圾回收标准,弱化强引用(例如ArrayList.clear()),使用合理的Reference存储对象。
    3. 创建本地变量优于实例变量,因为线程私有的Java虚拟机栈会随着方法执行完毕后释放内存。
    4. 等等还有许多。

    参考

    Java之美[从菜鸟到高手演变]之JVM内存管理及垃圾回收

    Android内存泄漏小谈

    引用队列ReferenceQueue

    引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中。分别与软,弱,虚引用搭配使用。

    • 软引用,当SoftReference引用的对象被回收后,JVM会把这个软引用加入到与之关联的引用队列中,一般实现内存敏感的高速缓存;
    • 弱引用,当WeakReference引用的对象被回收后,JVM会把这个弱引用加入到与之关联的引用队列中;
    • 虚引用,必须与引用队列一起使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。一般用于跟踪对象被垃圾回收器回收的活动,这样就可以在被回收之前拯救该对象。

    使用软引用构建内存敏感的高速缓存

    原因

    实际应用中,总会有“回看”的操作,例如浏览器的回退等。那么这些信息如何处理这个需求以提高程序性能呢?一般有两种:

    1. 内存缓存,把信息对象保存在内存中,对象的生命周期贯穿整个应用;
    2. 重构信息,当用户查看其它信息时,结束当前信息对象引用,让其被GC。一旦用户回看时,从其它介质(磁盘文件,网络资源,数据库等等)获取信息,重构实例对象。

    这两种方法都有各自的缺陷:

    方法一,由于大量对象存在,导致大量内存浪费,容易造成OOM;方法二,由于垃圾回收线程的优先级比较低,所以对象可能无法及时回收,但是程序又无法使用它,只能通过重建对象来获取信息。而访问磁盘文件,网络资源,数据库等都是影响程序性能的重要因素。

    总结,使用方案二,使用完毕后结束引用,在内存紧张时可以回收这些信息对象。如果需要重新使用信息时,先去尝试获取未被回收的对象减少不必要的访问,然后在考虑是否重建。

    能够像这样去控制对象生命周期的引用-软引用。

    对象的可及性

    引用队列是用于检测对象的可及性变化,那对象的可及性是如何判定的?

    树形引用链.jpg
    1. 单条引用路径可及性判断:在这条路径中,最弱的一个引用决定对象的可及性。
    2. 多条引用路径可及性判断:几条路径中,最强的一条的引用决定对象的可及性。

    例如考虑对象5的可及性,由于路径1-5中,引用1是强引用,引用5是软引用,所以此路径对象5的可及性是软引用;而路径3-7中,引用3是强引用,引用7是弱引用,则此路径对象5的可及性是弱引用。综合两条路径,对象5的可及性就是软引用。

    所以需要使一个对象只具备软引用必须在实例化后弱化强引用。

    Object o = new Object();
    SoftReference sr = new SoftReference(o);//此时o的可及性的强可及
    o = null;//此时o的可及性的软可及
    o = sr.get();//在回收之前重新获取,此时o的可及性是强引用
    

    使用引用队列清除失去引用对象的SoftReference

    一旦JVM把SoftReference所引用的对象回收后,这个SoftReference就没有存在的价值。

    因此需要采用一种清除机制,去清除无用的SoftReference,避免大量的SoftReference导致内存泄漏。

    此时就要用到ReferenceQueue。

    Object o = new Object();
    ReferenceQueue queue = new ReferenceQueue();
    SoftReference sr = new SoftReference(o, queue);
    o = null;
    
    //此时JVM内存不足
    System.gc();
    SoftReference sfr = null;
    if((sfr = queue.poll()) != null) {
        //clear softreference reference
        sr = null;
    }
    

    代码分析:

    1. 创建软引用时,将其绑定一个ReferenceQueue;
    2. 当内存不足时,JVM回收软引用所引用的对象,并且将这个软引用添加到ReferenceQueue;
    3. 调用队列的poll(),将队列中头部的软引用返回,如果队列为空,返回null;
    4. 此时终结强可及软引用的生命周期。

    总结来说ReferenceQueue对于非强引用的作用就是检测其可及性变化,以便针对这种变化提供处理。

    实例

    SoftReference回收的规则:JVM会在内存不足时,回收SoftReference。但是会优先回收长时间闲置不用的SoftReference,而保留刚创建或使用过的SoftReference。

    利用这一特性,配合重获实例方法,来实现内存敏感的高速缓存。

    首先建立一个Cache单利类,内部有集合存储SoftReference,ReferenceQueue。

    Cache类中有一个缓存对象的公共方法,首先清除队列和集合中无用的SoftReference,然后生成软引用加入集合

    public void cacheModel(Object o) {
        cleanCache();
        SoftReference sr = new SoftReference(o, queue);
        collection.add(o);
    }
    

    清除方法

    private void cleanCache() {
        SoftReference sr = null;
        while((sr = queue.poll()) != null) {
            collection.remove(sr);
        }
    }
    

    可以看到清除方法只要是利用ReferenceQueue.poll(),循环地去消除无用的SoftReference引用,让其变为可回收。同时不要忘记对集合中同一个SoftReference做清除引用的操作,否则无法被回收。

    注意poll()不只是简单的将SoftReference引用返回,其内部实现真正做到了弱化对SoftReference的强引用

        @SuppressWarnings("unchecked")
        private Reference<? extends T> reallyPoll() {       /* Must hold lock */
            Reference<? extends T> r = head;
            if (r != null) {
                head = (r.next == r) ?
                    null :
                    r.next; // Unchecked due to the next field having a raw type in Reference
                r.queue = NULL;
                r.next = r;
                queueLength--;
                if (r instanceof FinalReference) {
                    sun.misc.VM.addFinalRefCount(-1);
                }
                return r;
            }
            return null;
        }
    

    可以看到ReferenceQueue的队列形成是通过其成员变量head和Reference.next实现的,所以当poll时将head赋值为原先head的next或者为null(没有其他元素时)。这样一来队列不再对SoftReference持有引用。

    同理集合移除元素也是做了同样的处理。

    注意这里不需要对cleanCache()中的sr进行弱化,因为他是局部变量,随着方法的结束,自行释放内存。

    Cache类还需有一个获取缓存对象的方法

    public Object getObject(int id) {
        SoftReference sr = null;
        Object o = null;
        if((sr = collection.get(id)) != null)
            o = sr.get();
        if(o == null) {
            o = new Object();
            cacheModel();
        }
        return o;
    }
    

    代码中首先根据Cache类中的集合中的软引用去获取缓存的对象,然后去判断获取到的实例是否存在。不存在的可能性有两种:缓存的对象被JVM回收了或者从一开始就没有缓存。一旦判断为null,那么对其进行重构,并且返回。

    最后为了完善Cache类的用途,可以为其添加清空的公共方法:

    public void clean() {
        cleanCache();
        collection.clean();
    }
    

    参考

    使用WeakReference 与 ReferenceQueue 简单实现弱引用缓存

    Java:对象的强、软、弱和虚引用

    遗留问题

    问题 参考
    WeakReference实现非敏感数据缓存 Java:对象的强、软、弱和虚引用
    WeakHashMap的作用,与弱引用实现非敏感数据缓存有关吗
    弱引用实现的非敏感数据缓存与软引用实现的敏感数据缓存有什么区别
    String字面量存储在方法区的运行时常量区(String Pool),那么如何回收?

    由于String.intern()方法是native,所以字面量对象都是通过native方法在方法区运行时常量区分配内存。

    相关文章

      网友评论

        本文标题:Android中缓存理解(一)

        本文链接:https://www.haomeiwen.com/subject/egqezttx.html