首页 > 代码库 > 内存优化
内存优化
内存优化
一、垃圾回收概况
二、Java内存区域
1. 程序计数器(Program Counter Register):
程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。
每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。
如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写 完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区 域中唯一一个没有定义OutOfMemoryError的区域。
局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有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内存分配机制
五、内存检测工具
◆ 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,造成内存抖动。
常见的内存泄漏问题:
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拼接操作的时候,可以减少内存;
内存优化