我们说的不同的引用类型其实都是逻辑上的,而对于虚拟机来说,主要体现的是对象的不同的可达性(reachable) 状态和对垃圾收集(garbage collector)的影响。

初识引用

对于刚接触 Java 的 C++ 程序员而言,理解栈和堆的关系可能很不习惯。在 C++ 中,可以使用 new 操作符在堆上创建对象,或者使用自动分配在栈上创建对象。下面的 C++ 语句是合法的,但是 Java 编译器却拒绝这么写代码,会出现 syntax error 编译错误。

Integer foo = Integer(1);

Java 和 C 不一样,Java 中会把对象都放在堆上,需要 new 操作符来创建对象。本地变量存储在中,它们持有一个指向堆中对象的引用(指针)。下面是一个 Java 方法,该方法具有一个 Integer 变量,该变量从 String 解析值

public static void foo(String bar){Integer baz = new Integer(bar);
}

这段代码我们使用堆栈分配图可以看一下它们的关系

首先先来看一下 foo() 方法,这一行代码分配了一个新的 Integer 对象,JVM 尝试在堆空间中开辟一块内存空间。如果允许分配的话,就会调用 Integer 的构造方法把 String 字符串转换为 Integer 对象。JVM 将指向该对象的指针存储在变量 baz 中。

上面这种情况是我们乐意看到的情况,毕竟我们不想在编写代码的时候遇到阻碍,但是这种情况是不可能出现的,当堆空间无法为 bar 和 baz 开辟内存空间时,就会出现 OutOfMemoryError,然后就会调用垃圾收集器(garbage collector) 来尝试腾出内存空间。这中间涉及到一个问题,垃圾收集器会回收哪些对象?

垃圾收集器

Java 给你提供了一个 new 操作符来为堆中的对象开辟内存空间,但它没有提供 delete 操作符来释放对象空间。当 foo() 方法返回时,如果变量 baz 超过最大内存,但它所指向的对象仍然还在堆中。如果没有垃圾回收器的话,那么程序就会抛出 OutOfMemoryError 错误。然而 Java 不会,它会提供垃圾收集器来释放不再引用的对象。

当程序尝试创建新对象并且堆中没有足够的空间时,垃圾收集器就开始工作。当收集器访问堆时,请求线程被挂起,试图查找程序不再主动使用的对象,并回收它们的空间。如果垃圾收集器无法释放足够的内存空间,并且JVM 无法扩展堆,则会出现 OutOfMemoryError,你的应用程序通常在这之后崩溃。还有一种情况是 StackOverflowError ,它出现的原因是因为线程请求的栈深度要大于虚拟机所允许的深度时出现的错误。

标记 - 清除算法

Java 能永久不衰的一个原因就是因为垃圾收集器。许多人认为 JVM 会为每个对象保留一个引用计数,当每次引用对象的时候,引用计数器的值就 + 1,当引用失效的时候,引用计数器的值就 - 1。而垃圾收集器只会回收引用计数器的值为 0 的情况。这其实是 引用计数法(Reference Counting) 的收集方式。但是这种方式无法解决对象之间相互引用的问题,如下

class A{public B b;}
class B{public A a;
}
public class Main{public static void main(String[] args){A a = new A();B b = new B();a.b=b;b.a=a;}
}

然而实际上,JVM 使用一种叫做 标记-清除(Mark-Sweep)的算法,标记清除垃圾回收背后的想法很简单:程序无法到达的每个对象都是垃圾,可以进行回收。

标记-清除收集具有如下几个阶段

  • 阶段一:标记

垃圾收集器会从 根(root) 引用开始,标记它到达的所有对象。如果用老师给学生判断卷子来比喻,这就相当于是给试卷上的全部答案判断正确还是错误的过程。

  • 阶段二:清理

在第一阶段中所有可回收的的内容都能够被垃圾收集器进行回收。如果一个对象被判定为是可以回收的对象,那么这个对象就被放在一个 finalization queue(回收队列)中,并在稍后会由一个虚拟机自动建立的、低优先级的 finalizer 线程去执行它。

  • 阶段三:整理(可选)

一些收集器有第三个步骤,整理。在这个步骤中,GC 将对象移动到垃圾收集器回收完对象后所留下的自由空间中。这么做可以防止堆碎片化,防止大对象在堆中由于堆空间的不连续性而无法分配的情况。

所以上面的过程中就涉及到一个根节点(GC Roots) 来判断是否存在需要回收的对象。这个算法的基本思想就是通过一系列的 GC Roots 作为起始点,从这些节点向下搜索,搜索所走过的路径称为 引用链(Reference Chain),当一个对象到 GC Roots 之间没有任何引用链相连的话,则证明此对象不可用。引用链上的任何一个能够被访问的对象都是强引用 对象,垃圾收集器不会回收强引用对象。

因此,返回到 foo() 方法中,仅在执行方法时,参数 bar 和局部变量 baz 才是强引用。一旦方法执行完成,它们都超过了作用域的时候,它们引用的对象都会进行垃圾回收。

下面来考虑一个例子

LinkedList foo = new LinkedList();
foo.add(new Integer(111));

变量 foo 是一个强引用,它指向一个 LinkedList 对象。LinkedList(JDK.18) 是一个链表的数据结构,每一个元素都会指向前驱元素,每个元素都有其后继元素。

当我们调用add() 方法时,我们会增加一个新的链表元素,并且该链表元素指向值为 111 的 Integer 实例。这是一连串的强引用,也就是说,这个 Integer 的实例不符合垃圾收集条件。一旦 foo 对象超出了程序运行的作用域,LinkedList 和其中的引用内容都可以进行收集,收集的前提是没有强引用关系。

Finalizers

C++ 允许对象定义析构函数方法:当对象超出作用范围或被明确删除时,会调用析构函数来清理使用的资源。对于大多数对象来说,析构函数能够释放使用 new 或者 malloc 函数分配的内存。在Java中,垃圾收集器会为你自动清除对象,分配内存,因此不需要显式析构函数即可执行此操作。这也是 Java 和 C++ 的一大区别。

然而,内存并不是唯一需要被释放的资源。考虑 FileOutputStream:当你创建此对象的实例时,它从操作系统分配文件句柄。如果你让流的引用在关闭前超过了其作用范围,该文件句柄会怎么样?实际上,每个流都会有一个 finalizer 方法,这个方法是垃圾回收器在回收之前由 JVM 调用的方法。对于 FileOutputStream 来说,finalizer 方法会关闭流,释放文件句柄给操作系统,然后清除缓冲区,确保数据能够写入磁盘

任何对象都具有 finalizer 方法,你要做的就是声明 finalize() 方法。如下

protected void finalize() throws Throwable
{// 清除对象
}

虽然 finalizers 的 finalize() 方法是一种好的清除方式,但是这种方法产生的负面影响非常大,你不应该依靠这个方法来做任何垃圾回收工作。因为 finalize 方法的运行开销比较大,不确定性强,无法保证各个对象的调用顺序。finalize 能做的任何事情,可以使用 try-finally 或者其他方式来做,甚至做的更好。

对象的生命周期

综上所述,可以通过下面的流程来对对象的生命周期做一个总结

对象被创建并初始化,对象在运行时被使用,然后离开对象的作用域,对象会变成不可达并会被垃圾收集器回收。图中用红色标明的区域表示对象处于强可达阶段。

JDK1.2 介绍了 java.lang.ref 包,对象的生命周期有四个阶段:????强可达????(Strongly Reachable????)软可达(Soft Reachable????)弱可达(Weak Reachable????)、 幻象可达(Phantom Reachable????)

如果只讨论符合垃圾回收条件的对象,那么只有三种:软可达、弱可达和幻象可达。

  • 软可达:软可达就是????我们只能通过软引用????才能访问的状态,软可达的对象是由 SoftReference 引用的对象,并且没有强引用的对象。软引用是用来描述一些还有用但是非必须的对象。垃圾收集器会尽可能长时间的保留软引用的对象,但是会在发生 OutOfMemoryError 之前,回收软引用的对象。如果回收完软引用的对象,内存还是不够分配的话,就会直接抛出 OutOfMemoryError。

  • 弱可达:弱可达的对象是 WeakReference 引用的对象。垃圾收集器可以随时收集弱引用的对象,不会尝试保留软引用的对象。

  • 幻象可达:幻象可达是由 PhantomReference 引用的对象,幻象可达就是没有强、软、弱引用进行关联,并且已经被 finalize 过了,只有幻象引用指向这个对象的时候。

除此之外,还有强可达和不可达的两种可达性判断条件

  • 强可达:就是一个对象刚被创建、初始化、使用中的对象都是处于强可达的状态

  • 不可达(unreachable):处于不可达的对象就意味着对象可以被清除了。

下面是一个不同可达性状态的转换图

判断可达性条件,也是 JVM 垃圾收集器决定如何处理对象的一部分考虑因素。

所有的对象可达性引用都是 java.lang.ref.Reference 的子类,它里面有一个get() 方法,返回引用对象。如果已通过程序或垃圾收集器清除了此引用对象,则此方法返回 null 。也就是说,除了幻象引用外,软引用和弱引用都是可以得到对象的。而且这些对象可以人为拯救,变为强引用,例如把 this 关键字赋值给对象,只要重新和引用链上的任意一个对象建立关联即可。

ReferenceQueue

引用队列又称为 ReferenceQueue,它位于 java.lang.ref 包下。我们在????建各种引用(软引用,弱引用,幻象引用)并关联到响应对象????时,可以选择是否需要关联引用队列。JVM 会在特定的时机将引用入队到队列中,程序可以通过判断引用队列中是否已经加入引用,来了解被引用的对象是否被GC回收。

Reference

java.lang.ref.Reference 为软(soft)引用、弱(weak)引用、虚(phantom)引用的父类。因为 Reference 对象和垃圾回收密切配合实现,该类可能不能被直接子类化。

文章参考:

https://www.jianshu.com/p/f86d3a43eec5

《深入理解Java虚拟机》第二版

http://www.kdgregory.com/index.php?page=java.refobj

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

小心点,别被当成垃圾回收了。相关推荐

  1. js垃圾回收机制,内存泄露和内存溢出,解决闭包产生的内存泄露详解

    一.内存的周期和回收机制 分配内存----->使用内存----->释放内存 1.JS 环境中分配的内存有如下声明周期: 内存分配:当我们声明变量.函数.对象的时候,系统会自动为他们分配内存 ...

  2. JavaScript垃圾回收机制理解

    JS的内存的生命周期: 1- JS运行程序 分配你所需要的暂时使用内存大小. 2- JS运行程序 在每次创建字符串.对象的时候,程序都会分配新内存来存储那个实体. 3- JS运行程序 对于不需要使用的 ...

  3. jvm对象从新生代到老年代_JVM内存管理、JVM垃圾回收机制、新生代、老年代以及永久代...

    内存模型 JVM运行时数据区由程序计数器.堆.虚拟机栈.本地方法栈.方法区部分组成,结构图如下所示. JVM内存结构由程序计数器.堆.栈.本地方法栈.方法区等部分组成,结构图如下所示: 1)程序计数器 ...

  4. 11 JVM 垃圾回收(上)

    11 JVM 垃圾回收(上) 引用计数法和可达性分析 垃圾回收,就是将已经分配出去的,但却不在使用的内存回收回来,以便再次分配.在 Java 虚拟机语境下,垃圾指的是死亡的对象所占据的堆空间.下面就总 ...

  5. 垃圾回收器之 G1 垃圾回收器

    4.4 G1 定义:Garbage First 2004论文发布 2009 JDK 6u14 体验 2012 JDK 7u4 官方支持 2019 JDK9 默认 (废弃了之前的 CMS 垃圾回收器) ...

  6. JVM 上篇之内存与垃圾回收(个人笔记,勿看)

    内存与垃圾回收篇 字节码与类的加载篇 性能监控与调优篇 大厂面试篇 文章目录 JVM 跨语言的平台 虚拟机与Java虚拟机 虚拟机 Java 虚拟机 Java 代码的执行流程 JVM的架构模型 JVM ...

  7. 看完后你会明白什么是垃圾回收机制

    内存的生命周期: 分配你所需要的内存: 由于字符串.对象等没有固定的大小,js程序在每次创建字符串.对象的时候,程序都会分配内存来存储那个实体. 使用分配到的内存做点什么. 不需要时将其释放回归: 在 ...

  8. JVM:垃圾回收相关算法

    文章目录 标记阶段:引用计数算法 垃圾标记阶段:对象存活判断 引用计数算法 小结 标记阶段:可达性分析算法 可达性分析(或根搜索算法.追踪性垃圾收集) GC Roots 对象的finalization ...

  9. 深度揭秘垃圾回收底层,这次让你彻底弄懂她

    Java 与 C++ 之间有一堵由内存动态分配和垃圾收集技术所围成的高墙 ---<深入理解Java虚拟机> 我们知道手动管理内存意味着自由.精细化地掌控,但是却极度依赖于开发人员的水平和细 ...

最新文章

  1. 解决xcode升级之后安装的插件失效
  2. 从超链接调用ActionScript
  3. 一张图了解华为手机背后的隐性福利,这些你不能再错过了!
  4. 认证服务号可通过模板消息向用户发送重要的服务通知
  5. mysql 获取结果_【原创】7. MYSQL++中的查询结果获取(各种Result类型)
  6. png、jpg、gif那些事情
  7. 虚拟机环境下Centos6.5如何上网
  8. RHEL 5基础篇—Linux常用命令参考手册
  9. dedecms channel php,DedeCMS在{dede:channel}标签前加序列号
  10. 批量doc转docx方法,使用软件、插件
  11. JPGPNG图片压缩java实现
  12. 深入浅出学大数据(一)大数据发展历程及大数据的简单介绍
  13. matlab imrotate中心,MATLAB imrotate函数的用法
  14. 协方差、相关系数(Pearson 相关系数)
  15. OppoR17被查找手机锁死问题解锁
  16. 论文研读 —— 7. Very Deep Convolutional Networks for Large-Scale Image Recognition (3/3)
  17. 阿里云虚拟机 php $_SESSION 失效问题
  18. Linux 中实用但很小众的 11 个炫酷终端命令
  19. 百度网盘真实地址解析(告别下载百度网盘)
  20. java实现:啤酒2元一瓶,10个盖子可以换一瓶啤酒,4个瓶子可以换一瓶啤酒,请问x元最多可换多少瓶啤酒

热门文章

  1. flutter usb串口_Flutter 调试方式
  2. gateway 车辆网关
  3. python读取大文件的坑_Python读取大文件的坑“与内存占用检测
  4. c语言中 快速输出字符数组后几位方法
  5. CentOS搭建安装SVN
  6. C#几个经常用到的字符串的截取
  7. 【React Native开发】React Native控件之DrawerLayoutAndroid抽屉导航切换组件解说(13)
  8. Linux多网口绑定配合华为5700 eth-trunk技术,提高网络性能
  9. 验证登陆信息的合法性
  10. 创建DataTable并把列默认值