《深入理解java虚拟机 笔记》
第三章 垃圾收集器与内存分配策略
此章主要围绕三个问题,java虚拟机中哪些内存需要回收?什么时候回收?如何回收?第二章中介绍了虚拟机内存中程序计数器、虚拟机栈、本地方法栈三个数据区随着线程结束而消亡,所以这三个区域不用考虑回收问题。因此需要注意的,垃圾回收是在java堆和方法区中回收,另外三个区域不用。
如何判断出对象已死?(对象已死是回收必要条件)
引用计数算法
作者说,很多应届生都是这么回答的,给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。但是他自己并没有给出定义。在主流的java虚拟机中没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。
要查看GC日志,需要设置一下jvm的参数。
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:…/logs/gc.log 日志文件的输出路径
ReferenceCountingGC例子中, objA.instance = objB; objB.instance = objA;两个对象相互引用,其引用计数都不为0,但是GC收集器还是回收他们,说明虚拟机并不是使用引用计数算法判断对象是否存活的。
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;
//假设在这行发生GC,objA和objB是否能被回收?
System.gc();
}
public static void main(String[] args) {
ReferenceCountingGC referenceCountingGC = new ReferenceCountingGC();
referenceCountingGC.testGC();
}
}
运行结果:
[GC (System.gc()) [PSYoungGen: 7455K->2536K(18944K)] 7455K->2688K(62976K), 0.0027933 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 2536K->0K(18944K)] [ParOldGen: 152K->2594K(44032K)] 2688K->2594K(62976K), [Metaspace: 2727K->2727K(1056768K)], 0.0246085 secs] [Times: user=0.05 sys=0.00, real=0.02 secs]
Heap
PSYoungGen total 18944K, used 164K [0x00000000eb300000, 0x00000000ec800000, 0x0000000100000000)
eden space 16384K, 1% used [0x00000000eb300000,0x00000000eb3290d0,0x00000000ec300000)
from space 2560K, 0% used [0x00000000ec300000,0x00000000ec300000,0x00000000ec580000)
to space 2560K, 0% used [0x00000000ec580000,0x00000000ec580000,0x00000000ec800000)
ParOldGen total 44032K, used 2594K [0x00000000c1800000, 0x00000000c4300000, 0x00000000eb300000)
object space 44032K, 5% used [0x00000000c1800000,0x00000000c1a88918,0x00000000c4300000)
Metaspace used 2734K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 304K, capacity 386K, committed 512K, reserved 1048576K
由于调用了System.gc()方法,所以触发了 [Full GC (System.gc()),PSYoungGen为Parallel Scavenge收集器新生代,ParOldGen为Parallel Scavenge收集器老年代。2688K->2594K(62976K)表示回收前该内存区域使用容量2688K,回收后该内存使用容量2594K,虚拟机并没有因为这两个对象相互引用就没有回收它们。
可达性算法
在主流的商用程序语言(Java、C#,甚至包括前面提到的古老的Lisp)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图3-1所示,对象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)、虚引用(PhantomReference)4种,这4种引用强度依次逐渐减弱。
强引用描述垃圾收集器永远不会回收对象,就是指在程序代码之中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。
可达性分析算法中不可达的对象并非“非死不可”
在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记。
第二次标记:第二次标记发生在“即将回收”的集合中,进一步标记确认要回收的对象,当然如果对象执行方法finalize()成功拯救了自己就不会标记。
如果对象有覆盖finalize()方法并且系统之前没有调用过对象的finalize()方法方法,那么这个对象会放在一个F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。对象在执行方法finalize()中要想成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合。
如果对象的finalize()方法有被系统调用过,那将不会再执行finalize()方法,第二次标记时会直接标记确认回收。
从代码清单3-2中我们可以看到一个对象的finalize()被执行,但是它仍然可以存活。
/**
*此代码演示了两点:
*1.对象可以在被GC时自我拯救。
*2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
*@author zzm
*/
public class FinalizeEscapeGC{
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive(){
System.out.println("yes,i am still alive:)");
}
@Override
protected void finalize()throws Throwable{
super.finalize();
System.out.println("finalize mehtod executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[]args)throws Throwable{
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
//因为finalize方法优先级很低,所以暂停0.5秒以等待它
Thread.sleep(500);
if(SAVE_HOOK != null){
SAVE_HOOK.</