JVM内存回收-对象引用关系
说起内存回收,一般需要考虑以下三个事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
前面介绍了JVM中的内存区域分布,其中程序计数器,虚拟机栈和本地方法栈是和线程的生命周期绑定的,线程销毁时,其对应的内存空间也会被回收.
但堆和方法区就存在着不确定性:
- 一个程序在运行期会加载多少个类是不确定的
- 一个线程在运行期间会创建多少个对象是不确定的
而内存回收,就是关注这部分内存该如何管理
引用计数法
判断一个对象是否存活,道理很简单:一个对象只要被至少一个对象引用.那么就是存活的. 因此便有了引用计数法.
引用计数法的原理很简单: 对于每个对象,新增一个字段存储其被引用从次数,当建立引用关系时,引用次数+1,当取消引用关联时,引用次数-1. 很显然,当对象的引用次数=0时,说明这个对象需要被回收了.
在大多数情况下,这个算法的效率很高,缺点就是引入了额外的物理空间,但是也存在一个无法解决的问题: 循环引用
循环引用
假如有两个对象,ObjA和ObjB,他们相互引用,同时外部引入ObjC引用ObjA,他们的关系如下
ObjC -> ObjA <=> ObjB
此时,ObjA和ObjB的引用计数分别为ref(ObjA)=2
和ref(ObjB)=1
. 当ObjC取消对ObjA的引用后,此时ref(ObjA)
和ref(ObjB)
均为1,此时没有任何方式可以访问到着两个对象,但是其引用计数都不为0,所以他们无法被回收掉
可达性分析算法
前面提及的引用计数法无法解决循环依赖的问题,本质原因在于,无法确定当前对象能否被用户线程所看到,引用计数法只关注了引用的次数,却忽视了线程是否可达.
目前Java是通过可达性分析算法来判断对象是否存活: 通过一系列被称为GC Roots
的对象作为起始节点,然后从这些节点开始,根据引用关系向下搜索,整个搜索过程所走过的路径被称为”引用链”.如果一个对象没有与任何引用链相连,说明这个对象不可能再次被使用
如上图所示,object 5
,object 6
和object 7
无法从GC Roots
访问到,属于可回收对象
在Java体系中,固定可作为GC Root
的对象包含以下几种
- 在虚拟机栈中引用的对象
- 类静态属性引用的对象
- 方法区常量引用的对象
- 本地方法栈中引用的对象
- Java虚拟机内部引用,比如基本属性类型对于的Class,常驻异常,类加载器
- 所有被同步锁(
synchronized
关键字)持有的对象 - JMX相关Bean
引用强弱
无论是引用计数法还是可达性分析算法,判断对象的存活都与”引用”离不开关系.
在JDK1.2之前,Java的引用是很传统的定义: 如果reference类型的数据中存储的是另一块内存的起始地址,就称该reference数据是代表某块内存,或者说是某个对象的引用
在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Reference), 软引用(Soft Reference), 弱引用(Weak Reference)和虚引用(Phantom Reference)
- 强引用是最传统的引用的定义,这种关系下,只要引用关系还存在,就不会被回收掉
- 软应用用来描述一些还有用,但是非必要的对象. 被软引用关联的对象,在系统发生内存溢出之前,会把这些对象加入回收范围. JDK中使用
SoftReference
来实现软引用 - 弱引用用来描述那些非必须对象,被弱引用的对象,在下次内存回收触发之时一定会被回收. JDK使用
WeakReference
来实现弱引用 - 虚引用是最弱的应用. 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取某个对象实例. 为对象设置一个虚引用仅仅只是为了能在被回收时收到系统通知. JDK使用
PhantomReference
来实现虚引用