垃圾回收是指JVM将堆中已经死亡或者长时间没有使用的对象进行清除和回收的过程。C++等语言在创建对象后需要手动去释放对象占用的内存空间,既要写构造函数,又要写析构函数,这样有些许麻烦;Java语言的JVM虚拟机通过垃圾回收可以自动帮我们把堆内存中需要清除的对象清除掉,用户侧是不需要感知到对象内存回收这个过程的。
1、如何定义哪些对象是垃圾
明确了垃圾回收的定义后,就要确定判断内存中对象是垃圾对象的规则。当前主流的内存管理系统都是根据可达性分析算法判断对象是否存活的,Java虚拟机也是。此外,引用计数算法也需要做了解。
1.1 引用计数算法
1.1.1 引用计数算法介绍
引用计数算法(Reachability Counting
)是通过在对象头中分配一个空间来保存对象被引用的次数(Reference Count
),如果该对象被其他对象引用,则引用计数加1,如果指向该对象的引用置为null,则引用计数减1,当该对象的引用计数为0时,该对象就会被垃圾回收。
举例:String m = new String("jack");
先创建一个字符串,此时在堆中的对象jack有一个指向它的引用,即m,此时对象jack的引用计数器值为1,如下:
然后将引用m置为null,此时对象jack对应的引用计数器就等于0了,意味着该对象需要被回收了,如下:
JVM垃圾回收中有一个很著名的名词:Stop-The-World
,意思是在垃圾回收时,整个Java应用是挂起状态(即不能对外提供服务),在此期间在堆中进行垃圾回收工作,直到堆中完成了垃圾回收后Java服务才能恢复。这里要说的是:引用计数算法并不是Stop-The-World,而是在整个Java应用程序运行过程中进行引用计数器值变更和清除对象的。
1.1.2 引用计数算法存在的问题
引用计数算法之所以没有被用在JVM垃圾回收中作为判断一个对象是否该被回收的一个重要原因是:引用计数算法很难解决对象之间循环引用的问题。
举例:
// 定义一个类
public class ReferenceCountingGC {
// 类的属性对象
public Object instance;
public ReferenceCountingGC(String name){}
}
// 测试
public static void testGC() {
// 定义两个对象
ReferenceCountingGC a = new ReferenceCountingGC("objA");
ReferenceCountingGC b = new ReferenceCountingGC("objB");
// 这两个对象通过各自属性引用指向对方,实现相互引用
a.instance = b;
b.instance = a;
// 置空这两个对象的引用
a = null;
b = null;
}
在引用计数算法下,将这两个对象的引用置空后这对象a和对象b已经无法再被访问了,但是由于他们各自的instance对象仍引用这对方,导致对象a和对象b的的引用计数器永远不会为0,这种情况其实就是内存泄漏。最后的结果就是:通过引用计数算法,永远无法通知GC收集器回收这两个对象,出现了内存泄漏问题,最终会导致内存溢出。
1.2 可达性分析算法
1.2.1 可达性分析算法介绍
可达性分析算法(Reachability Analysis
)的基本思路是:通过一些被称为GC Roots
的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为Reference Chain
,当一个对象到GC Roots之间没有任何引用链时(即从 GC Roots 节点到该节点不可达),则证明该对象是需要被垃圾回收的对象。如下图所示:
在上图中,object1、object2、object3、object4到GC Roots之间是存在引用链的,以object4为例,引用链是:GC Roots -> object1 -> object3 -> object4,因此object1、object2、object3、object4不会被垃圾回收。而object5、object6、object7到GC Roots之间是不存在引用链的,因此object5、object6、object7将要被垃圾回收。
1.2.2 Java中的GC Roots
这样就引入了一个问题:哪些对象属于GC Roots呢?在Java语言中,可以作为GC Roots的对象包含以下四种:
- 虚拟机栈帧中的局部变量表里存放的对象的引用指向的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中引用的对象。
(1)虚拟机栈帧中的局部变量表中的引用对象
举例:
public class StackLocalParameter {
public StackLocalParameter(String name) {}
}
public static void testGC() {
StackLocalParameter s = new StackLocalParameter("localParameter");
s = null;
}
s即对象localParameter的引用,当s置为null时,对象localParameter将被垃圾回收。
(2)方法区中类静态属性引用的对象
举例:
public class MethodAreaStaicProperties {
public static MethodAreaStaicProperties m;
public MethodAreaStaicProperties(String name) {}
}
public static void testGC() {
MethodAreaStaicProperties s = new MethodAreaStaicProperties("properties");
s.m = new MethodAreaStaicProperties("parameter");
s = null;
}
其中m即方法区中类静态属性对象parameter的引用,s为对象properties的引用,m和s指向的对象都为GC Roots。
- s指向的对象为GC Root,将s置为null,经过gc后,s指向的对象properties由于无法与GC Root建立关系而被垃圾回收;
- m指向的静态属性对象parameter不会被回收。
(3)方法区中常量引用的对象
举例:
public class MethodAreaStaicProperties {
public static final MethodAreaStaicProperties m = MethodAreaStaicProperties("final");
public MethodAreaStaicProperties(String name){}
}
public static void testGC() {
MethodAreaStaicProperties s = new MethodAreaStaicProperties("staticProperties");
s = null;
}
其中m为方法区中指向常量字符串final的引用,也为GC Roots,当s置为null后,final 对象m也不会因没有与 GC Root 建立联系而被回收。
(4)本地方法栈中引用的对象
任何 native 接口都会使用某种本地方法栈,实现的本地方法接口是使用 C 连接模型的话,那么它的本地方法栈就是 C 栈。当线程调用 Java 方法时,虚拟机会创建一个新的栈帧并压入 Java 栈。然而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不再在线程的 Java 栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。
2、垃圾回收算法
JVM垃圾回收算法主要有标记-清除算法、标记-复制算法和标记-整理算法,这些都只是一种思想,需要具体落地到垃圾收集器的实现中去体现。
现在想想JVM里的垃圾回收算法做的事情就是华为云对象存储里的gc……也是要对plog进行回收,也是会出现很多碎片需要重新拼成新的plog,相当于obs的gc是自己实现了一套垃圾收集器,不过是真的难啊,丰哥都不太能搞定……
2.1 标记-清除算法
标记清除算法(Mark-Sweep)是最基础的一种垃圾回收算法,算法主要分为两步:
- 根据可达性分析算法,把内存区域中需要回收的对象标记出来;
- 把这些标记出来的垃圾对象清理掉。
如下图所示,清理掉的垃圾对象区域就变成了未使用的内存区域,等待被再次使用:
标记清除算法简单清晰,但是有一个致命的问题:内存碎片。举个例子:上图中垃圾回收后,内存就会切成了很多段,比如2M、4M、1M,且这些片段不是连续分布的。开辟内存空间时,需要的是连续的内存区域,假如我们现在需要开辟一块2M的内存空间,那这些分割开的1M的内存碎片就用不上了,如果这样的内存碎片太多的话,即使内存里还是有很多未使用空间,但依然用不上(就是obs里gc面对的问题)。
2.2 标记-复制算法
标记-复制算法是在标记-清除算法上演化而来,解决的就是标记-清除算法的内存碎片问题。标记-复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后将已使用的那块内存空间一次性清理掉。这样就保证了在新的那块内存空间里内存是连续的(因为复制时就是连续复制上去的),内存分配时也就不用考虑内存碎片等复杂情况,逻辑清晰,运行高效。注意:这两块内存是可以来回颠倒使用的,就在这两块内存中来回复制清空数据。如下图所示:
标记-复制算法的问题也很明显,一块完整的内存只有一半的内存空间可以被使用,另一半的内存空间无时无刻都在做备用,内存空间利用率很低。而且当对象存活率较高时,标记-复制算法要复制移动的对象太多,效率会低。
2.3 标记-整理算法
标记-整理算法(Mark-Compact)标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉存活对象区域的边界以外的内存区域。如下图所示:
标记-整理算法在标记-清除算法基础上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。但从上图可以看到,它对内存变动更频繁,需要整理移动所有存活对象的引用地址,在效率上比复制算法要差很多。
3、垃圾分代收集理论
3.1 为什么要使用分代收集理论?
收集器将JVM堆划分为不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域中存储。如果一个区域内的对象绝大多数都是朝生夕灭,难以熬过垃圾回收过程的话,那么回收这个区域的对象时应重点关注如何保留少量存活对象而不是去标记大量要回收的对象;如果一个区域内的对象绝大多数是难以被回收的对象,那虚拟机可以使用较低频率来回收这个区域。总之一句话:虚拟机根据对象特点将其分别存放到堆中不同的区域,每个区域由于其对象特点差异应“因地制宜”,采用针对性的垃圾回收策略。
结合前面介绍的三种垃圾回收算法,分代收集算法(Generational Collection)严格来说并不是一种思想或理论,而是融合上述3种基础的垃圾回收算法思想,而产生的针对不同情况所采用不同算法的一套组合拳。对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记 -整理算法来进行回收。
3.2 JVM中堆的分代
JVM堆是垃圾回收的主要区域,堆中按照对象存活周期的不同主要分为两个区域:新生代(Young Generation)和老年代(Old Generation),其中新生代又可以分为三个区域:Eden区、From Survivor区和To Survivor区,这几个分区在堆中的比例如下:
(1)Eden区
大多数情况下对象会在新生代的Eden区存储,当Eden区没有足够的内存空间进行分配时,虚拟机会发起一次Minor GC,Minor GC相比Major GC更频繁,回收速度也更快。通过Minor GC,Eden分区会被清空,Eden区绝大多数部分对象会被回收掉,而那些无需回收的存活下来的对象,将会进到Survivor的From区,如果From区不够则直接进入Old区。
(2)Survivor区
Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC时,会将Eden区和From区存活的对象放到Survivor的To区(如果To区不够,则直接进入 Old区),然后From区和To区互换,本次Minor GC的From区变成下一次Minor GC的To区,本次Minor GC的To区变成下一次Minor GC的From区,Survivor区就这两个分区,来回倒对象。
为什么需要Survivor区呢?不能直接从Eden区进入到老年代么?Survivor区的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历16次(可以通过参数设置) Minor GC还能在新生代中存活的对象,才会被送到老年代。
那为什么Survivor区仅有两个分区呢?其实Survivor分区中的垃圾回收算法就是上面介绍的标记-复制算法。
- 如果仅有1个分区:Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有一些是需要被清除的。问题来了,这时候我们怎么清除它们?在这种场景下,我们只能标记清除,而我们知道标记清除最大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。因为 Survivor 有2个区域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。这种机制最大的好处就是,整个过程中,永远有一个 Survivor space 是空的,另一个非空的 Survivor space 是无碎片的。
- 如果有多于2个分区:如果 Survivor 区再细分下去,每一块的空间就会比较小,容易导致 Survivor 区满,两块 Survivor 区可能是经过权衡之后的最佳方案。
(3)Old区
老年代占据堆中2/3的空间,只有在Major GC的时候才会清理老年代,每次GC都会触发“Stop-The-World”。由于老年代中对象存活率较高,因此老年代采用的一般是标记-整理算法。除了上面介绍的新生代中From区空间不够会直接将Eden区的对象进入老年代以外,还有以下几种情况也会进入老年代:
- 大对象:大对象指需要大量连续内存空间的对象,这部分对象不管是不是“朝生夕死”,都会直接进到老年代。这样做主要是为了避免在 Eden 区及2个 Survivor 区之间发生大量的内存复制。当你的系统有非常多“朝生夕死”的大对象时是不正常的;
- 长期存活对象:虚拟机给每个对象定义了一个对象年龄(Age)计数器。正常情况下对象会不断的在 Survivor 的 From 区与 To 区之间移动,对象在 Survivor 区中每经历一次 Minor GC,年龄就增加1岁。当年龄增加到一定值时(15)就会被转移到老年代,该值可以通过参数设置;
动态对象年龄:虚拟机并不总是要求对象年龄必须到15岁,才会放入老年区,如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间内存值的一半,年龄大于等于该年龄的对象就可以直接进去老年区。
3.3 空间分配担保
(1)什么是空间分配担保机制?
在发生Minor GC前,虚拟机必须检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果条件成立,那么这一次Minor GC是安全的会进行Minor GC;如果不成立,则虚拟机会判断是否开启HandlerPromotionFailure(是否允许空间担保,通过XX: HandlerPromotionFailure参数指定,true代表开启),如果开启,会继续检查老年代最大可用的连续内存空间是否大于历代晋升到老年代对象的平均大小,如果大于,则进行Minor GC;如果小于,或者没有开启空间担保,会进行一次Full GC。
(2)为什么要有空间分配担保机制
新生代中的From Survivor和To Survivor分区是采用的标记-复制算法,在复制算法下仅有一个Survivor分区作为轮换备份,因此当出现大量对象在Minor GC后依旧存活的情况,即在2.4.2 Eden分区里说的”Minor GC时,如果From区不够,则存活下来的对象由Eden区直接进入Old区”。空间分配担保机制,担保的就是对象从Eden区直接进入Old区时,Old区有足够的内存空间容纳这些对象,判断方法就是虚拟机会比较历代晋升到老年代对象的平均大小和当前Old区的剩余内存空间,如果大于直接进行Full GC,如果小于则允许对象由Eden区直接进入Old区。3.4 几种gc
3.4.1 Minor GC
又叫做Yong GC,是发生在新生代中的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。在进行Minor GC时,会将那些存活下来的对象进入From Survivor区,如果From Survivor区空间大小不够,会经过空间分配担保机制,来决定是进入老年代还是进行Full GC。
3.4.2 Major GC
Major GC概念上是针对Old区老年代进行垃圾收集的。很多时候将Major GC和Full GC会混淆。
3.4.3 Mixed GC
Mixed GC 是回收整个新生代和部分老年代的垃圾收集过程,g1收集器会有这种行为。
3.4.4 Full GC
Full GC是相对于partial gc(部分GC),而partial gc则包括minor gc、major gc和mixed gc。Full GC清理的是整个堆(包括新生代、老年代)以及方法区(JDK1.8之前是永久代,之后对应元空间MetaSpace)的数据。触发Full GC的条件有:
老年代空间不足:如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发Full GC;
- 永久代空间不足:在JDK1.8之前,永久代会存放要加载的类的相关信息,如果永久代空间不足,也会触发Full GC;
- Minor GC时空间担保机制:在Minor GC时,Eden区对象会进入From Survivor区,如果From Survivor区内存不够,会检查是否开启空间担保机制。如果开启,则虚拟机会比较历代晋升到老年代对象的平均大小和当前Old区的剩余内存空间,如果小于,则会执行Minor GC;如果大于,或者没有开启空间分配担保机制,则会进行Full GC;
-
4、常见的垃圾收集器
前面介绍的三种垃圾回收算法是内存回收的方法论,具体的垃圾收集器才是内存回收的实践者,或者说是垃圾回收算法的落地实现。针对新生代和老年代中对象的各自特点,不同分代宜采用不同的垃圾收集器,如下图所示,其中分界线上面的是新生代对应的垃圾收集器,分界线下面的是老年代对应的垃圾收集器,两个收集器之间有连线,说明它们可以配合使用:
4.1 Serial 收集器
Serial收集器是一个单线程收集器,单线程是指:不仅仅会使用一个 cpu或一条垃圾收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集的时候,必须暂停其他所有用户线程即“stop the world”,直到他收集结束。Serial收集器如下图所示,其中黄色的是用户线程,灰色的是gc线程:
说明: Serial收集器一般指的是新生代中的收集器(上图中的左半部分),老年代对应的收集器是Serial Old收集器;
- Serial收集器新生代垃圾收集算法采用的是标记-复制算法;
- Serial收集器的gc线程仅有一个;
- Serial收集器在进行gc回收时,会暂停用户线程,即Stop-The-World;
Serial收集器的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
4.2 ParNew 收集器
ParNew收集器可以看做是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、 收集算法、 Stop The World、 对象分配规则、 回收策略等都与Serial收集器完全一样。ParNew收集器如下图所示:
ParNew收集器存在的意义是:除Serial收集器外,目前只有ParNew收集器能与老年代的CMS收集器配合工作。但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。4.3 Parallel Scavenge 收集器
Parallel Scavenge收集器是基于标记-复制算法的新生代收集器,也是能够并行收集的多线程收集器。Parallel Scavenge收集器关注点是达到一个可控制的吞吐量,所谓吞吐量是指:处理器用于运行用户代码的时间与处理器总消耗时间的比值,即:吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 运行垃圾收集时间)。
Parallel Scavenge收集器如下图所示(跟ParNew收集器差不多):
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务,例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序等。4.4 Serial Old 收集器
Serial Old收集器是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法。Serial Old收集器主要用在以下两个场景:
在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用;
- 作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。
4.5 Parellel Old 收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,采用多线程进行收集,采用标记-整理算法,当注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。
Parallel Old收集器如下图所示:
4.6 CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。采用的算法是“标记-清除”,运作过程分为四个步骤:
- 初始标记:标记GC Roots 能够直接关联到达对象;
- 并发标记:进行GC Roots Tracing 的过程,即从GC Roots的直接关联对象开始遍历整个对象图的过程 ;
- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录;
- 并发清除:用标记清除算法清除对象。
其中初始标记、重新标记这两个步骤仍是 Stop The World。初始标记只是标记GC Roots能直接关联到的对象,速度很快; 并发标记阶段进行GC Roots tracing的过程。而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。 运行过程如下:
CMS收集器有如下特点:
- 针对老年代;
- 基于”标记-清除”算法(不进行压缩操作,产生内存碎片);
- 以获取最短回收停顿时间为目标;
- 并发收集、低停顿;
- 需要更多的内存。
CMS收集器的优点:
- 停顿时间短;
- 吞吐量大;
- 并发收集。
CMS收集器的缺点:
- 对CPU资源非常敏感;
- 无法收集浮动垃圾;
- 容易产生大量内存碎片。
CMS收集器使用场景:
- 与用户交互较多的场景;
- 希望系统停顿时间最短,注重服务响应速度的场景;
- 比如常见的Web应用。
4.7 g1 收集器
G1(Garbage-First)是JDK7才推出商用的收集器,G1收集器不同于以往的垃圾收集器,其思想从严格的分代回收转变为对Region的回收。以往对于Java堆区域的划分为:新生代和老年代,新生代又划分为 Eden区和 Survivor区,Survivor区又分为 from区和 to区。但是现在,G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆空间划分为多个大小相等的独立区域(Region),每个Region都可以成为 Eden空间、Survivor空间、老年代空间。这种思想上的转变和设计,使得G1可以面向堆内存任何部分来组成回收集来进行回收,衡量标准不再是它属于哪个分代,而是哪块内存存放的垃圾最多,回收收益最大,这就是G1收集器的 Mixed GC模式,即混合GC模式。
g1收集器对Java堆的划分如下:
g1垃圾回收器的工作流程如下:
- 初始标记(Initial Marking):这阶段仅仅只是标记GC Roots能直接关联到的对象并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的可用的Region中创建新对象,这阶段需要停顿线程,但是耗时很短。而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 并发标记(Concurrent Marking):从GC Roots开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这阶段耗时较长,但是可以与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。
- 筛选回收(Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。可以自由选择多个Region来构成回收集,然后把回收的那一部分Region中的存活对象==复制==到空的Region中,在对那些Region进行清空。
除了并发标记外,其余过程都要 Stop-The-World。
g1垃圾回收期器的优点:
- 能充分利用多CPU、多核环境下的硬件优势;
- 能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
- 不会产生内存碎片,有利于长时间运行;
- 除了追求低停顿处,还能建立可预测的停顿时间模型;
5、其他
5.1 Java中的引用类型
Java中将对象的引用类型强度从强到弱分为以下4种:
(1)强引用类型(Strongly Reference)
强引用类型就是传统意义上的引用,即类似Object obj = new Object()
这种引用关系。无论何时,只要强引用关系还存在,垃圾收集器就永远不会回收掉被强引用的对象。
(2)软引用类型(Soft Reference)
软引用是用来描述一些还有用,但非必须的对象。只被软引用关联的对象,在系统将要发生内存溢出前,会将这些对象进行第二次回收,如果第二次回收还是没有足够的内存才会抛出内存溢出的异常。
String str= new String("abc");
SoftReference<String> softRef = new SoftReference<String>(str);
(3)弱引用类型(Weak Reference)
弱引用也是用来描述那些非必须对象,但是强度比软引用类型更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否充足,都会回收掉只被弱引用关联的对象。
String str= new String("abc");
WeakReference<String> weakRef = new WeakReference<String>(str);
(4)虚引用类型(Phantom Reference)
一个对象是否有虚引用的关联,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象实例,为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
PhantomReference ref = new PhantomReference(str, queue);
四种引用对比:
引用类型 | 被垃圾回收时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般引用都是强引用 | JVM停止运行时终止 |
软引用 | 内存不足时 | 对象缓存 | 内存不足时终止 |
弱引用 | 垃圾回收时 | 对象缓存 | 垃圾回收运行后终止 |
虚引用 | UnKnown | 标记、哨兵 | UnKnown |
参考
咱们从头到尾说一次 Java 垃圾回收
【289期】面试官:说一下JVM常用垃圾回收器的特点、优劣势、使用场景和参数设置
JVM面试必问:G1垃圾回收器