一、为什么需要垃圾回收
如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收。除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此。所以,垃圾回收是必须的。
二、哪些内存需要进行垃圾回收
对于虚拟机中线程私有的区域,如程序计数器、虚拟机栈、本地方法栈都不需要进行垃圾回收,因为它们是自动进行的,随着线程的消亡而消亡,不需要我们去回收,比如栈的栈帧结构,当进入一个方法时,就会产生一个栈帧,栈帧大小也可以借助类信息确定,然后栈帧入栈,执行方法体,退出方法时,栈帧出栈,于是其所占据的内存空间也就被自动回收了。而对于虚拟机中线程共享的区域,则需要进行垃圾回收,如堆和方法区,线程都会在这两个区域产生自身的数据,占据一定的内存大小,并且这些数据又可能会存在相互关联的关系,所以,这部分的区域不像线程私有的区域那样可以简单自动的进行垃圾回收,此部分区域的垃圾回收非常复杂,而垃圾回收也主要是针对这部分区域。
三、垃圾收集算法
任何垃圾收集算法都必须做两件事情。首先,它必须检测出垃圾对象。其次,它必须回收垃圾对象所使用的堆空间并还给程序。那么问题来了,如何检测出一个对象是否为垃圾对象呢?一般有两种算法解决这个问题。1. 引用计数算法 2. 可达性分析算法。
1.引用计数算法
堆中的每一个对象有一个引用计数,当一个对象被创建,并把指向该对象的引用赋值给一个变量时,引用计数置为1,当再把这个引用赋值给其他变量时,引用计数加1,当一个对象的引用超过了生命周期或者被设置为新值时,对象的引用计数减1,任何引用计数为0的对象都可以被当成垃圾回收。当一个对象被回收时,它所引用的任何对象计数减1,这样,可能会导致其他对象也被当垃圾回收。
问题:很难检测出对象之间的额相互引用(引用循环问题)
如下代码段可以从反面验证虚拟机的垃圾回收不是采用的引用计数。
package com.leesf.chapter3; public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过 */ private byte[] bigSize = new byte[2 * _1MB]; public static void testGC() { // 定义两个对象 ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); // 给对象的成员赋值,即存在相互引用情况 objA.instance = objB; objB.instance = objA; // 将引用设为空,即没有到堆对象的引用了 objA = null; objB = null; // 进行垃圾回收 System.gc(); } public static void main(String[] args) { testGC(); } }
代码的运行参数设置为: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
在代码objA = null 和 objB = null 之前,内存结构示意图如下
注意:局部变量区的第一项并没有this引用,因为testGC方法是类方法。
在代码objA = null 和 objB = null 之后,内存结构示意图如下
objA和objB到堆对象的引用已经没有了,但是ReferenceCountingGC对象内部还存在着循环引用,我们在图中也可以看到。即便如此,JVM还是把这两个对象当成垃圾进行了回收。具体的GC日志如下:
由GC日志可知发生了两次GC,由11390K -> 514K,即对两个对象都进行了回收,也从侧面说明JVM的垃圾收集器不是采用的引用计数的算法来进行垃圾回收的。
2.可达性分析算法
此算法的基本思想就是选取一系列GCRoots对象作为起点,开始向下遍历搜索其他相关的对象,搜索所走过的路径成为引用链,遍历完成后,如果一个对象到GCRoots对象没有任何引用链,则证明此对象是不可用的,可以被当做垃圾进行回收。
那么问题又来了,如何选取GCRoots对象呢?在Java语言中,可以作为GCRoots的对象包括下面几种:
1. 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
2. 方法区中的类静态属性引用的对象。
3. 方法区中常量引用的对象。
4. 本地方法栈中JNI(Native方法)引用的对象。
下面给出一个GCRoots的例子,如下图,为GCRoots的引用链。
由图可知,obj8、obj9、obj10都没有到GCRoots对象的引用链,即便obj9和obj10之间有引用链,他们还是会被当成垃圾处理,可以进行回收。
四、对象的内存布局
Java中我们提到最多的应该就是对象,但是我们真的了解对象吗,对象在内存中的存储布局如何?对象的内存布局如下图所示
几点说明:1.Mark Word部分数据的长度在32位和64位虚拟机(未开启压缩指针)中分别为32bit和64bit。然后对象需要存储的运行时数据其实已经超过了32位、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的外存储成本,
Mark Word一般被设计为非固定的数据结构,以便存储更多的数据信息和复用自己的存储空间。2.类型指针,即指向它的类元数据的指针,用于判断对象属于哪个类的实例。3.实例数据存储的是真正有效数据,如各种字段内容,各字段的分配策略为longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同宽度的字段总是被分配到一起,便于之后取数据。父类定义的变量会出现在子类前面。3.对齐填充部分仅仅起到占位符的作用,并非必须。
说完对象的内存布局,现在来说说对象的引用,当我们在堆上创建一个对象实例后,如何对该对象进行操作呢?好比一个电视机,我如何操作电视机来收看不同的电视节目,显然我们需要使用到遥控,而虚拟机中就是使用到引用,即虚拟机栈中的reference类型数据来操作堆上的对象。现在主流的访问方式有两种:
1. 使用句柄访问对象。即reference中存储的是对象句柄的地址,而句柄中包含了对象示例数据与类型数据的具体地址信息,相当于二级指针。
2. 直接指针访问对象。即reference中存储的就是对象地址,相当于一级指针。
两种方式有各自的优缺点。当垃圾回收移动对象时,对于方式一而言,reference中存储的地址是稳定的地址,不需要修改,仅需要修改对象句柄的地址;而对于方式二,则需要修改reference中存储的地址。从访问效率上看,方式二优于方式一,因为方式二只进行了一次指针定位,节省了时间开销,而这也是HotSpot采用的实现方式。下图是句柄访问与指针访问的示意图。
五、对象的引用
前面所谈到的检测垃圾对象的两种算法都是基于对象引用。在Java语言中,将引用分为强引用、软引用、弱引用、虚引用四种类型。引用强度依次减弱。具体如下图所示
对于可达性分析算法而言,未到达的对象并非是“非死不可”的,若要宣判一个对象死亡,至少需要经历两次标记阶段。1. 如果对象在进行可达性分析后发现没有与GCRoots相连的引用链,则该对象被第一次标记并进行一次筛选,筛选条件为是否有必要执行该对象的finalize方法,若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视作不必要执行该对象的finalize方法,即该对象将会被回收。反之,若对象覆盖了finalize方法并且该finalize方法并没有被执行过,那么,这个对象会被放置在一个叫F-Queue的队列中,之后会由虚拟机自动建立的、优先级低的Finalizer线程去执行,而虚拟机不必要等待该线程执行结束,即虚拟机只负责建立线程,其他的事情交给此线程去处理。2.对F-Queue中对象进行第二次标记,如果对象在finalize方法中拯救了自己,即关联上了GCRoots引用链,如把this关键字赋值给其他变量,那么在第二次标记的时候该对象将从“即将回收”的集合中移除,如果对象还是没有拯救自己,那就会被回收。如下代码演示了一个对象如何在finalize方法中拯救了自己,然而,它只能拯救自己一次,第二次就被回收了。具体代码如下
/* * 此代码演示了两点: * 1.对象可以再被GC时自我拯救 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次 * */ public class FinalizeEscapeGC { public String name; public static FinalizeEscapeGC SAVE_HOOK = null; public FinalizeEscapeGC(String name) { this.name = name; } public void isAlive() { System.out.println("yes, i am still alive :)"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed!"); System.out.println(this); FinalizeEscapeGC.SAVE_HOOK = this; } @Override public String toString() { return name; } public static void main(String[] args) throws InterruptedException { SAVE_HOOK = new FinalizeEscapeGC("leesf"); System.out.println(SAVE_HOOK); // 对象第一次拯救自己 SAVE_HOOK = null; System.out.println(SAVE_HOOK); System.gc(); // 因为finalize方法优先级很低,所以暂停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead : ("); } // 下面这段代码与上面的完全相同,但是这一次自救却失败了 // 一个对象的finalize方法只会被调用一次 SAVE_HOOK = null; System.gc(); // 因为finalize方法优先级很低,所以暂停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead : ("); } } }
运行结果如下:
leesf
null
finalize method executed!
leesf
yes, i am still alive :)
no, i am dead : (
由结果可知,该对象拯救了自己一次,第二次没有拯救成功,因为对象的finalize方法最多被虚拟机调用一次。此外,从结果我们可以得知,一个堆对象的this(放在局部变量表中的第一项)引用会永远存在,在方法体内可以将this引用赋值给其他变量,这样堆中对象就可以被其他变量所引用,即不会被回收。