JVM探秘:四种引用、对象的生存与死亡
本系列笔记主要基于《深入理解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探秘:四种引用、对象的生存与死亡的更多相关文章
- Java虚拟机15:再谈四种引用状态
JVM的四种引用状态 在Java虚拟机5:Java垃圾回收(GC)机制详解一文中,有简单提到过JVM的四种引用状态,当时只是简单学习,知道有这么一个概念,对四种引用状态理解不深.这两天重看虚拟机这部分 ...
- Java虚拟机19:再谈四种引用状态
JVM的四种引用状态 在Java虚拟机5:Java垃圾回收(GC)机制详解一文中,有简单提到过JVM的四种引用状态,当时只是简单学习,知道有这么一个概念,对四种引用状态理解不深.这两天重看虚拟机这部分 ...
- 深入理解JVM虚拟机13:再谈四种引用及GC实践
Java中的四种引用类型 一.背景 Java的内存回收不需要程序员负责,JVM会在必要时启动Java GC完成垃圾回收.Java以便我们控制对象的生存周期,提供给了我们四种引用方式,引用强度从强到弱分 ...
- Java中四种引用:强、软、弱、虚引用
这篇文章非常棒:http://alinazh.blog.51cto.com/5459270/1276173 Java中四种引用:强.软.弱.虚引用 1.1.强引用当我们使用new 这个关键字创建对象时 ...
- JAVA中的四种引用以及ReferenceQueue和WeakHashMap的使用示例
简介: 本文主要介绍JAVA中的四种引用: StrongReference(强引用).SoftReferenc(软引用).WeakReferenc(弱引用).PhantomReference(虚引用) ...
- Java基础:Java的四种引用
在Java基础:java虚拟机(JVM)中,我们提到了Java的四种引用.包括:强引用,软引用,弱引用,虚引用.这篇博客将详细的讲解一下这四种引用. 1. 强引用 2. 软引用 3. 弱引用 4. 虚 ...
- Java四种引用--《深入理解Java虚拟机》学习笔记及个人理解(四)
Java四种引用--<深入理解Java虚拟机>学习笔记及个人理解(四) 书上P65. StrongReference(强引用) 类似Object obj = new Object() 这类 ...
- Java四种引用
Java中提供了一个Reference抽象类,此类定义所有引用对象共有的操作,与垃圾收集器密切配合实现的.主要是为了决定某些对象的生命周期,有利于JVM进行垃圾回收.而继承此类的有四种引用,分别是St ...
- Java虚拟机(五)Java的四种引用级别
1.前言 HotSpot采取了可达性分析算法用来判断对象是否被能被GC,无论是引用计算法还是可达性分析算法都是判断对象是否存在引用来判断对象是否存活.如果reference类型的数据中存储的数值代表的 ...
- Java中的四种引用
引用定义 实际上,Java中存在四种引用,它们由强到弱依次是:强引用.软引用.弱引用.虚引用.下面我们简单介绍下这四种引用: 强引用(Strong Reference):通常我们通过new来创建一个新 ...
随机推荐
- Ext--Layout(布局)
EXT中的布局,常用的有border.column.fit.form.tabel这几种. Fit布局,子元素将自动填满整个父容器(对元素设置宽度无效),如果容器组件中有多个子元素,则只会显示第一个子元 ...
- C++ 第四次作业 继承
继承 继承时从新的类从已有类那里得到新的特征.继承实现了代码的重用,极大地减少了代码量,同时通过新增成员,加入了自身的独有特性,达到了程序的扩充. 派生类继承了基类的全部数据类和除了构造函数.析构函数 ...
- BERT大火却不懂Transformer?读这一篇就够了 原版 可视化机器学习 可视化神经网络 可视化深度学习
https://jalammar.github.io/illustrated-transformer/ The Illustrated Transformer Discussions: Hacker ...
- Python--day72--Django内置的serializers序列化介绍
序列化 Django内置的serializers def books_json(request): book_list = models.Book.objects.all()[0:10] from d ...
- Educational Codeforces Round 11、A B题
A. Co-prime Array 题意:给你一个数列,要求构造两两相邻之间的数互质的数列,可以插入的数的小于10的9次方 思路:其实可以选择靠近10的9次方的最大的三个素数.然后按我下面的方法做就可 ...
- Python--day41--守护线程
1,守护线程:守护线程会在主线程结束之后等待其他子线程的结束才结束 拓展--守护进程:守护进程随着主进程代码的执行结束而结束 代码示例:守护线程.py import time from threadi ...
- spring security自定义指南
序 本文主要研究一下几种自定义spring security的方式 主要方式 自定义UserDetailsService 自定义passwordEncoder 自定义filter 自定义Authent ...
- UVA 1025 "A Spy in the Metro " (DAG上的动态规划?? or 背包问题??)
传送门 参考资料: [1]:算法竞赛入门经典:第九章 DAG上的动态规划 题意: Algorithm城市的地铁有 n 个站台,编号为 1~n,共有 M1+M2 辆列车驶过: 其中 M1 辆列车从 1 ...
- linux内核符号表
我们已经看到 insmod 如何对应共用的内核符号来解决未定义的符号. 表中包含了全局内 核项的地址 -- 函数和变量 -- 需要来完成模块化的驱动. 当加载一个模块, 如何由模块 输出的符号成为内核 ...
- H3C DNS域名解析完整过程