垃圾收集器
垃圾收集需要完成三件事情:
- 哪些地方的内存需要回收?
- 如何判断能否回收?
- 如何回收?
哪些内存需要回收
程序计数器, 虚拟机栈, 本地方法栈这3个区域随线程而生, 随线程而灭, 栈中的栈帧随着方法的进入和退出而有条不紊的执行着出栈和入栈操作. 每一个栈帧中分配多少内存基本上在类结构确定下来时就已知了. 因此这几个区域的内存分配和回收都具备确定性, 在这几个区域内不需要过多考虑如何回收的问题, 当方法结束或者线程结束时, 内存就跟着回收了.
而Java堆和方法区这两个区域则有很显著的不确定性: 一个接口的多个实现类需要的内存可能会不一样, 一个方法所执行的不同条件分支所需要的内存也可能不一样, 只有处于运行期间, 我们才能知道程序究竟会创建哪些对象. 创建多少个对象, 这部分内存的分配和回收是动态的. 垃圾收集器所关注的正是这部分内存该如何管理.
如何判断能否回收(如何判定对象已死)
Java堆里面存放这几乎所有的对象实例, 垃圾收集器在对Java堆进行回收前, 需要先确定这对象哪些还“存活”, 哪些已经“死去”.
引用计数算法(Reference Counting)
在对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器值加1; 当引用失效时, 计数器值就减去1; 任何时刻计数器为0的对象就说不可能再被使用的. 虽然占用了一些额外的内存空间来进行计数, 但是原理简单, 判定效率高, 在大多数情况下都是一个不错的算法.
在Java中没有选用引用计数法来管理内存, 主要原因是, 在例外情况下需要配合大量额外处理才能保证正确的工作, 譬如单纯的引用计数就难以解决对象之间的相互循环引用问题.
-
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public class ReferenceCountingGC { public Object instance = null; public static void testGC () { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC ****objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; System.gc(); } }
这个例子中: 对象objA和objB都有字段instance, 赋值令 objA.instance = objB及 objB.instance = objA. 除此之外, 这两个对象再无其他任何引用. 由于它们的引用计数器都不为零, 引用计数算法就无法回收它们.
objA.instance = objB. 这时objB的引用计数器加1; objB.instance = objA. 这时objA的引用计数器加1.
当回收objA时, 由于计数器为1, 无法删除, 引用对象为B. 当回收objB时, 同样计数器为1, 也无法删除, 引用对象为A.
可达性分析算法(Reachability Analysis)
当前主流的程序语言内存管理子系统都是通过可达性分析算法来判定对象是否存活的.
基本思路是通过一系列称为GC Roots的根对象作为起始节点集, 从这些节点开始, 根据引用关系向下搜索, 搜索过程所走的路径称为“引用链”(Reference Chain), 如果某个对象到GC Roots间没有任何引用链相连, 用图论的话说就是GC Roots到这个对象不可达时, 证明这个对象是不可能再被使用的.
这里的obj5, obj6, obj7直接虽然互有关联, 但是它们到GC Roots是不可达的, 因此它们将被判定为可回收的对象.
固定可以作为GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧的本地变量表)中引用的对象 譬如各个线程被调用的方法堆栈中使用到的参数, 局部变量, 临时变量等
- 在方法区中类静态属性引用的对象 譬如Java类的引用类型静态变量
- 在方法区中常量引用的对象 譬如字符串常量池里的引用
- 在本地方法栈中JNI引用的对象 通常所说的Native方法
- Java虚拟机内部的引用 如基本数据类型对应的Class对象, 一些常驻的异常对象(NPE, OOM)等, 系统类加载器
- 所有被同步锁持有的对象 synchronized关键字
- 反应Java虚拟机内部情况的JMXBean, JVMTI中注册的回调, 本地代码缓存等.
除了这些固定的GC Roots集合以外, 根据用户所选用的垃圾收集器以及当前回收的内存区域不同, 还可以有其他对象临时性的加入, 共同构成完整的GC Roots集合.
引用
无论是通过引用计数算法判定对象的引用数量, 还是通过可达性分析算法判定对象是否引用链可达, 判定对象是否存活都和“引用”离不开关系
在JDK1.2之前, Java里面的引用定义是: 如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址, 就称该reference数据是代表某块内存, 某个对象的引用.
一个对象在这种定义下只有“被引用”或“未被引用”两种状态, 当我们希望描述这样一类对象(当内存空间还足够时, 保留在内存之中, 如果内存空间在进行垃圾回收后仍然非常紧张, 那就可以抛弃这些对象)时就无能为力了.
在JDK1.2之后, Java对引用的概念进行了扩充, 将引用分为强引用(Strongly Reference), 软引用(Soft Reference), 弱引用(Weak Reference)和虚引用(Phantom Reference)4种, 这4种引用强度依次逐渐减弱.
- 强引用
是指在程序代码之中普遍存在的引用赋值. 任何情况下, 只要强引用关系还存在, 垃圾收集器就永远不会回收掉被引用的对象.
- 软引用
描述一些还有用, 但非必须的对象. 只被软引用关联着的对象, 在系统将要发生内存溢出异常前, 会把这些对象列进回收范围之内进行第二次回收, 如果这次回收还没有足够的内存, 才会抛出内存溢出异常. 提供了SoftReference类来实现软引用.
- 弱引用
用来描述那些非必须对象, 但是它的强度比软引用更弱一些, 被弱引用关联的对象只能生存到下一次垃圾收集发生为止. 当垃圾收集器开始工作时, 无论当前内存是否足够, 都会回收掉被弱引用关联的对象. 提供了WeakReference类实现来弱引用
- 虚引用
也被称为“幽灵引用”或者“幻影引用”, 它是最弱的一种引用关系. 一个对象是否有虚引用的存在, 完全不会对其生存时间构成影响, 也无法通过虚引用来取得一个对象实例. 为一个对象设置虚引用关联的唯一目的是为了能在这个对象被收集器回收时收到一个系统通知. 提供了PhantomReference类来实现虚引用.
二次标记
在可达性分析算法中判定为不可达的对象, 也不一定会真正被回收掉, 要真正回收, 至少要经历两次标记过程.
- 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链, 那它将会被第一次标记.
然后进行一次筛选, 筛选的条件是此对象是否有必要执行finalize方法, 假如对象没有覆盖finalize
方法, 或者finalize
方法已经被虚拟机调用过, 那么虚拟机将这两种情况都视为“没有必要执行” — finalize
方法只会被系统自动调用一次
- 如果这个对象被判定为有必要执行
finalize
方法, 那么虚拟机会去执行finalize
方法, 如果对象在finalize
方法中重新与引用链上的任何一个对象建立关联. 那么在第二次标记时它将被移出“即将回收”的集合; 如果没有重新建立连接, 那么它才会真正被回收.
回收方法区
方法区垃圾回收的“性价比”是比较低的, 而且《Java虚拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾回收. — 在Java堆中, 尤其是在新生代中, 对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间
方法区的垃圾收集主要回收两部分内容:
-
废弃的常量
-
不在使用的类型
判定一个类型是否属于“不再被使用的类”条件就比较苛刻,需要同时满足下面三个条件
- 该类所有的实例都已经被回收 Java堆中不存在该类及其任何派生子类的实例
- 加载该类的类加载器已经被回收 这个条件通常很难达成, 除非是经过精心设计的可替换类加载器的场景
- 该类对于的java.lang.Class堆栈没有在任何地方被引用 无论在任何地方通过反射访问该类的方法
Java虚拟机被允许对满足上述三个条件的无用类进行回收.
在大量使用反射, 动态代理, CGLib等字节码框架, 动态生成JSP以及OSGi这类频繁自定义类加载器的场景中, 通常都需要Java虚拟机具有类型卸载的能力, 以保证不会对方法区造成过大的内存压力.