彻底理解JVM常考题之分级引用模型

转载自   彻底理解JVM常考题之分级引用模型

本文通过探析Java中的引用模型,分析比较强引用、软引用、弱引用、虚引用的概念及使用场景,知其然且知其所以然,希望给大家在实际开发实践、学习开源项目提供参考。

Java的引用

对于Java中的垃圾回收机制来说,对象是否被应该回收的取决于该对象是否被引用。因此,引用也是JVM进行内存管理的一个重要概念。Java中是JVM负责内存的分配和回收,这是它的优点(使用方便,程序不用再像使用C语言那样担心内存),但同时也是它的缺点(不够灵活)。由此,Java提供了引用分级模型,可以定义Java对象重要性和优先级,提高JVM内存回收的执行效率

关于引用的定义,在JDK1.2之前,如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称为这块内存代表着一个引用;JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种。

软引用对象和弱应用对象主要用于:当内存空间还足够,则能保存在内存之中;如果内存空间在垃圾收集之后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的使用场景。

而虚引用对象用于替代不靠谱的finalize方法,可以获取对象的回收事件,来做资源清理工作。

 

对象生命周期

无分级引用对象生命周期

前面提到,分层引用的模型是用于内存回收,没有分级引用对象下,一个对象从创建到回收的生命周期可以简单地用下图概括:对象被创建,被使用,有资格被收集,最终被收集,阴影区域表示对象“强可达”时间:

有分级引用对象生命周期

JDK1.2引入java.lang.ref程序包之后,对象的生命周期多了3个阶段,软可达,弱可达,虚可达,这些状态仅适用于符合垃圾回收条件的对象,这些对象处于非强引用阶段,而且需要基于java.lang.ref包中的相关的引用对象类来指示标明。

  • 软可达
    软可达对象用SoftReference来指示标明,并没有强引用,垃圾回收器会尽可能长时间地保留对象,但是会在抛出OutOfMemoryError异常之前收集它。

  • 弱可达
    弱可达对象用WeakReference来指示标明,并没有强引用或软引用,垃圾回收器会随时回收对象,并不会尝试保留它,但是会在抛出OutOfMemoryError异常之前收集它。
    在对象回收阶段中,该对象在major collection期间被回收,但是可以在minor collection期间存活

  • 虚可达 
    虚可达对象用PhantomReference来指示标明,它已经被标记选中进行垃圾回收并且它的finalizer(如果有)已经运行。在这种情况下,术语“可达”实际上是用词不当,因为您无法访问实际对象。

对象生命周期图中添加三个新的可选状态会造成一些困惑。逻辑顺序上是从强可达到软,弱和虚,最终到回收,但实际的情况取决于程序创建的参考对象。但如果创建WeakReference但不创建SoftReference,则对象直接从强可达到弱到达最终到收集。

 

强引用

强引用就是指在程序代码之中普遍存在的,比如下面这段代码中的obj和str都是强引用:

Object obj = new Object();
String str = "hello world";

只要强引用还存在,垃圾收集器永远不会回收被引用的对象,即使在内存不足的情况下,JVM即使抛出OutOfMemoryError异常也不会回收这种对象。

实际使用上,可以通过把引用显示赋值为null来中断对象与强引用之前的关联,如果没有任何引用执行对象,垃圾收集器将在合适的时间回收对象。

例如ArrayList类的remove方法中就是通过将引用赋值为null来实现清理工作的:

    /*** Removes the element at the specified position in this list.* Shifts any subsequent elements to the left (subtracts one from their* indices).** @param index the index of the element to be removed* @return the element that was removed from the list* @throws IndexOutOfBoundsException {@inheritDoc}*/public E remove(int index) {rangeCheck(index);modCount++;E oldValue = elementData(index);int numMoved = size - index - 1;if (numMoved > 0)System.arraycopy(elementData, index+1, elementData, index,numMoved);elementData[--size] = null; // clear to let GC do its workreturn oldValue;}

 

引用对象

介绍软引用、弱引用和虚引用之前,有必要介绍一下引用对象,

引用对象是程序代码和其他对象之间的间接层,称为引用对象。每个引用对象都围绕对象的引用构造,并且不能更改引用值。

引用对象提供get()来获得其引用值的一个强引用,垃圾收集器可能随时回收引用值所指的对象。
一旦对象被回收,get()方法将返回null,要正确使用引用对象,下面使用SoftReference(软引用对象)作为参考示例:

    /*** 简单使用demo*/private static void simpleUseDemo(){List<String> myList = new ArrayList<>();SoftReference<List<String>> refObj = new SoftReference<>(myList);List<String> list = refObj.get();if (null != list) {list.add("hello");} else {// 整个列表已经被垃圾回收了,做其他处理}}

也就是说,使用时:

  • 1、必须经常检查引用值是否为null
    垃圾收集器可能随时回收引用对象,如果轻率地使用引用值,迟早会得到一个NullPointerException。

  • 2、必须使用强引用来指向引用对象返回的值
    垃圾收集器可能在任何时间回收引用对象,即使在一个表达式中间。

    /*** 正确使用引用对象demo*/private static void trueUseRefObjDemo(){List<String> myList = new ArrayList<>();SoftReference<List<String>> refObj = new SoftReference<>(myList);// 正确的使用,使用强引用指向对象保证获得对象之后不会被回收List<String> list = refObj.get();if (null != list) {list.add("hello");} else {// 整个列表已经被垃圾回收了,做其他处理}}/*** 错误使用引用对象demo*/private static void falseUseRefObjDemo(){List<String> myList = new ArrayList<>();SoftReference<List<String>> refObj = new SoftReference<>(myList);// XXX 错误的使用,在检查对象非空到使用对象期间,对象可能已经被回收// 可能出现空指针异常if (null != refObj.get()) {refObj.get().add("hello");}}
  • 3、必须持有引用对象的强引用
    如果创建引用对象,没有持有对象的强引用,那么引用对象本身将被垃圾收集器回收。

  • 4、当引用值没有被其他强引用指向时,软引用、弱引用和虚引用才会发挥作用,引用对象的存在就是为了方便追踪并高效垃圾回收。

 

软引用、弱引用和虚引用

引用对象的3个重要实现类位于java.lang.ref包下,分别是软引用SoftReference、弱引用WeakReference和虚引用PhantomReference。

软引用

软引用用来描述一些还有用但非必需的对象。对于软引用关联着的对象,在系统将要发生抛出OutOfMemoryError异常之前,将会把这些对象列入回收范围之内进行第二次回收。如果这次回收还没有足够的内存,才会抛出OutOfMemoryError异常。在JDK1.2之后,提供了SoftReference类来实现软引用。

下面是一个使用示例:

import java.lang.ref.SoftReference;public class SoftRefDemo {public static void main(String[] args) {SoftReference<String> sr = new SoftReference<>( new String("hello world "));// hello worldSystem.out.println(sr.get());}
}

JDK文档中提到:软引用适用于对内存敏感的缓存:每个缓存对象都是通过访问的 SoftReference,如果JVM决定需要内存空间,那么它将清除回收部分或全部软引用对应的对象。如果它不需要空间,则SoftReference指示对象保留在堆中,并且可以通过程序代码访问。在这种情况下,当它们被积极使用时,它们被强引用,否则会被软引用。如果清除了软引用,则需要刷新缓存。

实际使用上,要除非缓存的对象非常大,每个数量级为几千字节,才值得考虑使用软引用对象。例如:实现一个文件服务器,它需要定期检索相同的文件,或者需要缓存大型对象图。如果对象很小,必须清除很多对象才能产生影响,那么不建议使用,因为清除软引用对象会增加整个过程的开销。

弱引用

弱引用也是用来描述非必需对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发送之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象

在JDK1.2之后,提供了WeakReference类来实现弱引用。

    /*** 简单使用弱引用demo*/private static void simpleUseWeakRefDemo(){WeakReference<String> sr = new WeakReference<>(new String("hello world " ));// before gc -> hello world System.out.println("before gc -> " + sr.get());// 通知JVM的gc进行垃圾回收System.gc();// after gc -> nullSystem.out.println("after gc -> " + sr.get());}

可以看到被弱引用关联的对象,在gc之后被回收掉。
有意思的地方是,如果把上面代码中的:

WeakReference<String> sr = new WeakReference<>(new String("hello world "));

改为

WeakReference<String> sr = new WeakReference<>("hello world ");

程序将输出

before gc -> hello world 
after gc -> hello world 

这是因为使用Java的String直接赋值和使用new区别在于:

  • new 会在堆区创建一个可以被正常回收的对象。

  • String直接赋值,会在Java StringPool(字符串常量池)里创建一个String对象,存于pergmen(永生代区)中,通常不会被gc回收。

WeakHashMap
为了更方便使用弱引用,Java还提供了WeakHashMap,功能类似HashMap,内部实现是用弱引用对key进行包装,当某个key对象没有任何强引用指向,gc会自动回收key和value对象。

    /***  weakHashMap使用demo*/private static void weakHashMapDemo(){WeakHashMap<String,String> weakHashMap = new WeakHashMap<>();String key1 = new String("key1");String key2 = new String("key2");String key3 = new String("key3");weakHashMap.put(key1, "value1");weakHashMap.put(key2, "value2");weakHashMap.put(key3, "value3");// 使没有任何强引用指向key1key1 = null;System.out.println("before gc weakHashMap = " + weakHashMap + " , size=" + weakHashMap.size());// 通知JVM的gc进行垃圾回收System.gc();System.out.println("after gc weakHashMap = " + weakHashMap + " , size="+ weakHashMap.size());}

程序输出:

before: gc weakHashMap = {key1=value1, key2=value2, key3=value3} , size=3
after: gc weakHashMap = {key2=value2, key3=value3} , size=2

WeakHashMap比较适用于缓存的场景,例如Tomcat的缓存就用到。

引用队列

介绍虚引用之前,先介绍引用队列:
在使用引用对象时,通过判断get()方法返回的值是否为null来判断对象是否已经被回收,当这样做并不是非常高效,特别是当我们有很多引用对象,如果想找出哪些对象已经被回收,需要遍历所有所有对象。

更好的方案是使用引用队列,在构造引用对象时与队列关联,当gc(垃圾回收线程)准备回收一个对象时,如果发现它还仅有软引用(或弱引用,或虚引用)指向它,就会在回收该对象之前,把这个软引用(或弱引用,或虚引用)加入到与之关联的引用队列(ReferenceQueue)中。

如果一个软引用(或弱引用,或虚引用)对象本身在引用队列中,就说明该引用对象所指向的对象被回收了,所以要找出所有被回收的对象,只需要遍历引用队列。

当软引用(或弱引用,或虚引用)对象所指向的对象被回收了,那么这个引用对象本身就没有价值了,如果程序中存在大量的这类对象(注意,我们创建的软引用、弱引用、虚引用对象本身是个强引用,不会自动被gc回收),就会浪费内存。因此我们这就可以手动回收位于引用队列中的引用对象本身。

    /*** 引用队列demo*/private static void refQueueDemo() {ReferenceQueue<String> refQueue = new ReferenceQueue<>();// 用于检查引用队列中的引用值被回收Thread checkRefQueueThread = new Thread(() -> {while (true) {Reference<? extends String> clearRef = refQueue.poll();if (null != clearRef) {System.out.println("引用对象被回收, ref = " + clearRef + ", value = " + clearRef.get());}}});checkRefQueueThread.start();WeakReference<String> weakRef1 = new WeakReference<>(new String("value1"), refQueue);WeakReference<String> weakRef2 = new WeakReference<>(new String("value2"), refQueue);WeakReference<String> weakRef3 = new WeakReference<>(new String("value3"), refQueue);System.out.println("ref1 value = " + weakRef1.get() + ", ref2 value = " + weakRef2.get()+ ", ref3 value = " + weakRef3.get());System.out.println("开始通知JVM的gc进行垃圾回收");// 通知JVM的gc进行垃圾回收System.gc();}

程序输出:

ref1 value = value1, ref2 value = value2, ref3 value = value3
开始通知JVM的gc进行垃圾回收
引用对象被回收, ref = java.lang.ref.WeakReference@48c6cd96, value=null
引用对象被回收, ref = java.lang.ref.WeakReference@46013afe, value=null
引用对象被回收, ref = java.lang.ref.WeakReference@423ea6e6, value=null

虚引用

虚引用也称为幽灵引用或者幻影引用,不同于软引用和弱引用,虚引用不用于访问引用对象所指示的对象,相反,通过不断轮询虚引用对象关联的引用队列,可以得到对象回收事件。一个对象是否有虚引用的存在,完全不会对其生产时间构成影响,也无法通过虚引用来取得一个对象实例。虽然这看起来毫无意义,但它实际上可以用来做对象回收时资源清理、释放,它比finalize更灵活,我们可以基于虚引用做更安全可靠的对象关联的资源回收。

  • finalize的问题

  • Java语言规范并不保证finalize方法会被及时地执行、而且根本不会保证它们会被执行
    如果可用内存没有被耗尽,垃圾收集器不会运行,finalize方法也不会被执行。

  • 性能问题
    JVM通常在单独的低优先级线程中完成finalize的执行。

  • 对象再生问题
    finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的。

针对不靠谱finalize方法,完全可以使用虚引用来实现。在JDK1.2之后,提供了PhantomReference类来实现虚引用。

下面是简单的使用例子,通过访问引用队列可以得到对象的回收事件:

    /*** 简单使用虚引用demo* 虚引用在实现一个对象被回收之前必须做清理操作是很有用的,比finalize()方法更灵活*/private static void simpleUsePhantomRefDemo() throws InterruptedException {Object obj = new Object();ReferenceQueue<Object> refQueue = new ReferenceQueue<>();PhantomReference<Object> phantomRef = new PhantomReference<>(obj, refQueue);// nullSystem.out.println(phantomRef.get());// nullSystem.out.println(refQueue.poll());obj = null;// 通知JVM的gc进行垃圾回收System.gc();// null, 调用phantomRef.get()不管在什么情况下会一直返回nullSystem.out.println(phantomRef.get());// 当GC发现了虚引用,GC会将phantomRef插入进我们之前创建时传入的refQueue队列// 注意,此时phantomRef对象,并没有被GC回收,在我们显式地调用refQueue.poll返回phantomRef之后// 当GC第二次发现虚引用,而此时JVM将phantomRef插入到refQueue会插入失败,此时GC才会对phantomRef对象进行回收Thread.sleep(200);Reference<?> pollObj = refQueue.poll();// java.lang.ref.PhantomReference@1540e19dSystem.out.println(pollObj);if (null != pollObj) {// 进行资源回收的操作}}

比较常见的,可以基于虚引用实现JDBC连接池,锁的释放等场景。
以连接池为例,调用方正常情况下使用完连接,需要把连接释放回池中,但是不可避免有可能程序有bug,造成连接没有正常释放回池中。基于虚引用对Connection对象进行包装,并关联引用队列,就可以通过轮询引用队列检查哪些连接对象已经被GC回收,释放相关连接资源。具体实现已上传github的caison-blog-demo仓库。

 

总结

对比一下几种引用对象的不同:

引用类型GC回收时间常见用途生存时间
强引用永不对象的一般状态JVM停止运行时
软引用内存不足时对象缓存内存不足时终止
弱引用GC时对象缓存GC后终止


虚引用,配合引用队列使用,通过不断轮询引用队列获取对象回收事件。

虽然引用对象是一个非常有用的工具来管理你的内存消耗,但有时它们是不够的,或者是过度设计的 。例如,使用一个Map来缓存从数据库中读取的数据。虽然可以使用弱引用来作为缓存,但最终程序需要运行一定量的内存。如果不能给它足够实际足够的资源完成任何工作,那么错误恢复机制有多强大也没有用。

当遇到OutOfMemoryError错误,第一反应是要弄清楚它为什么会发生,也许真的是程序有bug,也许是可用内存设置的太低。

在开发过程中,应该制定程序具体的使用内存大小,而已要关注实际使用中用了多少内存。大多数应用程序在实际运行负载下,程序的内存占用会达到稳定状态,可以用此来作为参考来设置合理的堆大小。如果程序的内存使用量随着时间的推移而上升,很有可能是因为当对象不再使用时仍然拥有对对象的强引用。引用对象在这里可能会有所帮助,但更有可能是把它当做一个bug来进行修复。

文章所有涉及源码已经上传github,地址:https://github.com/caison/caison-blog-demo,可以点击查看原文获取。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/324417.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

越优秀的人越努力,越努力的人越幸运!

昨日晚间&#xff0c;学校值班&#xff0c;在楼道里面巡查几遍之后发现无异常情况&#xff0c;接着就去值班室中打开电脑忙自己的事。过了没一会儿&#xff0c;3班三学生去找我了&#xff0c;对我说&#xff0c;最近班里的学习情况不是太好&#xff0c;部分学生上课容易打盹&am…

SOA和微服务之间的区别

近几年&#xff0c;我们有很多文章对SOA和微服务之间的不同点和相似点进行了分析。有些人认为SOA有很多地方是值得微服务学习的&#xff0c;而有些人则认为区别对待微服务和SOA会更好。而Neal Ford认为&#xff0c;将单体迁移到面向服务的架构要比迁移到微服务来得容易。关于选…

考研生的努力程度是有多恐怖!

2021年全国研究生招生考试在12月26日开始&#xff0c;不过我没有参加。早就听闻考研生是多么多么的努力&#xff0c;今日一见&#xff0c;果然名不虚传&#xff0c;在这之前他们是有多努力我不知道&#xff0c;但是今天我所见到他们努力的程度&#xff0c;可以用恐怖来形容。偶…

全文索引 - Pomelo.EFCore.MySql

背景 全文索引&#xff1a;MySQL全文检索是利用查询关键字和查询列内容之间的相关度进行检索&#xff0c;可以利用全文索引来提高匹配的速度。Pomelo.EntityFrameworkCore.MySql&#xff1a;一款第三方MySQL的Entity Framework Core驱动&#xff0c;在GitHub中开源&#xff0c…

“老师,为什么我一上课就感到困,听课听的总是走神?”

“老师,为什么我一上课就感到困,听课听的总是走神?” “老师,为什么我你讲的每个题我都能听明白,但是让我自己做我就一点思路都没有呢?” “老师,我记下来语法了,但是还是不会用怎么办?” “老师,我……” 怎么办?怎么办?急死了! 今天老师就给你解答一下你的为什么…

高并发场景下的限流策略

转载自 高并发场景下的限流策略 在高并发的场景下&#xff0c;我们的优化和保护系统的方式通常有&#xff1a;多级缓存、资源隔离、熔断降级、限流等等。 今天我们来聊聊限流。 为什么需要限流 举个比较简单的例子&#xff0c;正常来说&#xff0c;一个员工A他每天能够处理…

Entity Framework Core 批处理语句

在Entity Framework Core (EF Core)有许多新的功能&#xff0c;最令人期待的功能之一就是批处理语句。那么批处理语句是什么呢&#xff1f;批处理语句意味着它不会为每个插入/更新/删除语句发送单独的请求&#xff0c;它将在数据库的单次请求中批量执行多个语句。在这篇文章中&…

mybatis和spring整合时这个报错,应该这样解决!

01问题描述今天在写mybatis和spring整合的时候&#xff0c;出现了个问题&#xff0c;其实也没有多难&#xff0c;就是自己没有仔细看&#xff0c;特此记录一下。报错问题如下&#xff1a;org.springframework.beans.factory.BeanCreationException: Error creating bean with n…

一个简单的例子,学习自定义注解和AOP

转载自 一个简单的例子&#xff0c;学习自定义注解和AOP 记得今年年初刚开始面试的时候&#xff0c;被问的最多的就是你知道Spring的两大核心嘛&#xff1f;那你说说什么是AOP&#xff0c;什么是IOC&#xff1f;我相信你可能也被问了很多次了。 1、到底是什么是AOP&#xff…

Xamarin for iOS 11(一) - 初尝ARKit

编者语&#xff1a;Xamarin 的兼容性是它最大的优点&#xff0c;对于iOS / Android 新的功能支持也是无缝的&#xff0c;做到100%兼容。Xamarin.ios for iOS 11.0的支持已经开始&#xff0c;大家可以在这里下载最新的Preview支持 (https://releases.xamarin.com/preview-xcode-…

挺不喜欢下雪的

你要储蓄你的可爱&#xff0c;眷顾你的善良&#xff0c;变得勇敢。当这个世界越来越坏时&#xff0c;只希望你能越来越好。——《南山南》昨天夜间&#xff0c;窗外雪花飘飘&#xff0c;朋友圈中到处都是晒下雪的照片&#xff0c;有的说是“这是2020年的第一场雪”&#xff0c;…

是什么使你留在你的公司

一个故事 之前离职的一个同事&#xff0c;是典型的技术型人才&#xff0c;前段时间跟他一起吃饭&#xff0c;得知他工作状况不是很理想&#xff08;他在创业型团队&#xff0c;工作强度非常大&#xff0c;且做出来的东西得不到应有的认可&#xff0c;公司给他的报酬也不是很高…

有些事,父母一定不能依着孩子!

今天的文章&#xff0c;可能有的人不太喜欢。在我教学这么些年以来&#xff0c;这个问题在今年出现的最多——父母总是站在孩子的角度&#xff0c;不管什么事都是依着孩子&#xff1b;如果此事对孩子好也就罢辽&#xff0c;但是明明对孩子没有一点好处&#xff0c;还要和孩子一…

“看起来你每天都要做好多事,你有时间吗?”

“我看你每天要干的事好多啊,你有那么多时间吗?” “有啊,早起会儿,晚睡会儿,时间就出来了” 我每天确实是要做好多事。 01 我是一个喜欢规律生活的人,通常会将自己的一天划分为好多块,每一块时间安排的事都不一样,不太喜欢别人打乱我的生活规律,也不知道这是好事还是…

Sentinel限流保护

自定义限流错误页

浅谈 EF CORE 迁移和实例化的几种方式

出于学习和测试的简单需要&#xff0c;使用 Console 来作为 EF CORE 的承载程序是最合适不过的。今天笔者就将平时的几种使用方式总结成文&#xff0c;以供参考&#xff0c;同时也是给本人一个温故知新的机会。因为没有一个完整的脉络&#xff0c;所以也只是想起什么写点什么&a…