很多事情不是看到希望才去坚持,而是坚持了才看得到希望。
正是因为有垃圾回收机制,才可以让程序员不用考虑程序执行之后对象内存的释放问题,将其交给JVM管理即可。
垃圾回收——标记算法
怎样的对象会被判定为垃圾?
- 没有被其他对象引用
此时这个对象占据的内存会被释放,此对象也会被销毁。
用什么方法判定对象不被引用了呢?
- 引用计数算法
- 可达性分析算法
引用计数算法
通过判断对象的引用数量来决定对象是否可以被回收。
具体执行方法:
- 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
- 任何引用计数为0的对象实例可以被当做垃圾收集
引用计数算法的优劣:
- 优点:执行效率高,程序执行受影响较小。因为我们只需要过滤掉引用计数为0的对象,然后将其回收即可,可以交织在程序运行中。由于垃圾回收的过程中可以做到几乎不打断程序的执行,所以这种方法适用于程序需要不被长时间打断的实时环境。
- 缺点:无法检测出循环引用的情况,导致内存泄漏。这个缺点是很致命的,如果存在父对象与子对象互相引用的情况,那么它们的引用计数永远不可能为零,那么永远都不会被检测到为0,永远不会被释放。
由于这种比较致命的缺点,主流JDK没有使用引用计数算法进行垃圾判定,而是用了下面的可达性分析算法。
可达性分析算法
通过判断对象的引用链是否可达来决定对象是否可以被回收。
这种方法从图论中引入。程序把所有的引用关系看作是一张图,通过一系列的名为GC Root的对象作为起始点,从这些节点开始向下搜索,搜索经过的路径会被称为”引用链”,即”reference chain”,当某个对象到其他图中的节点都不能相连的时候,也就是说从这个对象到其他部分的GC Root是不可达的,那么就判定这个对象为垃圾。
如图:
蓝色为存活对象,即可达对象,灰色的部分不可达了,为垃圾对象。
什么对象可以作为GC Root的对象呢?
- 虚拟机栈中引用的对象(栈帧中的本地变量表)。比如在方法中new了一个Object,并赋值给了一个局部变量,那么在该局部变量没有被销毁之前,new出来的对象就会是GC Root。
- 方法区中的常量引用的对象。比如在类中定义了一个常量,而该常量保存的是某个对象的地址,那么被保存的对象也会成为GC的根对象。
- 方法区中的类静态属性引用的对象。这个和上面常量的情况如出一辙。
- 本地方法栈中JNI(Native方法)的引用对象
- 活跃线程的引用对象
垃圾回收——回收算法
判断了哪些对象是垃圾只是第一步,我们还需要解决一个很重要的问题:如何处理这些垃圾?或者说,如果回收这些垃圾?
垃圾回收算法有以下这几种:
标记-清除算法(Mark and Sweep)
- 标记:从根集合进行扫描,对存活的对象进行标记
- 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存
如图:
如上图所示,经过了Mark阶段到达Sweep阶段的时候,所有不可达的对象都会被当做垃圾回收掉。
但是这种方法会存在一些问题,在标记-清除之后,可能会产生大量不连续的碎片,空间碎片多,可能导致之后开辟大对象空间的时候出现内存不够用的情况。
复制算法(Copying)
复制算法将可用的内存按照容量和一定比例划分为两块或多块,并选择其中一块两块作为对象面,其他的作为空闲面。
- 分为对象面和空闲面
- 对象在对象面上创建
- 存活的对象被从对象面复制到空闲面。当被定义为对象面的块的内存用完了,就将还存活着的对象复制到其中一块空闲面上
- 将对象面所有对象清除
这种算法适用于对象存活率低的场景,比如年轻代。这样每次都对内存块进行回收,这样就解决了内存碎片的问题。
推倒重建的过程只需要移动堆顶指针,按顺序分配内容即可。
优势:
- 解决碎片化问题
- 顺序分配内存,简单高效
- 适用于对象存活率低的场景(现在很多虚拟机都采用这种方法回收年轻代,因为年轻代每次都只存活10%左右,用复制算法效果不错)
但是在老年代不能轻易选用这种算法,因为可能出现存活率特别高的情况。
标记-整理算法(Compacting)
这种算法比较适合用于老年代的对象回收。它使用类似”标记-清除”算法的方式进行对象的标记,但是在清除的时候不同。
- 标记:从根集合进行扫描,对存活的对象进行标记
- 清除:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收
“标记-整理”算法是在”标记-清除”的基础上又进行了对象的移动,因此成本更高,但是能够解决内存碎片的问题。
也可以参考这张图:
执行这个算法的时候会把存活的对象压缩到一端,然后将所有可回收的对象清除掉。
这样做的好处:
- 避免内存的不连续性
- 不用设置两块内存互换
- 适用于存活率高的场景(比如涉及分代收集算法中老年代的回收)
分代收集算法(Generational Collector)
这是一种比较主流的垃圾回收算法。
可以理解是一套”组合拳”
- 垃圾回收算法的组合拳
- 按照对象生命周期的不同划分区域以采用不同的垃圾回收算法
- 目的:提高JVM的回收效率
前面已经提过,JDK8之前,比如JDK6和JDK7,里面有年轻代、老年代和永久代,如下图:
但是JDK8之后(包括JDK8)就去掉了永久代:
可以看到,JDK6、JDK7和JDK8中都有年轻代和老年代。其中年轻代的对象存活率低,采用复制算法。而老年代存活率高,一般使用”标记-清除算法”或者”标记-整理算法”。
GC的分类
分代收集的GC分为两种:
- Minor GC。发生在年轻代中的垃圾收集工作,采用复制算法。
- Full GC。与老年代的垃圾回收相关。
年轻代是所有Java对象出生的地方,即Java对象申请的内存和存放对象,都是在年轻代进行的。
实际上,Java大部分对象都不会长久存活,”朝生夕灭”。新生代是GC发生的频繁区域。
老年代的回收一般伴随着年轻代的垃圾收集,因此第二种垃圾回收方式被命名为”Full GC”
年轻代:尽可能快速地收集掉那些生命周期短的对象
- Eden区
- 两个Survivor区
对象刚被创建出来的时候,其内存空间首先被分配在Eden区。如果Eden区放不下新创建的对象的话,对象也有可能被直接放在Survivor甚至是老年代中。
而两个Survivor则分别被定义在from区和to区,并且哪个是from区,哪个是to区,也不是固定的,会随着垃圾回收的进行而相互转换。
年轻代垃圾回收的过程演示
通过一个实例演示年轻代的垃圾回收过程:
演示过程暂时忽略Eden区和Survivor区的大小比例,并且假设每个对象的大小都是一样的。Eden区最多能保存四个对象,Survivor区最多能保存三个对象。
一开始,如果对象在Eden出生,并且Eden被挤满,如下图:
此时会触发一次Minor GC。此时如果对象还存活(绿色的为存活对象),它就会被复制到一个Survivor区里面,假设是复制到了S0里面,此时我们称S0为from区。复制之后会增加1个年龄。比如图中复制过去之后年龄为1.
然后清理所有使用过的Eden区域,如下图:
之后会清空Eden
然后过了一段时间,发现Eden区又被填满了,如图:
此时又会触发一次Minor GC,然后将Eden和S0里面的存活的对象都拷贝到S1里面,同时会把存活的对象的年龄都加1。
此时S1从to区变成了from区,而S0从from区变成了to区。
拷贝完成后,Eden和S0都会被清空,以此完成了第二次Minor GC。
之后我们假设Eden区又满了:
此时会出发第三次Minor GC,操作行为也和之前一样,年龄加1。同时S1里面如果有一个对象没有被用到,那么也要把它清除。
每次拷贝,存活对象的年龄都要加1.
拷贝完成后,S1和Eden又会被再次清空:
周而复始。对象在Survivor区每熬过一次Minor GC,其年龄就会被加1,当对象的年龄达到某个值之后(默认是15岁),这些对象会成为老年代。
NOTE:这个默认年龄可以通过-XX:MaxTenuringThreshold
调整
但这也不是一定的,如果存储的对象过大,Eden区和Survivor区都存不下,可能会需要用到老年代的空间协助存储。
对象如何晋升到老年代
在分代算法当中,对象如何晋升到老年代?简单来说有3种场景:
- 经历一定Minor GC次数(比如是15次)依然存活的对象
- Survivor区中村放不下的对象
- 新生成的大对象(可以用:
-XX:+PretenuerSizeThreshold
来控制大对象的大小,只要大于这个大小,对象生成之后直接放入老年代)
常用的调优参数
介绍几个常用的用来做性能调优的参数。
- -XX:SurvivorRatio:Eden和Survivor的比值,默认8:1
- -XX:NewRatio:老年代和年轻代内存大小的比例(比如若值为2,则老年代是年轻代大小的两倍,即young generation占据内存的三分之一)。
- -XX:MaxTenuringThreshold:对象从年轻代晋升到老生代经历过GC次数的最大阈值
新生代和老年代的总内存大小是通过”-Xms”和”-Xmx”来决定的。
老年代:存放生命周期较长的对象
回顾这副图:
可以看到,老年代占的内存比新生代大,而且大致的比例为2:1
老年代的对象存活率较高,而且没有额外空间做担保,所以老年代主要用的算法为:
- 标记-清理算法
- 标记-整理算法
之前已经详细介绍过这两种算法,这里就不再介绍了。
Full GC
当触发老年代的垃圾回收的时候,往往也会伴随对新生代堆内存的回收,即对整个堆进行垃圾回收,也就是所谓的Full GC,或者叫做Major GC。Major GC和Full GC是等价的,即收集所有的GC堆。
主要是因为HotSpot VM发展了很多年,外界对很多名词的解读都已经混乱了,当有人说到了”Major GC”的时候,一定要问清楚,他说的到底是针对所有代的Full GC,还是只是针对老年代的GC。
Full GC比Minor GC慢(慢十倍),但因为老年代里面元素本身就不容易被淘汰,所以执行频率也会更低。
触发Full GC的条件
- 老年代空间不足——如果创建的对象很大,Eden区域放不下这个对象,会放入到老年代中。如果老年代空间也不足,就会触发Full GC。为了避免这种情况,最好就是不要创建太大的对象。
- 永久代空间不足——这主要是针对JDK7以及以前的版本。当系统中需要加载和调用的类很多,而同时持久代当中没有足够的空间去存放类的信息和方法信息的时候,就会触发出一次Full GC。而JDK8以后由于取消了永久代,就不存在”永久代空间不足”这种情况了。(这也是JDK8后面用元空间替代永久代的原因之一,为了降低Full GC的频率,减少GC的负担,提升其效率)
- CMS GC时出现promotion failed, concurrent mode failure。对于采用CMS 进行老年代GC的程序而言,如果GC日志中出现了这两个字段。如果出现了,可能会触发Full GC。
- Minor GC晋升到老年代的平均大小大于老年代的剩余空间
- 调用System.gc()——这个是我们在程序里面手动调用的,触发Full GC。需要注意这个方法只是提醒虚拟机,程序员希望你在这里回收一下对象。但是具体怎么做还是要看虚拟机自己,程序员没有控制权
- 使用RMI来进行RPC或管理的JDK应用,每小时执行1次Full GC
这些点很多,面试的时候只要能够提到3点,基本可以点到为止了,可以答到老年代空间不足,程序手动调用System.gc(),然后如果用的JDK版本比较老,在JDK8之前的版本,会有永久代空间不足的情况。当然其他的能说出来更好。
需要注意:
1.promotion failed是在进行Minor GC的时候Survivor放不下了,对象只能放入老年代,而此时恰好老年代也放不下,这时候就会造成promotion failed。
2.concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代中,而此时老年代空间不足,就会造成这个failure。
而对于Minor GC晋升的这第四点,是比较复杂的触发情况。HotSpot为了避免由于新生代对象晋升到老年代而导致老年代空间不足的现象,在进行Minor GC的时候做了一个判断:如果之前统计所得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间,就直接触发Full GC。例如,程序第一次触发GC后有6M的对象晋升到老年代,当下一次Minor GC发生的时候,首先先检查老年代的剩余空间是否大于6M,如果小于6M,则执行Full GC。
垃圾回收——垃圾收集器
Stop-the-World
- JVM由于要执行GC而停止了应用程序的执行
- 会在任何一种GC算法中发生
- 多数GC优化通过减少Stop-the-World发生的时间来提高程序性能,从而让系统有高吞吐,低停顿的特点
Safepoint
JVM垃圾回收就好比是保洁阿姨在打扫卫生,如果一边打扫一遍有人扔垃圾,那很难能打扫完。怎么办呢?可以在开始打扫之前和所有人说好:”我要开始打扫了!你们不准扔垃圾了!”,这样就可以了。
- 分析过程中对象引用关系不会发生变化的点——这是程序运行过程中的一个特殊点的,在这个点所有线程都被冻结了,不能出现分析过程中对象引用关系还在不断变化的情况。类似函数的可导,我们分析的结果需要在某个节点具备确定性,这个节点就叫做安全点。
- 产生Safepoint的地方一般是:方法调用;循环跳转;异常跳转等
- 安全点数量得适中——安全点选择不能太多也不能太少
JVM的运行模式
JVM有两种运行模式:Server和Client。
这两种运行模式的区别在于:Server启动较慢,Client启动较快。但是启动后运行进入稳定期之后,Server模式的程序运行速度比Client更快。
因为Server模式启动的是重量级的虚拟机,对程序采用了更多优化,对比之下Client模式启用的是轻量级的虚拟机。
如果想要查看当前Java是Server模式还是Client模式,可以直接用java -version
查看即可.
垃圾收集器之间的联系
垃圾收集器不存在哪个好那个坏的问题,而是涉及到适合哪个具体的JVM。不同的厂商,不同版本的JVM,提供的选择也不同,这也是HotSpot实现这么多收集器的原因。
一些常见的垃圾收集器、它们之间的关系和它们的适用范围,如图所示:
如果两个收集器之间有连线,说明它们可以搭配使用。
我们只需要大致熟悉每一个垃圾收集器的作用即可。
下面分别介绍:
年轻代收集器
Serial收集器
可以在程序启动的时候通过-XX:+UseSerialGC
设置使用此收集器。使用复制算法。
在JDK1.3之前,是Java虚拟机年轻代收集器的唯一选择。
Java中历史最悠久的收集器。
- 单线程收集,GC时必须暂停所有工作线程
- 简单高效,Client默认用这个作为年轻代收集器
工作过程如下图所示:
实际中系统分配给虚拟机管理的内存不会很大,一般就几十兆到一百兆,收集这么多的年轻代的停顿时间也就几十毫秒,一百毫秒左右。只要不是太频繁,这样的停顿是可以接受的。
ParNew收集器
可以在程序启动的时候通过-XX:+UseParNewGC
设置使用此收集器。使用复制算法。
- 除了是多线程收集,其余的行为、特点和Serial收集器一样
- 单核执行效率不如Serial,在多核下执行才有优势
在单核执行的环境中,表现不会比Serial更好,因为存在键程交互开销。但是随着CPU增加,它的表现会更好。它默认开启的收集线程数和CPU数相同。在CPU数量非常多的情况下,可以使用ParGCThreds的参数来限制垃圾收集的线程数
ParNew是Server模式下虚拟机首选的年轻代收集器。因为除了Serial之外,目前只有它可以和CMS收集器配合工作。
ParNew工作过程如下图:
Parallel Scavenge收集器
可以在程序启动的时候通过-XX:+UseParallelGC
设置使用此收集器。使用复制算法。
这个收集器和系统吞吐量有关。
什么是系统的吞吐量?
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
也就是运行用户代码时间/CPU消耗总时间。比如虚拟机一共运行了100分钟,垃圾收集用了2分钟,吞吐量就是98%
Parallel Scavenge收集器有些类似ParaNew收集器,也是多线程,但是与ParNew相比也有不同:
- 相比ParNew,Parallel Scavenge对系统吞吐量的重视程度大于对用户线程停顿的时间的重视程度。虽然停顿时间短比较适合与用户相互的程序,因为响应速度更快可以提升用户体验;但高吞吐量可以高效率利用CPU时间,尽可能快地完成运算任务,比较适合在后台运算而不用和用户交互的任务。
- 在多核下执行才有优势,Server模式下默认的年轻代收集器
Parallel Scavenge和ParNew工作过程基本相同,如下图:
值得一提的是,如果程序员本身对垃圾收集器不太了解,在程序优化过程中遇到了困难的时候,可以这样解决:在启动的时候加上参数-XX:+UseAdaptiveSizePolicy
,使用Parallel Scavenge的自适应调节策略,这样就可以把内存管理的调优任务交给虚拟机自己去完成。
老年代收集器
Serial Old收集器(MSC)
可以在程序启动的时候通过-XX:+UseSerialOldGC
设置使用此收集器。使用标记-整理算法。
Serial模式的老年版
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的老年代收集器
工作流程如图:
Parallel Old收集器
可以在程序启动的时候通过-XX:+UseParallelOldGC
设置使用此收集器。使用标记-整理算法。
这个收集器在JDK6之后才开始提供的。在此之前新生代的Parallel Scavenge收集器一直处在一个比较尴尬的位置,因为如果新生代选了它,老年代就只能选Serial Old收集器了。
Parallel Old收集器的出现就是为了解决这个问题。
直到Parallel Old出现之后,吞吐量优先收集器才有了名副其实的组合。
- 多线程,吞吐量优先
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge + Parallel Old收集器的组合。
工作流程如图:
CMS收集器
可以在程序启动的时候通过-XX:+UseConcMarkSweepGC
设置使用此收集器。使用标记-清除算法。
实际上,CMS收集器几乎占据着JVM老年代收集器的半壁江山。它的划时代的意义就是垃圾回收线程几乎能与用户线程做到同时工作——说是”几乎”,是因为它不能完全做到不”Stop-the-World”,它只是能尽可能地缩短停顿时间。需要注意如果你的程序对停顿比较敏感,并且在应用程序运行的时候可以提供更大的内存和更多的CPU,那么用CMS是好的选择。
此外,如果在JVM中有相对较多而且存活时间较长的对象,也更适合使用CMS。
CMS的整个垃圾回收过程可以分为下面六步:
- 初始标记:stop-the-world,JVM停顿正在执行的任务,从垃圾回收的根对象开始,只扫描和根对象直接关联的对象,时间短
- 并发标记:并发追溯标记,程序不停顿。这个阶段中应用标记的线程和并发标记的线程并发执行,所以用户不会感受到停顿。
- 并发预清理:查找执行并发标记时晋升老年代的对象。可能有一些对象从新生代晋升到老年代,或者有些对象直接被分配到老年代,通过重新扫描,减少下一个阶段重新标记的工作(因为下一阶段会重新stop-the-world)。这个过程不停顿
- 重新标记:暂停虚拟机,扫描CMS堆中剩余对象,扫描从根对象开始向下追溯,并处理对象单元。这一步相对较慢
- 并发清理:清理垃圾对象,程序不停顿
- 并发重置:重置CMS收集器的数据结构,等待下一次垃圾回收
上述过程中,初始标记和重新标记需要短暂的stop-the-world的
工作流程如下图:
并发标记的过程实际上是和用户线程同时工作的,也就是一边丢垃圾,一边打扫。但这也可能产生一个问题,就是某个垃圾如果在打扫之后产生的,那么这个垃圾就只能等到下次垃圾回收才能被收掉,也就是说垃圾打扫完一次后没有完全打扫干净。
但是CMS收集器因为用的是”标记-清除算法”而不是”标记-整理算法”,就不可避免导致了垃圾碎片化的问题。如果此时需要分配较大的对象,那就只能触发一次GC了。
G1收集器
可以在程序启动的时候通过-XX:+UseG1GC
设置使用此收集器。使用多种算法,即”复制算法 + 标记-整理算法”。
G1收集器既用于年轻代,也用于老年代。全称:Garbage First收集器。
实际上,HotSpot最终的目的是让G1收集器最后能替换掉JDK5发布的CMS收集器。
Garbage First收集器的特点:
- 并行和并发——使用多个CPU来缩短stop-the-world的停顿时间,与用户线程并发执行
- 分代收集——独立管理整个堆,但是能够采用不同的方式去处理新创建的对象和已经熬过多次GC的旧对象以获得更好的收集效果
- 空间整合——基于”标记-整理算法”,解决了内存碎片的问题
- 可预测停顿——能建立可预测的停顿时间模型,设置用户在某个地方的停顿时长不能超过m毫秒,类似这样
在Garbage First垃圾收集器之前的收集器,都是只针对年轻代或者老年代的。而Garbage First可以同时针对年轻代和老年代。
在使用Garbage First收集器的时候,Java堆的布局和使用其他垃圾收集器时有很大不同:
- 将整个Java堆内存划分成多个大小相等的Region——虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了
- 年轻代和老年代不再物理隔离——它们可以是不连续的Region的集合。这也使得分配内存空间的时候可以不是连续的
也就是说此时在JVM启动的时候不需要决定哪些Region属于老年代,哪些Region属于年轻代。因为随着时间推移,年轻代的Region被回收以后,就会变为可用状态,此时也可以把它分配成老年代。
和其他的HotSpot一样,当一个年轻代GC发生时,整个年轻代会被回收,G1的老年代收集器有所不同,它在老年代不需要整个老年代进行回收,只有一部分Region被调用。
G1的年轻代由Eden Region和Survivor Region组成。当一个JVM分配Eden Region失败后,会触发一个年轻代回收,意味着Eden区满了。之后GC开始释放空间,第一个年轻代收集器会移动所有的存储对象,从Eden Region到Survivor Region,这就是copy to survivor的过程。
JDK11还有研发Epsilon GC和ZGC,这里暂时先不介绍。
回顾一个问题
如上图,为什么CMS不能和Parallel Scavenge一起工作呢?两者为什么不兼容呢?
CMS是HotSpot在JDK5的时候推出的第一款整整意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程同时工作。
CMS作为老年代收集器不能和Parallel Scavenge一起工作主要是因为Parallel Scavenge和CMS代码框架不同。
GC相关面试题
Object的finalize()方法的作用是否与C++的析构函数作用相同
答:
- finalize()和C++的析构函数不同,析构函数调用的时机是确定的,对象离开作用域后就会调用,然后对象被delete掉。而finalize()具有不确定性,也就是对象还没用完可能就被GC
- 将未被引用的对象放置于F-Queue队列
- 方法执行随时可能会被终止
- 给予对象最后一次重生的机会
但是由于finalize()方法代价比较高昂,所以不建议使用。
Java中的强引用、软引用、弱引用、虚引用有什么用
强引用(Strong Reference)
- 最普遍的引用:Object obj = new Object(),这里obj就是强引用
- 抛出OutOfMemoryError终止程序也不会回收具有强引用的对象
- 通过将对象设置为null来弱化引用,使其被回收
软引用(Soft Reference)
- 对象处在有用但非必须的状态
- 只有当内存空间不足时,GC会回收该引用的对象的内存
- 可以用来实现高速缓存——这样我们就可以避免OutOfMemory的问题。因为软引用的内存会在内存不足的情况下回收。
强引用和软引用例子如下图:
弱引用(Weak Reference)
- 非必须的对象,比软引用更弱一些
- 生命周期更短,在GC时会被回收——无论当前内存是否紧缺,GC都会回收被弱引用关联的对象
- 被回收的概率也不大,因为GC线程优先级比较低
- 弱引用适用于偶尔使用且不影响垃圾收集的对象
弱引用案例:
弱引用同样可以配合引用队列去使用。
虚引用(Phantom Reference)
“虚无缥缈”,其生命周期比较不固定
- 不会决定对象的生命周期
- 任何时候都可能被垃圾收集器回收
- 跟踪对象被垃圾收集器回收的活动,起哨兵作用
- 比较特殊,必须和引用队列ReferenceQueue联合使用
GC在回收一个对象时,若发现这个对象有虚引用,那么回收前会先将这个引用加入到与之关联的引用队列当中。
四种引用之间的关系
引用类结构层次
引用队列(ReferenceQueue)
引用队列名义上是一个队列,但其内部没有实际存储结构。
- 无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达的——类似链表,节点是Reference本身,它自己只存储链表的头结点,而后面的节点都通过Reference指向下一个的next来保持。
- 存储关联的且被GC的软引用,弱引用以及虚引用