垃圾回收机制
1、为什么需要垃圾回收
Java 程序在虚拟机中运行,是会占用内存资源的,比如创建的对象、加载的类型数据等,而且内存资源都是有限的。当创建的对象不再被引用时,就需要被回收掉,释放内存资源,这个时候就会用到JVM的垃圾回收机制。
JVM 启动时就提供了一个垃圾回收线程来跟踪每一块分配出去的内存空间,并定期清理需要被回收的对象。Java 程序无法强制执行垃圾回收,我们可以通过调用 System.gc 方法来"建议"执行垃圾回收,但是否可执行,什么时候执行,是不可预期的。
2、垃圾回收发生在哪里
JVM内存模型中,程序计数器、虚拟机栈、本地方法栈这 3 个区域是线程私有的,随着线程的创建而创建,销毁而销毁。栈中的栈帧随着方法的调用而入栈,随着方法的退出而出栈,每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的。因此这三个区域的内存分配和回收都具有确定性。
而堆和方法区这两个区域则有着显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾回收的重点就是关注堆和方法区中的内存,堆中的回收主要是垃圾对象的回收,方法区的回收主要是废弃常量和无用的类的回收。
3、对象在什么时候可以被回收
一般一个对象不再被引用,就代表该对象可以被回收。主流的虚拟机一般都是使用 可达性分析算法 来判断该对象是否可以被回收,有些内存管理系统也是用 引用计数法 来判断。
1)引用计数算法:
这种算法是通过在对象中添加一个引用计数器来判断该对象是否被引用了。每当对象被引用,计数器就加 1;每当引用失效,计数器就减 1。当对象的引用计数器的值为 0 时,就说明该对象不再被引用,可以被回收了。
引用计数算法实现简单,判断效率也很高,但它无法解决对象之间相互循环引用的问题。两个对象若互相引用,但没有任何其它对象引用他们,而它们的引用计数器都不为零,就无法被回收。
2)可达性分析算法:
GC Roots 是该算法的基础,GC Roots 是所有对象的根对象。在垃圾回收时,会从这些 GC Roots 根对象开始向下搜索,在搜索的这个引用链上的对象,就是可达的对象;而一个对象到 GC Roots 没有任何引用链相连时,就证明此对象是不可达的,可以被回收。
在Java中,可作为 GC Roots 对象的一般包括如下几种:
- Java虚拟机栈中的引用的对象,如方法参数、局部变量、临时变量等
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象,如字符串常量池的引用
- 本地方法栈中JNI的引用的对象
- Java虚拟机内部的引用,如基本数据类型的 Class 对象,系统类加载器等
比如下面的代码:
其中,类静态变量 MAPPER,loadAccount 方法的局部变量 account1、account2、accountList 都可以作为 GC Roots(ArrayList 内部是用 Object[] elementData 数组来存放元素的)。
在调用 loadAccount 方法时,堆中的对象都是可达的,因为有 GC Roots 直接或间接引用到这些对象,此时若发生垃圾回收,这些对象是不可被回收的。loadAccount 执行完后,弹出栈帧,方法内的局部变量都被回收了,虽然堆中 ArrayList 对象还指向 elementData 数组,而 elementData 指向 Account 对象,但没有任何 GC Roots 的引用链能达到这些对象,因此这些对象将变为垃圾对象,被垃圾回收器回收掉。
4、回收方法区
方法区垃圾回收的“性价比”通常是比较低的,方法区的垃圾回收主要回收两部分内容:废弃的常量和不再使用的类型。
1)废弃的常量:
- 如常量池中废弃的字面量,字段、方法的符号引用等
2)不再使用的类型:
判定一个类型是否属于“不再被使用的类”需要同时满足三个条件:
- 该类所有的实例都已经被回收,Java堆中不存在该类及其任何派生子类的实例
- 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
5、Java中的引用类型
Java 中有四种不同的引用类型:强引用、软引用、弱引用、虚引用,这4种引用强度依次逐渐减弱。
1)强引用:
强引用是最普遍的引用方式,如在方法中定义:Object obj = new Object()。只要引用还在,垃圾回收器就不会回收被引用的对象。
2)软引用:
软引用是用来描述一些有用但非必须的对象,可以使用 SoftReference 类来实现软引用。对于软引用关联着的对象,在系统将要发生内存溢出异常之前(一般发生老年代GC时),会把这些对象列进回收范围之中。如果回收之后内存还是不足,才会报内存溢出的异常。
这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现内存缓存,当内存快满时,就回收掉这些软引用的对象,然后需要的时候再重新查询。比如下面的代码:
3)弱引用:
弱引用是用来描述非必须的对象,可以使用 WeakReference 类来实现弱引用。它只能生存到下一次垃圾回收发生之前(一般发生年轻代GC时),当垃圾回收机制开始时,无论是否会内存溢出,都将回收掉被弱引用关联的对象。
需注意的是,我们使用 SoftReference 来创建软引用对象,使用 WeakReference 来创建弱引用对象,垃圾回收时,是回收它们关联的对象,而不是 Reference 本身。同时,如果 Reference 关联的对象被其它 GC Roots 引用着,也是不能被回收的。如下面的代码,在垃圾回收时,只有 T002 这个 Account 对象能被回收,回收后 reference2.get() 返回值为 null,account、reference1、reference2 所指向的对象都不能被回收。
4)虚引用:
最没有存在感的一种引用关系,可以使用 PhantomReference 类来实现虚引用。存在不存在几乎没影响,也不能通过虚引用来获取一个对象实例,存在的唯一目的是被垃圾回收器回收后可以收到一条系统通知。