首页 > 代码库 > 内存优化

内存优化

内存优化

一、垃圾回收概况

        垃圾回收(Garbage Collection)机制,是Java与C/C++的主要区别之一。相比于C/C++这种需要通过手动编码来申请以及释放内存有所不同,Java拥有自动内存管理和回收机制,即GC。Android系统会自动跟踪所有的对象的申请、引用、被引用、赋值等生命周期的状态,由垃圾回收器负责回收程序中已经不使用,但是仍然被各种对象占用的内存。将程序员从繁重、危险的内存管理工作中解放出来。
        学习GC机制,可以帮助我们在日常工作中排查各种内存溢出或泄露问题,解决性能瓶颈,达到更高的并发量,写出更高效的程序。我们先从下面几个方面来了解GC机制:

二、Java内存区域

        了解GC机制,首先我们要清在JVM中内存区域的划分。在Java运行时的数据区里,由JVM管理的内存区域分为下图几个模块:
技术分享

其中:

1. 程序计数器(Program Counter Register)

       程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。

  每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。

  如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写 完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区 域中唯一一个没有定义OutOfMemoryError的区域。

2. 虚拟机栈(JVM Stack)
         一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。  

       局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占 用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定 好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。

  虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多 数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出 OutOfMemoryError(内存溢出)。

  每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。

3. 本地方法栈(Native Method Statck)

       本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。

  本地方法栈也是线程私有的。

4. 堆区(Heap)

       堆区是理解Java GC机制最重要的区域,没有之一。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存(不过现代技术里,也不是这么绝对的,也有栈上直接分配的)。

  一般的,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主 流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。

5. 方法区(Method Area)

       在Java虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但事实 上,方法区并不是堆(Non-Heap);另外,不少人的博客中,将Java GC的分代收集机制分为3个代:青年代,老年代,永久代,这些作者将方法区定义为“永久代”,这是因为,对于之前的HotSpot Java虚拟机的实现方式中,将分代收集的思想扩展到了方法区,并将方法区设计成了永久代。不过,除HotSpot之外的多数虚拟机,并不将方法区当做永 久代,HotSpot本身,也计划取消永久代。本文中,由于笔者主要使用Oracle JDK6.0,因此仍将使用永久代一词。

  方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。

  方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。一般的,方法区上 执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(HotSpot),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对 常量池的内存回收和对已加载类的卸载。

  在方法区上进行垃圾收集,条件苛刻而且相当困难,效果也不令人满意,所以一般不做太多考虑,可以留作以后进一步深入研究时使用。

  在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。

  运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译;运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。

6. 直接内存(Direct Memory)

       直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是 JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区 (Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。 由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。

(以上摘抄自博客http://www.cnblogs.com/hnrainll/archive/2013/11/06/3410042.html,为了防止以后遇到相关问题时找不到,直接就复制利于以后的”温故“。)

三、Java对象的访问方式

一般来说,一个Java的引用访问涉及到3个内存区域:JVM栈,堆,方法区。

  以最简单的本地变量引用:Object obj = new Object()为例:

  • Object obj表示一个本地引用,存储在JVM栈的本地变量表中,表示一个reference类型数据;
  • new Object()作为实例对象数据存储在堆中;
  • 堆中还记录了Object类的类型信息(接口、方法、field、对象类型等)的地址,这些地址所执行的数据存储在方法区中;

四、Java内存分配机制

 Young Generation
        ●  大多数新建的对象都位于Eden(伊甸园,很贴切的名字)区;
        ●  当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到survivor0;
        ●  当survivor0也满的时候,将其中仍然活着的对象直接复制到survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加到survivor1(此时,survivor0是空白的,两个存活区总有一个是空白的);
        ●  当两个存活区切换了几次(HotSpot虚拟机默认15次)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。
        从上面的过程可以看出,Eden区是一个连续的空间,且Survivor总有一个是空的。经过一次GC和复制,一个Survivor中保存着当前还活着的对象,而另一个Survivor区和Eden区的内容就不需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。这就是著名的“停止·复制(Stop-and-cope)”清理法(很不错的一个编程思路),在这种情况下分配内存和清除内存的效率都极高,但不适合在老年代采用这种方法。
 Old Generation
        ●  存放长期存活的对象和经历过多次Mintor GC之后依然存活下来的对象;
        ●  老年代的空间比年轻代大,能存放更多的对象,发生的GC次数也比年轻代少;
        ●  满了进行Major GC,也叫Full GC;
 Permantent Generation
        ●  就是方法区,方法区中有要加载的类信息、静态变量、final类型的常量、属性和方法信息
技术分享

五、内存检测工具

◆  Memory Monitor:

        ●  跟踪整个app的内存变化情况;

        ●  方便显示内存使用和GC情况;

        ●  快速定位卡顿是否和GC有关;

        ●  快速定位Crash是否和内存占用过高有关;

        ●  快速定位潜在的内存泄漏问题(如果内存泄漏,内存会呈现一直增长的状态);

        ●  简单易用,但不能准确定位问题;



◆  Allocation Tracker:

        ●  追踪内存对象的来源;

        ●  定位代码中分配的对象的类型、大小、时间、线程、堆栈等信息;

        ●  定位内存抖动问题;

        ●  配合Heap Viewer一起定位内存泄漏问题(在Heap Viewer中发现当前系统中存在某个实例的泄漏问题,然后在Allocation Tracker中找到这些对象是在哪些方法中创建出来的,从而找到问题的根源);


◆  Heap Viewer:

         ●  查看当前内存快照,便于对比分析那个对象有可能发生了泄漏;

         ●  每次GC之后收集一次信息;

         ●  查找内存泄漏利器(内存泄漏时,可以在Heap View中看到GC之后,泄漏的实例是不会销毁泄漏的);


◆  LeakCanary

         ●  查找内存泄漏的神器

         ●  在gradle的依赖中添加leakCanaty,然后在Activity中添加代码LeakCanary.install(this);即可使用;

         ●  当发现有内存泄漏问题时,会在通知栏提醒,我们可以点击通知查看详情;


六、内存泄漏

        内存泄漏表示的是不再用到的对象因为被错误引用而无法进行回收,导致一直占用内存,直到程序结束。发生内存泄漏会导致Memory Generation中的剩余可用Heap Size越来越小,这样会导致频繁触发GC,造成内存抖动。

        内存抖动:内存抖动是因为在短时间内大量的对象被创建又马上被释放。瞬间产生大量的对象对严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,会触发GC从而导致刚产生的对象又很快被回收。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。而在GC时,系统会暂停应用程序的执行,而独占CPU,所以这个操作有可能会影响到帧率,并使得用户感知到性能问题。如下图。
       技术分享
         帧率(FPS):Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,那么整个过程如果保证在16ms以内就能达到一个流畅的画面(60FPS)。如果某一帧的操作超过了16ms就会让用户感觉到卡顿。

常见的内存泄漏问题:

1.  单例造成的泄漏

public class  AppManager{
        private static AppManager instance;
        private Context context;
        private AppManager(Context context){   //这里传递了一个Activity对象
            this.context = context;
        }
        public static AppManager getInstance(Context context){
            if (instance != null){
                instance = new AppManager(context);
            }
            return instance;
        }
    }

        例子中,静态变量AppManager持有一个Activity对象的引用,造成Activity对象即使被销毁也不能被垃圾回收,因为AppManager是一直存在的。


2.  非静态内部类的静态实例造成的泄漏

public class MainActivity extends Activity{
        private static TestResource sResource = null;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            if (sResource == null){
                sResource = new TestResource();
            }
            //...
        }
        class TestResource{
            //...
        }
    }

        例子中,内部类TestResource的实例是间接持有MainActivity的引用的,所以这里创建的静态实例会一直持有MainActivity的引用,造成MainActivity不能被回收。


3.  Handler造成的内存泄漏

public class MainActivity extends Activity{
        private Handler mHandler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
            }
        };
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            loadData();
        }
        private void loadData() {
            //...request
            Message message = Message.obtain();
            mHandler.sendMessage(message);
        }
    }

        例子中,通过创建匿名对象的方法来创建一个Handler对象,在创建匿名对象时就会默认持有外部类的实例的引用,这里就是MAinActivity。MainActivity只有等到Handler结束之后,等到Handler被销毁之后才能被销毁。这里要使用静态内部类,这样handler就不会跟随类而不是跟随类的对象加载,也就不再持有外部类的对象引用了。

//handler类要写成静态的内部类,防止泄漏
    public static class DownloadHandler extends Handler{       

        public final WeakReference<MainActivity> mActivity;    //弱引用

        public DownloadHandler(MainActivity activity) {
            mActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity mainActivity = mActivity.get();

            switch (msg.what){
                case 0:
                    int progress = (int) msg.obj;
                    mainActivity.getTextView().setText(progress+"%");
                    if (progress == 100){
                        Toast.makeText(mainActivity,"download success",Toast.LENGTH_SHORT).show();
                    }
                    break;
            }

        }
    }


七、内存优化

1.  避免内存泄漏的方法:

?  尽量不要让静态变量引用Activity,一定要用的话就使用WrakReference方式;

?  使用静态内部类来代替内部类(因为静态内部类一般不会持有外部类的引用的);

?  静态内部类中使用弱引用来引用外部类;

?  在声明周期结束的时候释放资源;


2. 减少内存使用

?  使用更轻量的数据结构(比如用SpareArray代替HashMap);

?  避免在onDraw方法中创建对象(因为onDraw方法使用频率比较高);

?  对象池(例如Message.obtain());

?  LRUCache;

?  Bitmap内存复用,压缩(inSampleSize,inBitmap);

?  StringBuilder来代替String,尤其在String拼接操作的时候,可以减少内存;

内存优化