简单点说,当一个对象不再被使用(即失去了利用价值),但它的引用却依然被其他对象所持有,导致JVM的垃圾回收机制无法回收释放此对象,则该无用对象继续占用内存空间(占着茅坑。。。),即内存泄漏了。所有内存都被占用且无法进行垃圾回收时,就会发生内存溢出。
全面点说,那要从java虚拟机运行时的数据区域说起,《深入理解java虚拟机:JVM高级特性与最佳实践》这本书对此阐释很明确,jvm所管理的内存将会包括以下几个运行时的数据区域:
这几个数据区除程序计数器外都会发生内存溢出问题(内存泄露的后果是内存溢出,但内存溢出并不都是有内存泄露引起的)。
程序计数器是一块较小的内存空间,它可以看作是当前线程执行字节码的行号指示器,在虚拟机的概念模型里,字节码解释器的工作就是通过改变程序计数器的值来选去下一条执行的字节码指令,分之,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个程序计数器来完成。每条线程都持有一个独立的程序计数器,各个计数器之间互不影响独立存储,这类内存区域被称为“线程私有”内存。 如果执行java指令,此计数器中存有当前执行的字节码指令的行数,若执行native方法,则这个计数器的值为空(Undefined)。此内存区域是唯一一个jvm规范没有规定OutOfMemoryError的区域。
虚拟机栈也是一块儿线程私有内存,它描述的是java方法执行的内存模型:每个方法在执行的时候会创建一个帧栈,这个帧栈用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法调用到执行完的过程就是帧栈从虚拟机栈入栈到出栈的过程。我们常说的“堆栈”中的栈就是指虚拟机栈,更精确的说是指虚拟机栈中存储的局部变量表。局部变量表中存储了编译期可知的基本数类型,对象的引用,以及returnAddress类型——指向一条字节码指令的地址。(long和double占两个Slot——局部变量空间,其他类型占一个Slot) 当进入一个方法是,该方法对应的局部变量空间是确定了的,并且在方法执行期间不会改变。在jvm规范中,对这个区域规定了两种异常状况:如果请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverflowError;如果虚拟机可以动态扩展,并且在扩展时无法申请到足够内存,则或抛出OutOfMemoryError。
本地方法栈同虚拟机栈功能类似,都是为了执行方法而服务的,只不过本地方法栈是服务于Native方法,jvm并没有对此区域做具体的规定,同虚拟机栈一样本地方法栈也会抛出上文两个异常。
java堆我们很熟悉了,它是虚拟机管理的内存区域中最大的一块,被所有线程所共享,在虚拟机启动时被创建。堆存在的唯一目的是存储对象的实例,但随着技术的发展,堆的作用也不是那么“绝对”了。堆也是垃圾收集器管理的主要区域,jvm规范规定堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。堆的大小可以是固定的,也可以是扩展的,如果在堆中没有完成实例分配,并且堆也无法在扩展时,将会抛出OutOfMemoryError异常。
方法区也是一块被线程共享的区域,它用于存储已经被虚拟机挂在的类信息、常量、静态变量、即时编译器编译后的代码等数据,虽然Jvm规范把方法区描述为堆的一个逻辑部分,但他却有一个别名叫做Non-Heap(非堆),目的应该是与java堆区分开来。jvm对方法区的规范较为宽松,也可以不实现垃圾收集,当方法区无法满足内存分配需求时,会抛出OutOfMemoryError异常。
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区时存放。它同时受到方法区内存的限制,当其无法申请内存时会抛出OutOfMemoryError异常。
直接内存并不是虚拟机运行时数据区的一部分,但这部分内存也会被频繁的使用,当jvm动态扩展时各区域总内存超过了物理内存限制,也会抛出OutOfMemoryError异常。
在Android开发中我们主要关心的是java堆中的内存泄漏问题,Android中常见的内存泄漏有一下几个地方: 资源使用完毕没有关闭:数据库cursor,流等 静态变量、单例持有对象的引用会使该对象无法销毁 无限循环的属性动画也会使activity无法销毁 context被生命周期常于activity的对象持有导致activity无法销毁 集合类持有无用对象的引用 匿名内部类或非静态内部类实例会持有外部类的引用,导致无法回收 handler引用被MessageQueue持有直到消息被送达
新闻热点
疑难解答