欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JVM探秘:四种引用、对象的生存与死亡

程序员文章站 2022-04-18 21:10:20
本系列笔记主要基于《深入理解Java虚拟机:JVM高级特性与最佳实践 第2版》,是这本书的读书笔记。 Java虚拟机的内存区域中,程序计数器、Java栈和本地方法栈是线程私有的,随线程而生随线程而灭,因此这几个区域的内存回收和分配都有确定性,所以主要探究的是Java堆和方法区的内存分配及回收。 Ja ......

本系列笔记主要基于《深入理解java虚拟机:jvm高级特性与最佳实践 第2版》,是这本书的读书笔记。

java虚拟机的内存区域中,程序计数器、java栈和本地方法栈是线程私有的,随线程而生随线程而灭,因此这几个区域的内存回收和分配都有确定性,所以主要探究的是java堆和方法区的内存分配及回收。

java堆

在java堆中存放着所有的对象实例,垃圾收集器在对堆进行回收前,第一件事就是判断这些对象中哪些还存活,哪些已经死去(即不会再被使用到的对象)。

java中的引用

jdk1.2及之前,关于引用的定义是这样的:如果一块内存中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表一个引用(reference)。但是这种定义比较狭隘,一个对象就只有被引用和没有被引用两种状态。还有这样一种“食之无味,弃之可惜”的对象:当内存空间充足时,则能继续保留在内存中,如果内存空间在垃圾收集后非常紧张,则可以抛弃这些对象。很多缓存功能都符合这样的应用场景。

jdk1.2之后,对引用的概念进行了扩充,将引用分为强引用(strong reference)、软引用(soft reference)、弱引用(weak reference)、虚引用(phantom reference)4种,这4种引用强度依次递减:

  • 强引用(strong reference)就是在代码中普遍存在的,类似“object obj = new object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收被引用的对象。

  • 软引用(soft reference)是用来描述有用非必需的对象。软引用关联的对象,在系统将要发生内存溢出之前,将会对这些对象进行二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。上面所说的“食之无味,弃之可惜”的对象就是属于软引用。

  • 弱引用(weak reference)是用来描述非必需的对象,但是比软引用更弱一些,弱引用关联的对象只能生存到下一次垃圾收集发生之前。当下一次垃圾收集时,无论内存是否足够,都会回收掉被弱引用关联的对象。

  • 虚引用(phantom reference)也称为幽灵引用或者幻影引用,它是最弱的一种引用。一个对象是否有虚引用存在,完全不会对其生存时间造成任何影响,也无法通过虚引用获得一个对象实例。为对象设置虚引用的目的,就是能在这个对象被收集器回收时收到一个系统通知。

引用计数算法

很多书中判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当一个地方引用它,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0 的对象就是不再被使用的。

引用记数算法虽然实现简单,判定效率也高,但是有一个弊端,就是它很难解决对象之间相互循环引用的问题。下面的代码中,obja和objb互相引用,如果使用引用计数法,这两个对象的引用计数器值都为1,会导致垃圾收集器无法回收它们。

/**
 * 引用记数算法测试
 * vm args: -xx:+printgcdetails
 * run with jdk 1.8
 * */
public class referencecountinggc {

    public object instance = null;
    private static final int _1m = 1024 * 1024;
    private byte[] bigsize = new byte[2 * _1m];

    public static void main(string[] args) {
        referencecountinggc obja = new referencecountinggc();
        referencecountinggc objb = new referencecountinggc();
        obja.instance = objb;
        objb.instance = obja;

        obja = null;
        objb = null;

        //这时发生gc,obja和objb能否被回收?
        system.gc();
    }
}

运行结果:

[gc (system.gc()) [psyounggen: 7432k->728k(38400k)] 7432k->736k(125952k), 0.0012008 secs] [times: user=0.00 sys=0.00, real=0.00 secs] 
[full gc (system.gc()) [psyounggen: 728k->0k(38400k)] [paroldgen: 8k->667k(87552k)] 736k->667k(125952k), [metaspace: 3491k->3491k(1056768k)], 0.0044445 secs] [times: user=0.00 sys=0.00, real=0.00 secs] 
heap
 psyounggen      total 38400k, used 333k [0x00000000d5c00000, 0x00000000d8680000, 0x0000000100000000)
  eden space 33280k, 1% used [0x00000000d5c00000,0x00000000d5c534a8,0x00000000d7c80000)
  from space 5120k, 0% used [0x00000000d7c80000,0x00000000d7c80000,0x00000000d8180000)
  to   space 5120k, 0% used [0x00000000d8180000,0x00000000d8180000,0x00000000d8680000)
 paroldgen       total 87552k, used 667k [0x0000000081400000, 0x0000000086980000, 0x00000000d5c00000)
  object space 87552k, 0% used [0x0000000081400000,0x00000000814a6cf0,0x0000000086980000)
 metaspace       used 3497k, capacity 4498k, committed 4864k, reserved 1056768k
  class space    used 387k, capacity 390k, committed 512k, reserved 1048576k

从运行结果看,gc日志中包含“7432k->736k”,意味着虚拟机并没有因为两个对象互相引用就不回收它们,而说明虚拟机并不是通过引用计数算法来判断对象是否存活的。

可达性分析算法

在很多程序语言的主流实现中,都是通过可达性分析(reachability analysis)来判定对象是否存活的。这个算法的基本思想是:通过一系列的称为“gc roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(reference chain),当一个对象到gc roots没有任何引用链相连(用图论的话说就是从gc roots到这个对象不可达)时,则证明此对象是不可用的。

如下图所示,对象object 5、object 6、object 7虽然互相关联,但是它们到gc roots是不可达的,所以它们将被判定为可回收的对象:

JVM探秘:四种引用、对象的生存与死亡

在 java 中,可作为 gc roots 的对象有以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中 jni(即一般说的 native 方法)引用的对象。

对象的自我救赎

在可达性分析算法中不可达的对象也不是“必死无疑”的,这时它们会暂时处于“缓刑”阶段,要真正宣告死亡,至少要经历两次标记过程:第一次标记是当进行可达性分析后发现没有与gc roots相连的引用链,就标记一次;然后如果对象覆盖了finalize()方法并且还未执行过,对象就会被放入一个叫f-queue的队列中,会有一个单独的线程依次执行队列中对象的finalize()方法,finalize()方法是对象最后一次自我救赎的机会,只要跟gc roots引用链上的任意对象建立关联,就可逃脱死亡,f-queue的队列中的对象会被第二次标记。两次标记过后如果对象还没有逃脱,那基本上它就真的被回收了。

以下代码是对象一次自我救赎的演示:

/**
 * 对象的一次自我救赎
 * 1. 对象可以在gc时自我救赎
 * 2. 这种机会只有一次,因为一个对象的finalize()方法至多会被调用一次
 * */
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 method executed");
        //把自己赋值给类变量,即与gc roots建立了关联
        finalizeescapegc.save_hook = this;
    }

    public static void main(string[] args) throws throwable{
        save_hook = new finalizeescapegc();

        //对象第一次自我救赎
        save_hook = null;
        system.gc();
        thread.sleep(500);
        if(save_hook != null) {
            save_hook.isalive();
        } else {
            system.out.println("no, i am dead");
        }

        //第二次自我救赎失败,因为finalize()只执行一次
        save_hook = null;
        system.gc();
        thread.sleep(500);
        if(save_hook != null) {
            save_hook.isalive();
        } else {
            system.out.println("no, i am dead");
        }
    }
}

运行结果:

finalize method executed
yes, i am still alive
no, i am dead

从运行结果可知,save_hook对象的finalize()方法确实被垃圾收集器触发过,并且在被回收之前成功逃脱了。代码中两段相同的代码,第二次没有成功逃脱,是因为一个对象的finalize()方法只会被系统自动调用一次。另外,finalize()方法运行代价高昂,不确定性大,无法保证对象的调用顺序,所以不建议使用此方法,可以用try-finally替代。

方法区

方法区也存在垃圾收集,只不过这块内存区域的垃圾收集效率比较低。在jdk1.6及之前,方法区的垃圾收集主要回收两部分内容:废弃常量和无用的类。但在jdk1.7的时候运行时常量池挪到了java堆中,所以现在方法区主要是回收无用的类。运行时常量的回收跟堆内存中其他对象的回收方法基本一致。

同时满足以下三个条件,才会被判定为无用的类:

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  • 加载该类的classloader已经被回收。
  • 该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足以上3个条件的无用类进行回收,也仅仅是“可以”,并不是一定会回收。是否对类进行回收,hotspot虚拟机提供了-xnoclassgc参数进行控制。在大量使用反射、动态代理、cglib等bytecode框架、动态生成jsp以及osgi这类频繁自定义classloader的场景,都需要虚拟机具备类卸载的功能,以保证方法区不会溢出。

本文代码的 github repo 地址:https://github.com/cellei/jvm-practice