什么是内存泄露
程序向系统申请一块内存,使用完毕后没有及时清理,导致这块内存一致被占据,直到程序结束。
对于C,C++的开发者他们负责对象的重生到死,即负责对象的创建,也要负责对象的回收,需要人为管理对象的生命周期。
上述描述对于C,C++开发者来说的是贴切的。因为程序的复杂性,可能一个疏忽或遗漏使某个对象没有被正常释放,导致内存泄漏。
内存泄漏并不会马上把程序搞挂掉,但随着程序的运行,占据空间的垃圾对象越来越多,导致可用的内存越来越少,最后可能任何位置都会抛出OutOfMemoryError
,很难定位问题。
C,C++因为需要人为管理对象的生命周期,人都有疏忽的时候,导致内存泄漏。Java虚拟机有自动内存管理机制,怎么还会有内存泄漏问题呢?
Java虚拟机内存模型
引用自—深入理解Java虚拟机第三版
(1)程序计数器
是一块较小的内存空间,它可以看作是当前线程所执行的 字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器 的值来选取下一条需要执行的字节码指令。
由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一 个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因 此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程 之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地 址;如果正在执行的是本地(Native)方法,这个计数器值则应为空
(2)Java虚拟机栈
也是线程私有的,它的生命周期 与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧[1](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信 息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
(3)本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机 栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务
(4)Java堆
对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所 有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存
(5)方法区
与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
(6)运行时常量池
方法区的一部分。Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生 成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
以上描述 和结构图结合起来能够了解Java虚拟的内存模型和每一部分的作用,这对于我们有内存溢出有什么帮助么?
想象一下,Java虚拟机启动后,在系统中圈出了一大块内存,把进程所属的一整块内存分配给系统内的线程一部分,方法区一部分,堆一部分。
虽然存在扩容机制但总归是有上限的,代码编写的不合理占用内存超出了能够使用的界限就会产生内存问题。
内存溢出举例
因为JVM内存模型,每一块都有各自的职责,如果抛异常Java堆内存溢出,那么就说明在程序运行中产生了大量的无用对象,有个大致分析的方向。
各部分可能出现内存溢出的情况如下:
(1)Java堆
用于存储对象实例,写个死循环,创建新对象向集合中添加,当对象过多触及堆的最大限制并JVM无法回收时就会产生内存溢出异常
(2)虚拟机栈和本地方法
每调用一个方法就会创建一个栈帧,《Java虚拟机规范》中描述了两种情况会出现异常
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。白话一点就是方法调用层级太深了,假设虚拟机最多允许10层方法调用,开发时调用了11层方法,那么就会抛异常,最常见的情况就是方法递归调用。
如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError异常。这条是指方法内声明了过多的变量,emm 应该没人这么写代码叭?
(3)方法区和运行时常量
查了一些资料,不能很白话且准确的说出它们存储什么信息,大概存储的信息如下:类的全限定名,类型标志(是接口,类,注解还是枚举),方法信息,静态变量,静态常量,字符串值,数值等等
举两种情况 Java内存区域之方法区溢出_faith.huan的博客-CSDN博客_java 方法区溢出
当不断产生常量池中没有的新字符串的时候会产生泄漏
当大量使用动态代理创建新类时会产生泄漏
(4)程序计数器不会产生内存泄漏
垃圾回收机制
上节分析了虚拟机内存模型,每部分存储什么信息 以及为什么会发生内存泄漏,多半是因为代码不规范导致产生了过多的冗余数据
可Java虚拟机不是存在垃圾回收机制,自动管理内存么,这些冗余数据正该是被虚拟机处理掉的,为什么还会存在内存泄漏呢?
垃圾收集器主要关注Java堆和方法区,程序运行会创建多少对象,加载什么类都是在运行时才会确定的
而程序计数器、虚拟机栈、本地方法栈生命周期与线程一致。方法栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基 本上是在类结构确定下来时就已知的。
因此这几个区域的内存分配和回收都具备确定性, 在这几个区域内就不需要过多考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着 回收了。
所以垃圾回收机制简单说就是管理 new出来的对象
随之而来的问题是,程序运行时有那么多对象,如何判断哪些对象是应该回收的?有两种算法判断对象是否应该回收
引用计数算法
基本原理:在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
引用计数算法实现简单,判定效率高,但是它无法解决对象循环引用的问题 如下代码:
创建对象Test1,Test2,此时两对象的引用计数为1,
通过obj属性互相引用后引用计数为2
后将两对象置空,引用计数为1,因为obj属性仍然存在引用,即使obj属性引用的对象永远也不会用到了,垃圾收集器也不会回收。
所以Java虚拟机并没有使用引用计数算法,使用可达性算法
public static void main(String[] args) {
Test1 one = new Test1();
Test2 two = new Test2();
one.obj = two;
two.obj = one;
****one = null;
two = null;
}
可达性算法
基本原理:通过被称为GC Roots的根对象作为起点,根据引用关系向下搜索,如果某个对象到GC Roots间没有引用关系,则证明此对象可以被回收。
GC Roots对象包括一下几种:
(1)在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
(2)在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
(3)在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。 (4)在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
(5)Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
(6)所有被同步锁(synchronized关键字)持有的对象。 ·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
感觉最常见有可能出现问题的是静态变量,常用方便,容易埋雷。
Java四种引用方式
(1)强引用(Strongly Re-ference)
无论任何情况下,只要强引用关系存在,垃圾收集器就永远不会回 收掉被引用的对象
(2)软引用(Soft Reference)
被软引用关联着的对象,在系统将要发生内 存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常
(3)弱引用(Weak Reference)
被弱引用关联的对象只 能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只 被弱引用关联的对象
(4)虚引用(Phantom Reference)
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知
四种引用方法大家应该很熟悉了,Java设计引用方式可以让程序员通过代码的方式决定某些对象的生命周期,也有利于JVM进行垃圾回收。
额外要说的一点是,虚引用提及在对象被回收时收到一个系统通知,除了强引用外都有这个特性,如下代码:
其实就是在构造方法中传入队列ReferenceQueue
,当引用中的对象被GC清理后,会向队列中添加被清理对象的引用,所以可以通过引用队列判断哪些对象被回收了。
ReferenceQueue<String> queue = new ReferenceQueue<>();
SoftReference<String> softReference = new SoftReference<String>("1",queue);
WeakReference<String> weakReference = new WeakReference<String>("1",queue);
PhantomReference<String> phantomReference= new PhantomReference<String>("1",queue);
Android当中的内存泄漏
本质上是生命周期长的对象持有生命周期短的对象,从而导致生命周期短的对象不能被释放。
常见的内部类Handler持有Activity引用导致内存泄漏。
为什么?
handler发送message到主线程队列中执行,message中持有发送handler的引用,handler是内部类会自动持有外部类的引用。
在message执行过程中,Activity可能已经关闭了,但是由于上述引用链条的存在,Activity无法被回收,如果在handler中进行UI操作修改个文本啥的直接就崩溃了。
这就是Handler的生命周期 比 Activity的生命周期长。
参考
周志明—深入Java虚拟机第三版