Java内存管理

来源:互联网 发布:统计台账数据 编辑:程序博客网 时间:2024/06/08 02:39

1 前言

  最近在整理android内存管理的内容,写在这里和大家分享。因为个人能力有限,文中可能存在错漏之处,如有发现请留言,QQQ~~


2 java的内存区域

在java中,内存分为这么几块区域:

2.1 Heap堆内存

  线程间共享,存储运行时new出来的对象,分为年轻代和年老代。
  年轻代包含1块Eden区和2块Survivor区(分别叫from和to),大小默认8:1:1。新生对象分配到Eden区,由于大多数对象生命周期都很短,所以Eden区最大,以避免频繁的发生GC。Eden区满之后,在Eden和Survivor(from)发生MinorGC,仍存活的对象会被移到Survivor(to)区(from和to的名字是有对象的Survivor叫from,没对象的Survivor叫to),经过一定次数(可设置)的MinorGC仍存活的对象会被移到年老代中。

2.2 VM栈内存

  线程私有,栈中的单位是栈帧(StackFrame),每个方法执行时会在栈中新建一个栈帧,方法结束后栈帧中的内存被回收,每个栈帧包含几部分内容:

2.2.1 局部变量表

  包含基本数据类型或者Heap中的对象实例的引用reference,在编译期既已确定。
  另外关于reference说几句。reference是访问具体对象的,但是访问方式并不固定,目前有主流的实现方式有两种。
  (1) reference持有直接指针
  reference直接持有对象的地址,而对象中持有方法区中类型数据的指针。比起下面的句柄方式优势显而易见,就是访问速度快。
  (2) reference持有句柄
  Heap中有一块区域是句柄池,存放着一堆叫作句柄的东西。句柄中存储着对象实例的地址和对象类型数据的地址(方法区中),而栈中的reference持有该句柄的地址,通过句柄访问对象。句柄的优势在于十分稳定。为什么这么说呢?因为Heap中GC频繁,对象的地址可能会经常发生变化(垃圾回收时可能会在GC之后将仍然存活的对象移动到堆中的一侧,以减少碎片,这时对象的地址会发生变化),如果是持有直接指针的方式,此时就需要改变reference的值,但是如果是持有句柄,则不需要改变reference的地址。
  但是说到这里我却有个疑问,频繁改变reference的值和频繁改变句柄的值有什么不同吗?

2.2.2 操作数栈

  用于储存局部变量的中间计算结果,并继续与下一个局部变量根据运算符进行运算

2.2.3 方法出口

  当前方法运算结束后应该返回哪里继续执行?方法出口即是继续执行节点的字节码行号(应该是该方法的调用者的地址)

2.2.4 动态链表

  网上都是下面这么写的,然而并没能看懂。。。如果有哪位大哥看懂了,麻烦戳下留言区。
  每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
  Class文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

2.2.5 其他附加信息

2.3 Native栈内存

  线程私有,结构跟VM栈一样,但是保存的是Native方法的栈帧

2.4 程序计数器 Program Counter Register

  线程私有,用于记录该线程当前在执行的字节码指令的行号,字节码解释器通过改变程序计数器的行号来选择下一条字节码指令,从而形成分支,循环,跳转等

2.5 方法区

  线程间共享,有时相对于Heap堆内存,会称为永久区。用于储存静态变量,加载过的类的信息,常量池,以及由于执行次数较多而被即时编译器(JIT)编译生成的代码(可通过参数设置最低执行多少次后编译成本地机器指令)


3 各个区域发生的Error

3.1 Heap堆内存

  堆内存有初始内存和最大内存两个属性,当当前内存不足的时候就会扩充。当申请的内存达到最大内存但仍不足的时候,因为内存申请失败从而导致OutOfMemoryError。

3.2 VM栈内存

在栈上存在StackOverFlowError和OutOfMemoryError两种Error:
  StackOverFlowError是由于方法递归调用层数过多而导致的。
  OutOfMemoryError则是因为Java中栈内存并不在线程间共有,每个新建线程都会占用部分内存创建自己的栈。当内存中剩余内存不足以供新线程创建栈时,就会出错。

3.3 Native栈内存

  应该同上。只不过是在本地栈中发生。

3.4 程序计数器 Program Counter Register

  Java内存中唯一不存在OutOfMemoryError的内存区域。

3.5 方法区

  当加载的类过多时,会发生OutOfMemoryError。


4 内存泄漏

  在java中,内存泄漏是指一个对象已经没有存在的价值,其内存应该被回收,但仍存在指向该对象的指针(如果两个对象互相引用,但从其他节点无法达到这两个节点,则仍会被GC),导致内存无法被回收,包括但不限于以下几种情况:

4.1 静态变量指向对象

  静态对象存在于方法区(永久区),方法区很少发生GC,对象的生命周期都很长。而如果一个静态引用指向GC相对而言发生频繁的Heap区的对象,GC发生时发现该对象仍在被引用中,就不会回收该对象。
  而在android中,如果某个类(工具类或者单例之类)的static变量引用着其他activity或者view,则极有可能发生内存泄漏,因为当activity被finish,应该被回收内存时,由于activity仍被引用,则会导致和该activity相关的所有对象的内存都无法被回收。而一个View被引用也会导致相同的问题,因为View中存在Activity的引用,View无法释放,其中Activity的引用也无法释放,间接导致Activity的内存泄漏。

4.2 非静态内部类或匿名类生命周期过长

  在java中,内部类中存在外部类的引用。换言之,如果一个内部类的生命周期比所在的外部类生命周期要长的话,外部类该被回收内存时,由于仍在被内部类所引用,导致无法被释放。比较常见的就是在Activity中开启异步线程进行操作,如果Activity关闭时,异步线程还未结束,则会造成内存泄漏。

4.3 Handler队列执行延迟

  用Handler在子线程更新主线程UI的操作非常普遍,但是使用Handler不当却存在内存泄漏的问题。如果在一个Activity的内部类Handler中请求刷新UI(通常会在自定义的内部类Handler的handleMessage中直接进行刷新操作),但是直到Activity被finish,Message仍在MessageQueue排队未被执行,就会导致内存泄漏。原因在于Message持有Handler的引用,而非静态内部类Handler则持有Activity的引用(非静态内部类持有外部类的引用)。
  因此,需要注意在多线程操作时,Runnable和Handler最好用静态内部类,如果需要Activity的引用则最好用WeakReference,以便Activity被GC时,不会因为被持有强引用而无法释放。

4.4 资源对象未关闭

  包括IO流,数据库连接,Socket等。虽然百度上很多都说会造成内存泄漏,不过StackOverFlow上几乎都只提到了资源泄漏而已,当然偶尔也有说到内存泄漏的,不过都语焉不详,在官方文档上对close方法的解释是Closes this stream and releases any system resources associated with it,不知道是不是包括了内存。

戳我查看更多内存泄漏

总之,要想不让你的应用在运行过程中因为OOM而挂掉,首先应对各种类型的对象的生命周期和 GC时机有一定的了解,并对静态引用变量和内部类的使用保持谨慎才能写出健壮的程序。


5 关于常量池

  常量池存在于方法区中,顾名思意是用于存放常量的,包括基本数据类型的包装类(不包括浮点数)和字符串,通常在编译期就确定了(当然字符串可以在运行时通过intern再添加字符串进常量池)。由于可以复用常量,避免频繁的创建和销毁对象,所以节省空间的同时又提高了效率。


另可参阅:

Android 内存泄漏总结
Java 内存区域和GC机制
java常量池概念
Java常量池理解与总结
Hunting Memory Leaks in Java

0 0