Java对象引用的细化
前言
在 JVM 的垃圾回收策略中,无论是基于 “引用计数” 算法判断对象的引用数量,还是基于 “可达性分析” 算法判断对象是否引用链可达,在判断对象是否应该被回收时都离不开对象的 “引用”。
在 JDK1.2 之前,Java 中的对象只存在 “被引用” 和 “未被引用” 两种状态。
Java 中对引用的定义:如果 refence 类型的数据中存储的数值代表的是另一块内存的起始地址,就称该 refence 数据是代表某块内存、某个对象的引用。
但随着程序的空间复杂度越来越高,如何更有效地利用内存就成了大家思考的重点。为了使程序能更加灵活地控制对象的生命周期, Java 在 JDK1.2 后对 “引用” 的概念进行了更细粒度的划分,由高到低依次为:“强引用” 、"软引用" 、“弱引用” 、“虚引用” 、“未引用”。
测试环境
- JDK:
- java version "1.8.0_202"
- Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
- Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)
- OS:Windows 10
- IDE:
- IntelliJ IDEA 2021.1.3 (Ultimate Edition)
未引用
只创建了对象,但没有对该对象进行任何引用。
GC 情况
- 当 JVM 启动 GC(Garbage Collection,垃圾回收) 时,会直接将这个对象进行回收。
使用场景
- 几乎不会使用。
代码示例
// 此处的代码只是创建了一个 Object 类型的对象,
// 但是这个对象没有被引用。
new Object();
强引用
使用一个变量名存储了对象在内存中的地址(引用赋值),此时对象就处于强引用状态。
GC 情况
- 无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收被引用的对象。
- 当内存空间不足时,JVM 宁愿抛出
OutOfMemoryError
错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
使用场景
- 对于一些必要的数据对象,要用强引用。
代码示例
// 此处的代码在创建了一个Object类型的对象后,
// 将这个对象在内存中的地址赋值给了变量o,
// 如此该对象就处于强引用状态
Object o = new Object();
// 主动开启JVM的GC
System.gc();
// 注意:实际上 JVM 何时进行 GC 操作并不可控!
// 我们调用System.gc()方法只是起通知作用,JVM 什么时候扫描回收对象是 JVM 自己根据当前系统的状态决定的。
// 因此更严谨的做法是休眠等待一段时间再验证 GC 的情况
Thread.sleep(5000);
// 此时的变量o依然可以引用到之前的对象。
System.out.println(o);
手动回收被强引用的对象
- 如果在 Java 方法的内部有一个强引用,则这个引用保存在 Java 栈中(而真正的引用内容
Object
仍保存在 Java 堆中)。当这个方法执行完后,就会被弹出栈,若此时该强引用对象的引用数为 0,那么这个对象就会被回收。 - 为什么在一些代码中,会出现显式的对变量赋 null 值?
- 例如 ArrayList 的源码中,所有删除动作都会使用
elementData[i] = null
- 例如 ArrayList 的源码中,所有删除动作都会使用
// ArrayList 的 clear() 方法
/**
* Removes all of the elements from this list. The list will
* be empty after this call returns.
*/
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
- 其主要目的就是希望在强引用对象不使用时,能弱化其引用状态从而被 GC 及时回收。
但是也有资料认为这种显式的赋 null 值没有意义。在 JIT 编译下,JIT 编译器进行控制流和数据流分析后,生成的 OopMap 能提供比较精确的信息,不需要通过 null 值来告知对象使命已经完成。退一步说,这时即使有 "=null" 操作,也会被优化掉,生成出来的本地代码与没有 "=null" 操作的版本是一样的。
软引用
使用 SoftReference
类对某个对象进行 “描述包装” ,标志对象处于软引用状态。
GC 情况
- 在系统将要发生内存溢出异常前,会把这些对象列入回收范围内进行第二次回收。
- 如果这次回收仍没有足够的内存,才会抛出内存溢出异常。
- 当内存不足时,JVM 首先将软引用中的对象引用置为 null,然后通知垃圾回收器进行回收:(伪代码如下)
if( JVM 内存不足 ) {
// 将软引用中的对象引用置为 null
str = null;
// 通知垃圾回收器进行回收
System.gc();
}
使用场景
- 对于一些不是必要,但是有用的数据对象需要使用软引用。例如,网页缓存、图片缓存等...
- 还可用来实现内存敏感的高速缓存。
代码示例
// 此处代码先创建了一个Object类型的对象,
// 然后使用SoftReference对这个新建对象进行 “描述包装”,
// 如此这个新建对象就只处于软引用状态
SoftReference<Object> soft = new SoftReference<>(new Object());
// 处于软引用状态的对象,可以使用SoftReference对象的get方法获取
// 如果不想给对象添加其他引用状态,那么只可以使用SoftReference对象的get方法获取
// SoftReference对象的get方法的返回值 就好比 强引用状态中的变量名
// 强引用状态中的变量名如何使用,那么SoftReference对象的get方法的返回值就如何使用
System.out.println(soft.get());
// 主动开启JVM的GC
System.gc();
// 注意:实际上 JVM 何时进行 GC 操作并不可控!
// 我们调用System.gc()方法只是起通知作用,JVM 什么时候扫描回收对象是 JVM 自己根据当前系统的状态决定的。
// 因此更严谨的做法是休眠等待一段时间再验证 GC 的情况
Thread.sleep(5000);
// 因为此时系统内存依然充足,因此 JVM 并不会主动的去释放处于软引用的对象
System.out.println(soft.get());
弱引用
用来描述非必要的对象,且强度比软引用更弱一些。 使用 WeakReference
类对某个对象进行 “描述包装” ,标志对象处于弱引用状态。
GC 情况
- 当垃圾收集器开始工作,无论当前内存是否足够,都会回收只被弱引用关联的对象。
- 因此,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
使用场景
- 如果一个对象是偶尔(很少)使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你可以使用
WeakReference
来记住此对象。 - 但是使用弱引用也会带来一些额外的问题:
- 弱引用会使代码更加复杂,并且可能容易出错。任何使用弱引用的代码都需要处理每次使用弱引用时引用被破坏的可能性。如果过度使用弱引用,最终会编写大量额外代码。
- 使用弱引用会带来运行时开销。显而易见的代价是创建弱引用并调用 get 。一个不太明显的代价是每次 GC 运行时都需要做大量额外的工作。
- 如果对应用程序将来很可能需要的东西使用弱引用,则可能会导致重复创建它的成本。如果这一成本很高(在CPU时间、IO带宽、网络流量等方面)),可能会在一定程度上影响系统的吞吐量。此时,最好给 JVM 更多的内存,而不是使用弱引用。
- 更详细的内容可以浏览 Stackoverflow 中关于 When should weak references be used? 问题的讨论。
代码示例
// 此处代码先创建了一个Object类型的对象,
// 然后使用WeakReference对这个新建对象进行 “描述包装”,
// 如此这个新建对象就只处于弱引用状态
WeakReference<Object> weak = new WeakReference<>(new Object());
// 处于弱引用状态的对象,可以使用WeakReference对象的get方法获取
// 如果不想给对象添加其他引用状态,那么只可以使用WeakReference对象的get方法获取
// WeakReference对象的get方法的返回值 就好比 强引用状态中的变量名
// 强引用状态中的变量名如何使用,那么WeakReference对象的get方法的返回值就如何使用
System.out.println(weak.get());
// 主动开启JVM的GC
System.gc();
// 注意:实际上 JVM 何时进行 GC 操作并不可控!
// 我们调用System.gc()方法只是起通知作用,JVM 什么时候扫描回收对象是 JVM 自己根据当前系统的状态决定的。
// 因此更严谨的做法是休眠等待一段时间再验证 GC 的情况
Thread.sleep(5000);
// 输出为null
// 因为GC将只处于弱引用的对象给回收掉了,
// 所以WeakReference对象无法再引用到之前的 “描述包装” 的对象了
System.out.println(weak.get());
虚引用
使用 PhantomReference
类对某个对象进行 “描述包装” ,标志对象处于虚引用状态。 虚引用状态完全不会对对象的生存时间构成影响,也无法通过 PhantomReference
对象来取得一个对象实例。
别称:幽灵引用、幻影引用。
GC 情况
- 对其的处理与未引用的效果一样,当 JVM 启动 GC(Garbage Collection,垃圾回收) 时,会直接将这个对象进行回收。
使用场景
- 给一个对象设置虚引用状态的唯一目的就是在这个对象被垃圾回收器回收时希望能收到一个系统通知。
- 虚引用与软引用和弱引用的一个区别在于:
虚引用必须和引用队列
ReferenceQueue
联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
- 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
代码示例
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), new ReferenceQueue<Object>());
// 输出null
System.out.println(phantom.get());
引用队列
在创建 SoftReference
、WeakReference
、PhantomReference
对象时,如果使用两参数的构造函数,而构造函数的参数二是ReferenceQueue
对象时,那么当对象被 JVM 的 GC 回收时,JVM 会将被回收了对象的 SoftReference
、WeakReference
、PhantomReference
对象添加进 ReferenceQueue
对象,我们可以通过 ReferenceQueue
对象的 remove()
方法获取到添加进ReferenceQueue
对象的SoftReference
、WeakReference
、PhantomReference
对象。
总结
- Java 中 5 种引用的级别和强度由高到低依次为:强引用 -> 软引用 -> 弱引用 -> 虚引用 -> 未引用
- 图1 四种引用类型之间的区别
参考文献
- 周志明.深入理解Java虚拟机: JVM高级特性与最佳实践(3 版)[M]. 北京:机械工业出版社,2019:71-72.
- 与文章相关的疑问都可在评论区中留言,或者 笔者的个人博客 下进行评论。
冰,水为之,而寒于水。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
Redis 高可用篇:你管这叫 Sentinel 哨兵集群原理
概要 我们知道「主从复制是高可用的基石」,从库宕机依然可以将请求发送给主库或者其他从库,但是 Master 宕机,只能响应读操作,写请求无法再执行。 所以主从复制架构面临一个严峻问题,主库挂了,无法执行「写操作」,无法自动选择一个 Slave 切换为 Master,也就是无法故障自动切换。 深夜与女朋友么么哒……(此处省略 10000 字),突然宕机,总不能提起裤子从床上爬起来手工进行主从切换,再通知其他程序员把地址重新改成新主库上线。 如此一折腾自己已被女友切换成前男友了,万万使不得。所以我们必须有一个高可用的方案,为此,Redis 官方提供一个高可用方案——哨兵(Sentinel)。 开篇寄语 技术的迭代非常的快,但是从技术中沉淀下来的思维却是受益终生的。所以不要担心什么中年危机,那些担心中年危机的人通常很难成长起来。只要我们成长,只要我们的认知在不断突破,就不用担心中年危机,这个世界始终是需要那些优秀人才的。 什么是哨兵(Sentinel) 65 哥:码哥,虽然我没女朋友,但是,未雨绸缪我要掌握这个哨兵模式,防止当深夜与女朋友么么哒被打扰,你快说说哨兵的实现原理吧。 搭建实例采用...
-
下一篇
我与消息队列的八年情缘
本文作者:张勇,现任科大讯飞高级架构师。公众号:勇哥java实战分享。11年后端经验,曾就职于同程艺龙、神州优车等公司。乐于分享、热衷通过自己的实践经验平铺对技术的理解。 谈起消息队列,内心还是会有些波澜。 消息队列,缓存,分库分表是高并发解决方案三剑客,而消息队列是我最喜欢,也是思考最多的技术。 我想按照下面的四个阶段分享我与消息队列的故事,同时也是对我技术成长经历的回顾。 初识:ActiveMQ 进阶:Redis&RabbitMQ 升华:MetaQ 钟情:RocketMQ 1 初识ActiveMQ 1.1 异步&解耦 2011年初,我在一家互联网彩票公司做研发。 我负责的是用户中心系统,提供用户注册,查询,修改等基础功能。用户注册成功之后,需要给用户发送短信。 因为原来都是面向过程编程,我就把新增用户模块和发送短信模块都揉在一起了。 起初都还好,但问题慢慢的显现出来。 短信渠道不够稳定,发送短信会达到5秒左右,这样用户注册接口耗时很大,影响前端用户体验; 短信渠道接口发生变化,用户中心代码就必须修改了。但用户中心是核心系统。每次上线都必要谨小慎微。这种感觉很别扭,非...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Dcoker安装(在线仓库),最新的服务器搭配容器使用
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8编译安装MySQL8.0.19
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- MySQL数据库在高并发下的优化方案