经过半个世纪的发展,内存的动态分配与内存回收技术已经相当成熟,一切看起来都进入了"自动化"时代,那为什么我们还要了解GC和内存分配呢?答案很简单:当需要排查各种内存溢出,内存泄漏时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些"自动化"的技术实施必要的监控和调节。
程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内不需要过多的考虑垃圾回收问题,因为方法结束或者线程结束时,内存自然就能跟随着回收了。Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分的内存分配和回收是动态的,垃圾收集器所关注的是这部分的内存。
堆中几乎存放着Java世界中所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是确定这些对象哪些还"存活"着,哪些已经"死去"。
引用计数算法
很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。
客观的说,引用技术算法的实现简单,判定效率也很高,在大部分情况下他都是一个不错的算法。但是Java语言中没有选用引用计数算法来管理内存,其中最主要的原因是他很难解决对象之间的相互引用的问题。
举个简单的例子:对象objA和objB都有字段instance,赋值令objA.instance=objB,及objB.instance=objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相饮用者对方,胆汁它们的引用计数都不为0,于是引用计数算法无法统治GC收集器回收它们。
package com.dong.test;
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();
}
}
在<<深入理解java虚拟机>>的书中,没有给出main方法,因此需要我们自己写出main方法。并且在eclipse 的run ——>run configuration中配置相应的打印日志的方法。有以下四种输出方式:
-verbose:gc (开启打印垃圾回收日志)
-Xloggc:D:testgc.log (设置垃圾回收日志打印的文件,文件名称可以自定义)
-XX:+PrintGCTimeStamps (打印垃圾回收时间信息时的时间格式)
-XX:+PrintGCDetails (打印垃圾回收详情)
运行的结果如下图所示:
[GC (Allocation Failure) [DefNew: 2779K->483K(4928K), 0.0022188 secs] 2779K->2531K(15872K), 0.0022589 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [Tenured: 2048K->482K(10944K), 0.0017852 secs] 4666K->482K(15872K), [Metaspace: 78K->78K(4480K)], 0.0018335 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 4992K, used 89K [0x04800000, 0x04d60000, 0x09d50000)
eden space 4480K, 2% used [0x04800000, 0x048167e0, 0x04c60000)
from space 512K, 0% used [0x04c60000, 0x04c60000, 0x04ce0000)
to space 512K, 0% used [0x04ce0000, 0x04ce0000, 0x04d60000)
tenured generation total 10944K, used 482K [0x09d50000, 0x0a800000, 0x14800000)
the space 10944K, 4% used [0x09d50000, 0x09dc89a0, 0x09dc8a00, 0x0a800000)
Metaspace used 78K, capacity 2242K, committed 2368K, reserved 4480K
从运行结果中可以清楚地看到GC日志中包含"4666k->482k",意味着虚拟机并没有因为这两个对象相互引用就不回收它们,这也从侧面说明虚拟机并不是通过引用计数法来判断对象是否存活的。
根搜索算法
在主流的商用程序语言(Java、C # ,甚至包括前面提到的古老的Lisp ) 的主流实现中, 都是称通过可达性分析( Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链( Reference Chain ) ,当一个对象到GC Roots没有任何引用链相连 (用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如下图所示,对象object 5、 object 6、 object 7虽然互相有矣联,但是它们到GC Roots是不可达的 ,所以它们将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI ( 即一般说的Native方法)引用的对象。
再谈引用
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在JDK 1.2以前,Java中的引用的定义很传统 :如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以拋弃这些对象。很多系统的缓存功能都符合这样的应用场景。
在JDK 1.2之后 ,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference )、软引用(Soft Reference )、弱引用(Weak Reference )、虚引用(Phantom Reference) 4种 , 引用强度依次逐渐减弱。
强引用就是指在程序代码之中普遍存在的,类似“Object obj=new Object ( ) ”这类的引 用 ,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会拋出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够, 都会回收掉只被弱引用关联的对象。在JDK 1.2之后 ,提供了WeakReference类来实现弱引用。
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
参考资料:
1.深入理解java虚拟机:jvm高级特性与最佳实践 周志明著 2011