文章目录
周阳JVM
1、前言
-
JVM(Java二进制字节码的运行环境)。
-
好处:
- 一次编写,到处运行。
- 自动内存管理,垃圾回收功能。
-
JVM、JRE、JDK:
1.1、JVM体系结构
- 在运行时数据区域中,橙色的是共享的,同时也有GC的,灰色的是线程私有的。
1.2、类加载器(ClassLoader)
2. 这个安全标示就是cafe babe
。
3. 类加载器只负责将.class
文件(编译形成)加载到JVM。此时将.class
文件从磁盘
加载到方法区
,形成运行时数据结构(简单理解形成了一个Class模板,全局就这么一份)。
4. 具体关系:实例化对象
2、JVM运行时的内存划分
- 根据上图可以知道运行时的数据区域:堆、方法区(元空间)、虚拟机栈、本地方法栈、程序计数器。
2.1、堆
- 对象的实例以及数组的内存都是在堆上进行分配的。堆管存储的。
- 是线程共享的区域。是垃圾回收的主要区域。
- 类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
- 堆可以细分:新生代(eden区、幸存者1区(from)、幸存者2区(to))、老年代。
总结:堆在逻辑上分为新生+养老+永久代(元空间),在物理上分为新生+养老。永久代或者元空间保存Object/String/ArrayList
这些信息。
2.1.1、堆内存分配策略
- 对象
优先分配
在eden,当eden没有足够空间
进行分配的时候,此时虚拟机执行一次MinorGC
。没有回收的存活对象
将会进到 Survivor 的From 区
(From 区内存不足时,直接进入 Old区
)。 - 大对象直接进入到老年代(需要大量连续内存空间对象),目的是避免在eden区和两个幸存区之间发生大量的内存拷贝。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过1次Minor GC之后没有被回收,那么进入到幸存者区,之后每Minor GC,该对象年龄+1,直到达到阈值(15),对象就进入到老年区。
- 动态判断对象的年龄。如果幸存者区相同年龄的所有对象的总和大于幸存者区空间的一半的时候,年龄大于等于相同年龄的这些对象就直接进入老年代。
注:GC = 发生在新生代的minor GC + 发生在养老区的Full GC;垃圾回收 = GC + FullGC - 为什么需要from区和to区?幸存区存在的意义就是减少被送到老年代的对象,老年代对象越多,Full GC就越多,Full GC是重量级,Full GC可能导致JVM暂停。survivor的预筛选保证只有经历15次minor GC还能在新生代存活的对象才会被送到老年代。
- eden:from:to = 8:1:1,新生代和老年代分别占堆的1/3和2/3。
- 为什么有两个幸存区?这是Oracle经过多次实验得出的,分的越细,产生的内存碎片也就越多。
2.1.2、Full GC触发条件
- 每次进行Minor GC,JVM会计算幸存者区的对象的平均大小,如果这个值大于老年区的剩余值的大小的时候,那么进行一次Full GC。如果小于检查HandlePromotionFailure设置,如果是true,只进行Minor GC,false进行Full GC。
2.2、方法区
-
只是JVM里面提出的一种规范、接口。
-
方法区是抽象,永久代和元空间是具体实现。
JDK8之前:
JDK8之后
永久代缺点:- 可能会引起内存溢出
- 永久代本身的复杂设计并不是方法区需要的,并可能带来未知异常。
-
存储的是每一个类的结构信息(已经被Java虚拟机加载的类信息、常量、静态变量等)。
-
JDK1.8之前,方法区称为永久代;之后变为了元空间。
-
是线程共享的区域。
-
理解:在启动类加载器加载
rt.jar
的时候,就已经把最根本的方法区(元空间),不会产生频繁的垃圾回收。
2.2.1、加载问题
2.2.1.1、普通代码块
- 普通代码块:在
方法或者语句
中出现{}
。 - 普通代码块和一般的语句执行顺序由他们在代码中出现的次序决定,先出现先执行。
2.2.1.2、构造代码块
- 构造代码块:直接
在类中
定义且没有static修饰
没有名字的{}
代码块。也就是普通代码块 - 构造代码块在每次创建对象的时候都会被调用,并且构造代码块的执行次序优先级高于类的构造函数。
- 构造代码块new一次加载一次。
- 答案:312
2.2.1.3、静态代码块、普通代码块、构造方法
- 使用public修饰的类是主类,没有使用public的是次类。
- 静态代码块就是使用static{}修饰的代码块。
- 静态的东西是全局共有,100%最优先加载。 但只加载一次。
- 静态 > System.out.println() > 代码块 > 构造方法。
2.3、虚拟机栈
-
是线程私有的,声明周期与线程生命周期一致。栈管运行的。大小通常只有1M左右。
-
8种基本类型的变量+对象引用变量+实例方法都是在函数的栈内存中分配。
3. 栈帧中主要存储3类数据:本地变量:输入参数和输出参数以及方法内的变量;栈操作:记录出栈、入栈操作;栈帧数据:包括类文件、方法等; -
栈中存放的都是
栈帧
(java叫方法,JVM叫栈帧),每个方法执行的时候都会创建一个栈帧,栈帧中存放:局部变量表、操作数栈、动态链接、返回地址
。局部变量表
:是一组变量值存储空间,用来存放方法参数、方法内部的局部变量。操作数栈
:用来记录一个方法在执行过程中,字节码执行向操作数栈中进行入栈和出栈的过程。动态链接
:因为字节码文件中有很多符号的引用,这些符号引用一部分会在类加 载的解析阶段或第一次使用的时候转化成直接引用,这种称为静态解析;另一部分会 在运行期间转化为直接引用,称为动态链接。返回地址
:pc寄存器每执行一条指令都会被改变,而返回地址在调用call之前一直是上一条call后面的地址,不改变
-
会出现两种异常:
-
如果线程请求的栈深度大于虚拟机的深度,那么就会java.lang.StackOverflowError。
-
如果虚拟机栈动态扩展的时候无法申请到足够的内存,会抛出OOM:Java heap space异常。1:JVM的堆内存设置不够。2:代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在引用)。
-
-
是线程私有的。
2.3.1、栈、堆、方法区的交互关系
- Java堆中会存放访问类元数据的地址(大Class-模板)。
- reference存储的就直接是对象的地址。
- 举例,比如
MyObject myObject = new MyObject()
;,等号左边MyObject myObject的myObject
就是引用
,在Java栈
里面。等号右边的new MyObject()
,new出来的MyObject实例对象在堆里面
。简单来说,就是Java栈中的引用myObject指向了堆中的MyObject实例对象
。
2.4、本地方法栈
- 本地方法栈服务的是
native
方法。 - hotspot中把本地方法栈和虚拟机栈合而为一,那么也会产生上述所说的两个异常。
- 是线程私有的。
- 以
Thread t1 = new Thread(); t1.start();
为例,start()
方法(使用synchronized)做了些什么?
使用了native
关键字之后,就调用本地方法接口,此时就不归Java管了,表示调用底层操作系统或者C语言编写的第三方函数库
2.5、程序计数器
- 存放的是下一条指令的地址。
- 是线程私有的。
- 由于线程的切换,CPU在执行的过程中,一个线程执行完了,接下来CPU切换到另一个线程去执行,另外一个线程执行完再切回到之前的线程,这时需要记住原线程的下一条指令的位置,所以每一个线程都需要有自己的程序计数器。
- 在CPU里面,作用在内存上。
3、Java类加载的过程
- Class的生命周期:加载、验证、准备、解析、初始化、使用、卸载。
- 这些阶段都是互相交叉混合使用,会在一个阶段执行的过程中调用、激活另一个阶段。
3.1、加载
- 该阶段完成3个事:
- 通过一个
类的全限定名获取定义此类的二进制字节流
。 - 将这个字节流所代表的
静态存储结构转换成方法区的运行时数据结构
。 - 在内存中
生成一个代表这个类的java.lang.Class对象
,作为方法区这个类的各种数据的访问接口
。
- 通过一个
3.2、验证
- 该阶段的目的是
确保Class文件的字节流中包含的信息符合Java虚拟机规范的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身安全
。 - 验证分为4阶段:
- 文件格式验证,确保文件格式符合Class文件格式的规范。
- 元数据验证,确保Class的语义描述符合Java的Class规范。
- 字节码验证,通过分析数据流和控制流,确保程序语义符合逻辑。
- 符号引用验证,发生于符号引用转换为直接引用的时候(转换发生在解析阶段)。
3.3、准备
- 该阶段,虚拟机会
在方法区
中为类中定义的静态变量分配内存
,并设置static成员变量的初始值为默认设置
。如:int类型初始化为0,引用类型初始化为null。
3.4、解析
- 该阶段,
虚拟机会将常量池中的符号引用替换为直接引用,主要针对类、接口、方法、成员变量等符号引用
。在转换成直接引用之后,会触发验证阶段的符号引用进行验证,验证转换之后的直接引用是否能找到对应的类、方法、成员变量等。
3.5、初始化
- 在该阶段,会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源,即:
执行类构造器clinit的过程
。<clinit>()
方法中执行的是对static变量进行赋值的操作,以及static语句块中的操作。- 虚拟机会确保先执行父类的
<clinit>()
方法。 - 如果一个类中没有static的语句块,也没有对static变量的赋值操作,那么虚拟机不 会为这个类生成
<clinit>()
方法。 - 虚拟机会保证
<clinit>()
方法的执行过程是线程安全的。
六种必须立即对类进行初始化的情况:
- 使用new关键字实例化对象的时候,读取或设置一个静态字段的时候(不包括常量),调用一个类型的静态方法的时候。 (new对象的时候,调用了static定义的字段或者方法,除了常量)
- 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。(反射)
- 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。(优先加载父类)
- 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类 (初始化用户指定的执行主类)
- 当使用JDK7新加入的动态语言支持时 。
- 当一个接口定义了JDK8新加入的默认方法时,如果这个接口的实现类发生了初始化,那么该接口要在其之前被初始化。
4、Java内存模型
- Java Memory model,即JMM。
- JMM定义了一套在多线程读写共享数据的时候,对数据的可见性、有序性和原子性的规则和保障。
JMM的作用就是定义程序中各个变量的访问规则,就是关注在虚拟机中把变量存储到内存以及从内存取出的变量的底层细节。
(变量是包括了实例字段、静态字段以及构成数组对象的元素,但并不包括局部变量和方法参数,这些是线程私有的)。- JMM规定所有的变量都存储在主存中,每个线程都有自己的工作内存,线程的工作内存中保存了被该线程使用变量的主内存副本,线程间变量的传递均需要通过主内存来完成。
4.1、内存间交互操作
- `lock:作用于主内存的变量,把一个变量表示为一条线程独占的状态。
unlock
:作用于主内存变量,把一个处于锁定状态的变量释放出来,释放之后的变量才可以被其他线程锁定。read
:作用于主内存变量,把一个变量的值从主内存传输到线程的工作内存中。load
:作用于工作内存的变量,把read操作中从主内存得到的变量值放入工作内存的变量副本中。use
:作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机需要一个需要使用变量的值的字节码指令的时候,就会执行该操作。assign
:作用于工作内存的变量,把一个从执行引擎接受的值赋给工作内存变量,每当虚拟机遇到一个给变量赋值的字节码指令的时候,就执行该操作。store
:作用与工作内存的变量,把工作内存中一个变量的值传输到主内存中,方便write操作使用。write
:作用于主内存的变量,把store操作从工作内存得到的变量值放入到主内存的变量中。
4.1.1、执行基本操作的规则
- 不允许read和load、store、write三个任意一个操作一起出现,即:不允许一个变量从主内存读取但工作内存不接受,或者工作内存写回主内存不接受情况。
- 不允许一个线程丢弃它最近的assign操作,即:变量在工作内存中改变了之后必须把该变化同步回主内存。
- 不允许一个线程在没有发生任何assign操作情况下把数据从线程的工作内存同步回主内存,即:必须在工作内存有变化才能写回主内存。
- 变量只能在主内存中产生,不允许在工作内存中直接使用一个未被初始化的变量。即:对一个变量实施use、store操作之前,必须先执行assign和load操作。
- 一个变量在同一时刻值允许一个线程对其进行lock操作,但lock操作可以被同一线程执行多次,同时多次执行lock,就必须要执行相同次数的unlock,变量才会被解锁。
- 如果对一个变量执行lock操作,那么将会清空工作内存中此变量的值,在执行引擎使用这个变量之前,需要重新执行load或者assign操作用来初始化变量的值。
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中。
4.2、JMM三大特性
4.2.1、原子性
- JMM直接保证的原子性变量操作包括read、load、assign、use、write,可简单理解认为,基本数据类型的访问、读写都是具备原子性的。
4.2.2、可见性
- 是指当一个线程修改了共享变量的时候,其他线程能够立即得知这个修改,volatile、synchronized、final都能实现。
4.2.3、有序性
- 如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的
5、双亲委派机制、沙箱安全机制
- Java三层类加载器:
- 启动类加载器:用来加载Java核心类,是用原生代码实现的,并不继承Java.lang.ClassLoader。无法直接获取到启动类加载器的引用(使用C++写的,并不存在于JVM体系,输出是null)。加载
%JAVAHOME%/jre/lib/rt.jar
。 - 扩展类加载器:负责加载JRE的扩展目录,由Java语言实现,父类加载器为null。加载
%JAVAHOME%/jre/lib/ext/*.jar
。 - 应用程序加载器:负责加载用户类路径上所有的类库。加载
%CLASSPATH%
的所有类
- 启动类加载器:用来加载Java核心类,是用原生代码实现的,并不继承Java.lang.ClassLoader。无法直接获取到启动类加载器的引用(使用C++写的,并不存在于JVM体系,输出是null)。加载
- 双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应该有自己的父类加载器。父类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用加载器的代码。
5.1、工作流程
- 如果一个类加载器收到了类加载的请求,
首先
不会自己去尝试加载这个类,而是把这个请求委派给父类加载器
去完成,每一层次的类加载器都是这样,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中
,只有
当父加载器反馈自己无法
完成这个加载请求,子加载器才会自己尝试去加载
。
5.2、优点
- 使用双亲委派模型来组织类加载器之间的关系,好处就是Java中的类会随着它的类加载器一起具备了一种带有优先级的层次关系。
很好的解决了各个类加载器协作的时候基础类型的一致性问题。
- 简单说就是
防止内存中出现多份相同的字节码
。
5.3、如何打破双亲委派机制?
- 使用线程上下文类加载器,可以在执行线程中抛弃双亲委派加载链模式,使用线程上下文中的类加载器加载类。
- 通过重写loadClass()实现自定义加载委派机制。
5.4、沙箱机制
- 是基于双亲委派机制上采取的一种JVM的自我保护机制,假设写一个
java.lang.String
的类,由于双亲委派机制
的原理,此请求会先交给BootStrapClassLoader
试图进行加载,但是BootStrapClassLoader
在加载类时首先通过包和类名查找rt.jar
中有没有该类,有则优先加载rt.jar包中的类
,因此就保证了java的运行机制不会被破坏
,确保你的代码不会污染到Java的源码
。
6、四种引用以及使用场景
- 除了强引用,其他三种引用都可以在java.lang.ref包下找到。
- 终引用是包内可见的。
6.1、强引用
- 强引用就是在程序中普遍存在的引用赋值,即:
Object obj = new Object()
。 - 强引用的对象的GC Rooot标记可达的。
- 无论什么情况下,只要强引用关系存在,垃圾收集器就永远不会回收掉引用对象,宁可报OOM也不回收。
6.2、软引用
内存不足就回收
。- 只要被软引用关联的对象,在系统即将发生内存溢出之前,会把这些对象列进回收范围之中进行二次回收,如果回收之后还是没有足够内存,才会报OOM。
软引用通常用来实现内存敏感的缓存。
使用场景:高速缓存就使用到了软引用。有空闲内存,就可以暂时保留缓存,当内存不足的时候清理掉
。- 垃圾回收器在某个时刻决定回收软引用对象的时候,会清理对象,并将引用存放到一个引用队列(Reference Queue)中。
6.3、弱引用
只被弱引用关联的对象只能生存到下一次垃圾收集发生的时候。
在GC的时候,只要有弱引用,不管堆空间是否充足都会回收掉被弱引用关联的对象。
- 垃圾回收器的线程优先级很低,所以并不一定能很快发现持有若引用的对象,那么弱引用对象可以存在较长时间的。
- 在构造弱引用的时候,也可以指定一个引用队列,当弱引用对象被回收的时候,就会加入指定的应用队列,通过队列可以跟踪对象回收情况。
- 同样弱引用也适合保存缓存数据。
6.4、虚引用
- 是所有引用类型中最弱的一个。
- 一个对象是否是有虚引用的存在,完全不会决定对象的什么周期。一个对象仅持有虚引用,那么和没有引用几乎没什么区别,随时都可能被回收的。
- 虚引用不能单独使用,也无法通过虚引用来获取对象。
- 为一个对象设置虚引用关联的唯一目的:跟踪垃圾回收过程。
6、垃圾回收算法
参考:GC四大算法
6.1、标记清除算法
适用于老年代
-
首先标记处所有需要回收的对象,在标记完成之后,同一回收掉所有被标记的对象,也可以反过来,标记存活的对象,同一回收未被标记的对象。
-
优点:速度很快,清除操作只记录垃圾对象在内存中的起始地址和结束地址。下次分配内存直接进行覆盖。
-
缺点:
- 执行效率不稳定,当对重包含大量对象的时候,需要进行大量的标记和清除对象,导致标记和清除两个动作的执行效率都随着对象数量的增长而降低。(即:堆中对象越多,效率就越低)
- 内存碎片化,标记清除之后会产生大量不连续的内存碎片。
6.2、标记复制算法(GC)
适用于新生代
- HotSop把新生代分为eden区+幸存者1区(from区)+幸存者2区(to区),每次分配内存只使用eden和from区。
- 当eden区满的时候触发第一次GC,将没有被回收的对象拷贝至from区。当eden区再次触发GC的时候,此时扫描eden区和from区,对这两个区域进行垃圾回收。此时将还有GC Root引用的对象复制到to区,随后清空eden和from区。最后from区和to区交换。
- 优点:如果对象存活率较低,那么垃圾收集简单高效,也避免了内存碎片问题。
- 缺点:在对象存活率较高的时候,那么复制操作就要进行多次,效率较低,所以在老年代一般不能直接选用这种算法。
6.3、标记整理算法
- 让所有存活的对象都向内存空间一段进行移动,然后直接清理掉边界以外的内存。
- 优点:能让内存访问更加方便,能提高程序的整体吞吐量。
- 缺点:需要移动对象,效率不高
6.4、分代收集算法
- 分代收集建立在两个分代假说之上:
- 弱分代假说:绝大多数对象都是朝生夕灭的。
- 强分代假说:经过多次垃圾收集的对象就难以消灭。
- 两个分代假说共同奠定多款常用的垃圾收集器的一致设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象根据其年龄分配到不同的区域中存储。
7、经典垃圾收集器
7.1、Serial收集器(标记复制)
- 是最基本的垃圾收集器,足够成熟稳定,可以商用
- 特点:单线程工作、简单、高效、所需额外内存最小。
- 是HotSpot虚拟机在客户端模式下默认的新生代收集器。
7.2、ParNew收集器(标记-复制)
- 实质是Serial收集器的多线程并行版本,很多运行在服务器模式中的HotSpot虚拟机将其作为新生代垃圾收集器。
- 也是激活CMS后默认的新生代垃圾收集器。
7.3、Parallel Scavenge收集器(标记-复制)
- 是能够并行收集的多线程收集器。很多特性与ParNew非常相似。
- 特点:该收集器的目标是达到一个可控制的吞吐量,也被称为吞吐量优先收集器,适用于后台运算不需要太多交互的场景。
7.4、CMS(标记-清除)
- 是一种以最短回收停顿时间为目标的收集器,真正做到垃圾收集线程与用户线程并发,适用于交互较多的场景。
- 有四个阶段:初始标记、并发标记、重新标记、并发清除。
- 优点:并发收集、低延迟。
- 缺点:并发阶段使用程序变慢,吞吐量降低,存在浮动垃圾和空间碎片,需要预留足够的内存空间拱用户线程使用。
7.5、Serial Old收集器(标记-整理)
- 是Serial收集器的老年代版本,主要意义也是拱客户端模式下的HotSpot虚拟机使用。
7.6、Paralle Old收集器(标记-整理)
- 是Parallel Scavenge收集器的老年代版本,支持多线程并发手机,Parallel Old收集器出现之后,吞吐量优先收集器有了比较冥府其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge收集器+Parallel Old收集器组合。
7.7、G1收集器(标记-整理、标记-复制)
- 是一款主要面向服务端应用的垃圾收集器,建立了可预测的停顿时间模型。JDK9之后取代了Parallel Scavenge收集器+Parallel Old收集器组合,称为服务端模式下的默认垃圾收集器。
- 不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆分成多个大小相等的区域,每个区域都可以根据需要,扮演新生代的eden、survivor或者老年代。去榆中还有一类特殊的Humongous区域,专门用来存储大对象,将其作为老年代的一部分看待。
- 采用Mixed GC模式,可以面向对内存任何部分来组成回收,权衡标准不再是属于哪个分代,而是那块内存中存放的垃圾数量最多,回收收益最大。
8、如何判断对象是否存活
8.1、引用计数算法
- 在对象中添加一个引用计数器,每当有一个地方引用它的时候,计数器就+1,当引用失效的时候,计数器就-1,任何时刻计数器为0的对象就是不可能被使用的。
- 优点:实现简单、垃圾对象便于识别。
- 缺点:需要额外内存,需要大量额外处理才能正常工作(循环引用)。会造成内存泄露(两个对象相互引用,但没有引用指向这俩对象)。
8.2、可达性分析算法
周阳GC Roots对象
目的:解决引用计数法的循环引用问题。
-
将根对象作为起始节点集合,沿着引用链向下搜索,不可达的对象作为被回收的对象。只有能够被根对象集合直接或者间接连接的对象才是存活对象。
-
Java技术体系中,哪些是可以作为GC Roots的对象的:(GC Roots就是一组必须活跃的引用)
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,比如:各个线程被调用的方法堆栈中使用的参数、局部变量、临时变量。(
Book b1 = new Book()
) - 在方法区中静态属性引用对象,比如:Java类的
引用类型
静态变量。(private static Object obj = new Object();
) - 在方法区中
常量
引用的对象
,比如:字符串常量池中的引用。(private static final Object o1 = new Object();
) - 在本地方法栈内JNI(Native方法)引用的对象。
- Java虚拟机内部的引用,比如:基本数据类型对应的Class对象,一些常驻异常对象等。
- 所有被同步锁持有的对象(被加锁的对象)。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,比如:各个线程被调用的方法堆栈中使用的参数、局部变量、临时变量。(
-
可达性分析后的标记:
- 如果对象在可达性分析后发现没有与GC Roots相连的链,那么它将被第一次标记。
- 进行一次筛选,如果对象finalize方法已被虚拟机调用过,或者类没有覆盖此方法,那么不会调用该对象finalize方法,否则将对象加入一个队列。
- 虚拟机创建一个低调度优先级的线程去执行队列中这些对象的finalize方法。
- 如果对象在执行finalize方法的时候重新与引用链上的任何一个对象相关联,那么此对象就不会被回收,否则对象被回收。