您当前的位置: 首页 >  性能优化
  • 0浏览

    0关注

    674博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

性能优化专题二--内存优化(虚引用和弱引用的区别、枚举优化、对象池)

沙漠一只雕得儿得儿 发布时间:2020-05-10 17:25:15 ,浏览量:0

我们为什么要优化内存

在 Android 中我们写的 .java 文件,最终会编译成 .class 文件, class 又由类装载器加载后,在 JVM 中会形成一份描述 class 结构的元信息对象,通过该元信息对象可以知道 class 的结构信息 (构造函数、属性、方法)等。JVM 会把描述类的数据从 class 文件加载到内存,Java 有一个很好的管理内存的机制,垃圾回收机制 GC 。为什么 Java 都给我们提供了垃圾回收机制,程序有时还会导致内存泄漏,内存溢出 OOM,甚至导致程序 Crash 。接下来我们就对实际开发中出现的这些内存问题,来进行优化。

线程独占区

程序计数器

  • 相当于一个执行代码的指示器,用来确认下一行执行的地址
  • 每个线程都有一个
  • 没有 OOM 的区

虚拟机栈

  • 我们平时说的栈就是这块区域
  • java 虚拟机规范中定义了 OutOfMemeory , stackoverflow 异常

本地方法栈

  • java 虚拟机规范中定义了 OutOfMemory ,stackoverflow 异常

注意

  • 在 hotspotVM 中把虚拟机栈和本地方法栈合为了一个栈区
线程共享区

方法区

  • ClassLoader 加载类信息
  • 常量、静态变量
  • 编译后的代码
  • 会出现 OOM
  • 运行时常量池
    • public static final
    • 符号引用类、接口全名、方法名

java 堆 (本次需要优化的地方)

  • 虚拟机能管理的最大的一块内存 GC 主战场
  • 会出现 OOM
  • 对象实例
  • 数据的内容
JAVA GC 如何确定内存回收

随着程序的运行,内存中的实例对象、变量等占据的内存越来越多,如果不及时进行回收,会降低程序运行效率,甚至引发系统异常。

目前虚拟机基本都是采用可达性分析算法

可达性分析算法

 

GC Roots 的对象作为起始点,向下搜索走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链,即从GC roots 到这个对象不可达,则证明对象不可用,可被回收。

可以作为 GC Roots 的对象

  • 虚拟机栈正在运行使用的引用
  • 静态属性 常量
  • JNI 引用的对象

GC 是需要 2 次扫描才回收对象,所以我们可以使用 finalize 去救活丢失的引用

 @Override
    protected void finalize() throws Throwable {
        super.finalize();
        instace = this;
    }
不同引用类型的回收状态 问题一:软引用、虚引用和弱引用的区别、
  • 软引用 (SoftReference)

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存,只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收, java 虚拟机就会把这个软引用加入到与之关联的引用队列中。

 

注意: 软引用对象是在 jvm 内存不够的时候才会被回收,我们调用 System.gc() 方法只是起通知作用, JVM 什么时候扫描回收对象是 JVM 自己的状态决定的。就算扫描到了 str 这个对象也不会回收,只有内存不足才会回收。

  • 弱引用 (WeakReference)

弱引用与软引用的区别在于: 只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

 

可见 weakReference 对象的生命周期基本由 GC 决定,一旦 GC 线程发现了弱引用就标记下来,第二次扫描到就直接回收了。

注意这里的 referenceQueuee 是装的被回收的对象。

  • 虚引用 (PhantomReference)
    @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任何时候都可能被回收,相当于没有引用一样否

虚引用:该对象在回收前和回收后都无法通过虚引用获取到这个,在该对象被回收掉时会将对象部分信息存储到虚引用回收队列中,仅仅是拿到一个对象被回收掉的通知;

弱引用:在该对象被回收前可以通过弱引用获取到该对象,回收后无法获取到这个对象,在该对象被回收掉时会将对象部分信息存储到弱引用回收队列中,可以拿到一个对象被回收掉的通知;

下面通过程序我们来看下实验结果:

  • 虚引用:不对生存造成任何影响,用于跟踪GC的回收通知

一般没有什么用处,使用场景是要做一些系统跟踪工具,例如打log要看看GC什么时候来过,打印这些GC信息时

    @Test
    public void testPhantomReference() throws InterruptedException {
        //虚引用:功能,不会影响到对象的生命周期的,
        // 但是能让程序员知道该对象什么时候被 回收了
        ReferenceQueue referenceQueue = new ReferenceQueue();
        Object phantomObject = new Object();
        PhantomReference phantomReference = new PhantomReference(phantomObject, referenceQueue);
        phantomObject = null;
        System.out.println("GC回收前");
        System.out.println("phantomReference:" + phantomReference.get());//虚引用无法获取到对象,返回null
        System.out.println("referenceQueue:" + referenceQueue.poll());//引用回收队列中还未回收掉,返回null
        System.gc();
        Thread.sleep(2000);
        System.out.println("GC回收后");
        System.out.println("phantomReference:" + phantomReference.get());//虚引用无法获取到对象,返回null
        //referenceQueue:最后能够拿到一个被回收掉的对象的信息,而不是原来的对象,原来的对象已经被回收掉了
        System.out.println("referenceQueue:" + referenceQueue.poll());
    }

将一个对象放到虚引用中,在System.gc()前,通过虚引用无法获取到这个对象,且引用回收队列中没有任何信息;在GC后,引用回收队列中可以拿到回收对象的信息,但注意这个不是原对象,仅是原对象的一些信息:

  • 弱引用:

第一次扫到了,就标记下来,第二次扫到直接回收

    @Test
    public void testWeakReference() throws InterruptedException {
        ReferenceQueue referenceQueue = new ReferenceQueue();
        Object weakObject = new Object();
        //弱引用,可以将这个引用记录到引用队列 referenceQueue 中去
        WeakReference weakReference = new WeakReference(weakObject, referenceQueue);
        System.out.println("GC回收前");
        System.out.println("WeakReference:" + weakReference.get());//弱引用中可以拿到该对象
        System.out.println("referenceQueue:" + referenceQueue.poll());//引用回收队列中没有对象的任何信息

        weakObject = null;
        System.gc();
        Thread.sleep(2000);
        System.out.println("GC回收后");
        //对象被回收掉返回null
        System.out.println("WeakReference:" + weakReference.get());
        //被回收掉对象的信息,在回收队列中referenceQueue 可以查询到
        System.out.println("referenceQueue:" + referenceQueue.poll());
    }

GC回收前,通过弱引用我们可以拿到放在其中的对象;GC回收后,无法拿到该对象,且在回收队列中能够获取到被回收对象的一些信息:

 

问题二:枚举的性能损耗以及注解的解决方案

日常我们使用枚举来定义一些常量的取值,使用枚举能够确保参数的安全性。但是Android开发文档上指出,使用枚举会比使用静态变量多消耗两倍的内存,应该尽量避免在Android中使用枚举,那么枚举为什么会更消耗内存呢?下面一起分析一下。

public enum  Sex {
    MAN, WOMAN;
}

1、使用javac将其编译成.class文件,命令为:javac Sex.java;

2、用jad反编译.class文件,生成Sex.jad,命令为jad Sex.class,jad的下载地址:http://www.javadecompilers.com/jad

从反编译的代码来看,我们定义的枚举,编译器会将其转换成一个类,这个类继承自java.lang.Enum类,除此之外,编译器还会帮我们生成多个枚举类的实例,赋值给我们定义的枚举类型常量,并且还声明了一个枚举对象的数组,保存了所有的枚举对象。下面我们分别来计算一下采用静态变量和枚举占用内存的大小对比。

a、静态变量占用内存大小为:int占用内存大小为4,加起来占用内存大小为8

 public final static int MAN = 0;
 public final static int WOMAN = 1;

b、枚举占用内存大小为:216字节。打开Sex.jad文件:

public final class Sex extends Enum {
    public static Sex[] values()
    {
        return (Sex[])$VALUES.clone();
    }
 
    public static Sex valueOf(String s)
    {
        return (Sex)Enum.valueOf(com/liunian/androidbasic/enumtest/Sex, s);
    }
 
    private Sex(String s, int i)
    {
        super(s, i);
    }
 
    public static final Sex MAN;
    public static final Sex WOMAN;
    private static final Sex $VALUES[];
 
    static 
    {
        MAN = new Sex("MAN", 0);
        WOMAN = new Sex("WOMAN", 1);
        $VALUES = (new Sex[] {
            MAN, WOMAN
        });
    }
}

可以看到,枚举占用内存的大小比静态变量多得多。

枚举类型数据的内存优化:使用注解的方案

例如我们有个SHAPE类,里面有四种形状类型,在此我们定义一个注解:

public class SHAPE {
    public static final int RECTANGLE = 0;
    public static final int TRIANGLE = 1;
    public static final int SQUARE = 2;
    public static final int CIRCLE = 3;

    // 其中flag为true则使用时可以用|来连接,可以允许取多个值;
    // 后面的value是注解使用时的可选值
    @IntDef(flag = true, value = {RECTANGLE, TRIANGLE, SQUARE, CIRCLE})
    @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Model {

    }

    private @Model
    int value = RECTANGLE;

    public void setShape(@Model int value) {
        this.value = value;
    }

    @Model
    public int getShape() {
        return this.value;
    }
}

在需要使用注解的地方使用:

SHAPE shape = new SHAPE();
shape.setShape(SHAPE.CIRCLE | SHAPE.RECTANGLE);
问题三:使用对象池避免GC回收将来要重用的对象

内存设计模式对象沲+LRU算法:

设计的对象池基类:

import android.util.SparseArray;

public abstract class ObjectPool {
    //空闲沲,用户从这个里面拿对象
    private SparseArray freePool;
    //正在使用沲,用户正在使用的对象放在这个沲记录
    private SparseArray lentPool;

    //沲的最大值
    private int maxCapacity;

    public ObjectPool(int initialCapacity, int maxCapacity) {
        //初始化对象沲
        initalize(initialCapacity);
        this.maxCapacity = maxCapacity;
    }

    private void initalize(int initialCapacity) {
        lentPool = new SparseArray();
        freePool = new SparseArray();
        for (int i = 0; i < initialCapacity; i++) {
            freePool.put(i, create());
        }
    }

    /**
     * 申请对象
     *
     * @return
     */
    public T acquire() throws Exception {

        T t = null;
        synchronized (freePool) {
            int freeSize = freePool.size();
            for (int i = 0; i < freeSize; i++) {
                int key = freePool.keyAt(i);
                t = freePool.get(key);
                if (t != null) {
                    this.lentPool.put(key, t);
                    this.freePool.remove(key);
                    return t;
                }
            }
            //如果没对象可取了
            if (t == null && lentPool.size() + freeSize < maxCapacity) {
                //这里可以自己处理,超过大小
                if (lentPool.size() + freeSize == maxCapacity) {
                    throw new Exception();
                }
                t = create();
                lentPool.put(lentPool.size() + freeSize, t);


            }
        }
        return t;
    }

    /**
     * 回收对象
     *
     * @return
     */
    public void release(T t) {
        if (t == null) {
            return;
        }
        int key = lentPool.indexOfValue(t);
        //释放前可以把这个对象交给用户处理
        restore(t);

        this.freePool.put(key, t);
        this.lentPool.remove(key);

    }

    protected void restore(T t) {

    }

    protected abstract T create();

    public ObjectPool(int maxCapacity) {
        this(maxCapacity / 2, maxCapacity);
    }

}

然后我们自己MyObjectPool去继承基类即可使用:

public class MyObjectPool extends ObjectPool{
    public MyObjectPool(int initialCapacity, int maxCapacity) {
        super(initialCapacity, maxCapacity);
    }

    public MyObjectPool(int maxCapacity) {
        super(maxCapacity);
    }

    @Override
    protected Object create() {//LRU
        return new Object();
    }
}

使用:

MyObjectPool pool = new MyObjectPool(2, 4);
Object o1 = pool.acquire();
Object o2 = pool.acquire();
Object o3 = pool.acquire();
Object o4 = pool.acquire();
Object o5 = pool.acquire();

 

关注
打赏
1657159701
查看更多评论
立即登录/注册

微信扫码登录

0.0449s