本系列笔记主要基于《深入理解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是不可达的,所以它们将被判定为可回收的对象:

在 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

JVM探秘:四种引用、对象的生存与死亡的更多相关文章

  1. Java虚拟机15:再谈四种引用状态

    JVM的四种引用状态 在Java虚拟机5:Java垃圾回收(GC)机制详解一文中,有简单提到过JVM的四种引用状态,当时只是简单学习,知道有这么一个概念,对四种引用状态理解不深.这两天重看虚拟机这部分 ...

  2. Java虚拟机19:再谈四种引用状态

    JVM的四种引用状态 在Java虚拟机5:Java垃圾回收(GC)机制详解一文中,有简单提到过JVM的四种引用状态,当时只是简单学习,知道有这么一个概念,对四种引用状态理解不深.这两天重看虚拟机这部分 ...

  3. 深入理解JVM虚拟机13:再谈四种引用及GC实践

    Java中的四种引用类型 一.背景 Java的内存回收不需要程序员负责,JVM会在必要时启动Java GC完成垃圾回收.Java以便我们控制对象的生存周期,提供给了我们四种引用方式,引用强度从强到弱分 ...

  4. Java中四种引用:强、软、弱、虚引用

    这篇文章非常棒:http://alinazh.blog.51cto.com/5459270/1276173 Java中四种引用:强.软.弱.虚引用 1.1.强引用当我们使用new 这个关键字创建对象时 ...

  5. JAVA中的四种引用以及ReferenceQueue和WeakHashMap的使用示例

    简介: 本文主要介绍JAVA中的四种引用: StrongReference(强引用).SoftReferenc(软引用).WeakReferenc(弱引用).PhantomReference(虚引用) ...

  6. Java基础:Java的四种引用

    在Java基础:java虚拟机(JVM)中,我们提到了Java的四种引用.包括:强引用,软引用,弱引用,虚引用.这篇博客将详细的讲解一下这四种引用. 1. 强引用 2. 软引用 3. 弱引用 4. 虚 ...

  7. Java四种引用--《深入理解Java虚拟机》学习笔记及个人理解(四)

    Java四种引用--<深入理解Java虚拟机>学习笔记及个人理解(四) 书上P65. StrongReference(强引用) 类似Object obj = new Object() 这类 ...

  8. Java四种引用

    Java中提供了一个Reference抽象类,此类定义所有引用对象共有的操作,与垃圾收集器密切配合实现的.主要是为了决定某些对象的生命周期,有利于JVM进行垃圾回收.而继承此类的有四种引用,分别是St ...

  9. Java虚拟机(五)Java的四种引用级别

    1.前言 HotSpot采取了可达性分析算法用来判断对象是否被能被GC,无论是引用计算法还是可达性分析算法都是判断对象是否存在引用来判断对象是否存活.如果reference类型的数据中存储的数值代表的 ...

  10. Java中的四种引用

    引用定义 实际上,Java中存在四种引用,它们由强到弱依次是:强引用.软引用.弱引用.虚引用.下面我们简单介绍下这四种引用: 强引用(Strong Reference):通常我们通过new来创建一个新 ...

随机推荐

  1. PMC亮相IDF展示12G SAS分层存储解决方式

    引领大数据连接.传送以及存储,提供创新半导体及软件解决方式的PMC公司(纳斯达克代码:PMCS)出席了2014年4月2-3日在深圳举办的2014 IDF英特尔开发人员论坛. 此次,PMC将在 1层展示 ...

  2. CSS3表达式calc( )

    CSS3表达式calc( ) 第一次看到calc( )时,不太相信calc()是css中的部分.因为看其外表像个函数,但是CSS里为啥会有表达式我也不太清楚,偶然机会在网页里看到的,自己切片写自适应时 ...

  3. H3C 路由器单播IP包转发

  4. 【转载】Windows平台下利用APM来做负载均衡方案 - 负载均衡(下)

    概述 我们在上一篇Windows平台分布式架构实践 - 负载均衡中讨论了Windows平台下通过NLB(Network Load Balancer) 来实现网站的负载均衡,并且通过压力测试演示了它的效 ...

  5. Python--day69--pythonDjango终端打印SQL语句、在Python脚本中调用Django环境

    Django终端打印SQL语句 在Django项目的settings.py文件中,在最后复制粘贴如下代码: LOGGING = { 'version': 1, 'disable_existing_lo ...

  6. 2019-8-31-jekyll-在博客添加流程图

    title author date CreateTime categories jekyll 在博客添加流程图 lindexi 2019-08-31 16:55:59 +0800 2018-2-13 ...

  7. Python--day46--上节内容回顾及补充

    1,union(把两张表连起来,以上下的方式):具有自动去重的功能,有相同的就去掉. 结果: 2,union all就没有去重的功能 3,临时表,指定映射,条件,三元运算

  8. VS2017 OpenCV3.4.2 通过Release的版本 源码编译成 x86

    官方release的OpenCV3..2版本只有64bit,由于项目需要,现在把它重新编译成x86的库. 下载源码: github官方仓库 https://github.com/opencv/open ...

  9. 用Xshell连接谷歌云

    谷歌云服务器,默认用浏览器进行SSH链接,而且也不告知密码.以Centos为例,先使用浏览器连接 1,给root修改密码 1 sudo passwd root 2,编辑ssh配置文件 sudo nan ...

  10. JS 逻辑运算符 ||、 &&, 位运算符 |、&

    1.JS中的||符号: 运算方法: 只要“||”前面为false,不管“||”后面是true还是false,都返回“||”后面的值. 只要“||”前面为true,不管“||”后面是true还是fals ...