• 欢迎来到本博客,希望可以y一起学习与分享

JVM-GC垃圾回收器

Java benz 6个月前 (03-17) 16次浏览 0个评论 扫描二维码
文章目录[隐藏]

JVM很重要。尤其是GC算法。

程序计数器、虚拟机栈、本地方法栈。这几个区域完全不用管回收问题,因为方法结束或者线程结束的时候他们所占用的内存就自然跟着一起释放了,3个区域随线程而生,随线程而灭。所以我们只需要管堆和方法区。尤其是堆,因为一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,这部分内存的分配和垃圾回收都是动态的。

垃圾的判断算法

垃圾的标准
没有被其它对象引用。

引用计数法(reference-counting)

每个对象有一个引用计数器,当对象被引用一次则计数器加1,当对象引用失效一次则计数器减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。
优点:效率高
缺点:无法解决循环引用问题,导致内存泄漏。

可达性算法(GC Roots Tracing)

从GC Roots作为起点开始搜索,那么整个连通图中的对象便都是活对象,对于GC Roots无法到达的对象便成了垃圾回收的对象,随时可被GC回收。

可以作为GC Roots的对象包括以下几点

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中的类静态属性引用的对象或者常量引用的对象。
  • 本地方法栈中JNI(就是native方法)引用的对象。
  • 活跃线程的引用对象。

参考:java中垃圾回收机制中的引用计数法和可达性分析法
注意:Java虚拟机不使用引用计数法GC

垃圾回收算法

标记-清除法

思想:标记所有的可达对象(存在引用的对象),则未被标记的对象就是不存在引用的垃圾对象,GC时清除所有未被标记的对象

GC过程  标记清除法的GC时经历标记 + 清除两个过程,先标记,后清除

优点:只存在循环引用不存在其他引用的对象不会被标记,解决了循环引用问题

缺点:可能会产生空间碎片(不连续的内存空间),不连续的内存空间在内存分配时的工作效率低于连续的内存空间,尤其是对大对象的的内存分配

相关概念:空间碎片:不连续的内存空间

图解:

复制算法

思想:将内存空间分为两块相同的存储空间,每次只使用一块,GC时,将正在使用的内存中的存活对象复制到另一块存储空间中,然后清除正在使用的空间的所有对象

GC过程:先复制,再清除

优点:存活对象相对少时,效率很高(因为需要复制的对象少),存活对象复制到另一空间时,解决了空间碎片问题

缺点:系统内存只能使用一半的内存空间,而且如果存活对象相对多的话,比较耗时

图解

应用场景:java新生代串行垃圾回收器

新生代分为eden空间、from空间和to空间3个部分,其中from和to空间是两块相同的空间,同一时间只使用其中一块空间,另一空间用于GC时存放复制的存活对象,from和to空间也称为survivor空间,用于存放未被回收的对象。

GC过程:在垃圾回收时,eden空间中存活的对象会被复制到未使用的survivor空间中(图中的to),正在使用的survivor空间(图中的from)中的年轻对象也会被复制到to空间中(大对象或者老年对象会直接进入老年代,如果to空间已满,则对象也会进入老年代)。此时eden和from空间中剩余对象就是垃圾对象,直接清空,to空间则存放此次回收后存活下来的对象。

优点:保证了内存空间的连续性,又避免了大量的空间浪费

相关概念:

新生代对象:刚刚创建或者经历垃圾回收次数不多的对象。

老年代对象:经历多次垃圾回收依然存活的对象。

图解:

注意:复制算法比较适用于新生代。因为在新生代中,垃圾对象通常会多于存活对象

标记-整理法(标记清除压缩法)

思想:标记压缩法是对标记清除法的优化,所以也叫标记清除压缩法。和标记清除法一样,先标记所有的可达对象(存在引用的对象),不同的是,标记完成后并不是直接清除未标记的垃圾对象,而是将所有的被标记的对象(即存活对象)压缩到内存空间的一端后在清理边界外所有的空间。

GC过程:分为标记+压缩 + 清除三个步骤

优点:解决了标记清除法带来的空间碎片问题,又不需要折损可使用空间(复制算法折损了可使用空间)

图解:

应用场景:老年代的回收算法

新生代存活对象少,垃圾对象多,所以使用复制算法效率高(要复制的对象少),老年代刚好相反,存活对象多,垃圾对象少,复制算法不适用,使用标记压缩法(个人感觉是一个排除法,复制算法不适用,标记清除法产生空间碎片,也不适用,排除一下,使用标记压缩法)

分代算法

思想:将内存空间根据对象的特点不同进行划分,选择合适的垃圾回收算法,以提高垃圾回收的效率。

GC过程:因为不同的对象使用的算法不一致,所以GC的过程也不一样,例如,新生代使用复制算法,老年代使用标记压缩法

优点:提高了垃圾回收的效率

应用场景:分代的思想被现有的虚拟机广泛使用,几乎所有的垃圾回收器都区分新生代和老年代。

卡表:

概念及其意义:对于新生代和老年代来说,通常新生代回收的频率很高,但是每次回收的时间都很短,而老年代回收的频率比较低,但是被消耗很多的时间。为了支持高频率的新生代回收,虚拟机可能使用一种叫做卡表的数据结构,卡表为一个比特位集合,每一个比特位可以用来表示老年代的某一区域中的所有对象是否持有新生代对象的引用,

这样一来,新生代GC时,可以不用花大量时间扫描所有老年代对象,来确定每一个对象的引用关系,而可以先扫描卡表,只有当卡表的标记为1时,才需要扫描给定区域的老年代对象,而卡表为0的所在区域的老年代对象,一定不含有新生代对象的引用。

图解:

分区算法

思想:将整个堆空间划分为连续的不同小区间,每一个小区间都独立使用,独立回收。

优点:可以控制一次回收多少个小区间,通常,相同的条件下,堆空间越大,一次GC所需的时间就越长,从而产生的停顿时间就越长。为了更好的控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而减少一个GC的停顿时间。

GC的分类

Minor GC:清理年轻代垃圾。
Full GC:是清理整个堆空间—包括年轻代和老年代、永久代(如果有的话)。
Major GC:通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC。如果是old GC,那么只清理老年代。

轻代(新生代)、老年代、永久代

这是jvm 堆内存结构图:

仔细的你发现了 图中有些分数8/10和1/10,这是默认配置下各个代内存分配比例。

举个栗子:

假如总heap max分配1200M,那么年轻代占用1/3就是400M,老年代占2/3就是800M。

Eden占年轻代的8/10就是320M。Survivor占年轻代的2/10就是80M,from和to各占40M。
年轻代
也叫新生代,顾名思义,主要是用来存放新生的对象。新生代又细分为 Eden区、SurvivorFrom区、SurvivorTo区。

新创建的对象都会被分配到Eden区(如果该对象占用内存非常大,则直接分配到老年代区), 当Eden区内存不够的时候就会触发MinorGC(Survivor满不会引发MinorGC,而是将对象移动到老年代中),

在Minor GC开始的时候,对象只会存在于Eden区和Survivor from区,Survivor to区是空的。

Minor GC操作后,Eden区如果仍然存活(判断的标准是被引用了,通过GC root进行可达性判断)的对象,将会被移到Survivor To区。而From区中,对象在Survivor区中每熬过一次Minor GC,年龄就会+1岁,当年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认是15)的对象会被移动到年老代中,否则对象会被复制到“To”区。经过这次GC后,Eden区和From区已经被清空。

“From”区和“To”区互换角色,原Survivor To成为下一次GC时的Survivor From区, 总之,GC后,都会保证Survivor To区是空的。

奇怪为什么有 From和To,2块区域?
这就要说到新生代Minor GC的算法了:复制算法

把内存区域分为两块,每次使用一块,GC的时候把一块中的内容移动到另一块中,原始内存中的对象就可以被回收了,

优点是避免内存碎片。

老年代
随着Minor GC的持续进行,老年代中对象也会持续增长,导致老年代的空间也会不够用,最终会执行Major GC(MajorGC 的速度比 Minor GC 慢很多很多,据说10倍左右)。Major GC使用的算法是:标记清除(回收)算法或者标记压缩算法

标记清除(回收):1. 首先会从GC root进行遍历,把可达对象(存过的对象)打标记

2. 再从GC root二次遍历,将没有被打上标记的对象清除掉。

优点:老年代对象一般是比较稳定的,相比复制算法,不需要复制大量对象。之所以将所有对象扫描2次,看似比较消耗时间,其实不然,是节省了时间。举个栗子,数组 1,2,3,4,5,6。删除2,3,4,如果每次删除一个数字,那么5,6要移动3次,如果删除1次,那么5,6只需移动1次。

缺点:这种方式需要中断其他线程(STW),相比复制算法,可能产生内存碎片。

标记压缩:和标记清除算法基本相同,不同的就是,在清除完成之后,会把存活的对象向内存的一边进行压缩,这样就可以解决内存碎片问题。

当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。

永久代(元空间)
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间,Metaspace)的区域所取代。

值得注意的是:元空间并不在虚拟机中,而是使用本地内存(之前,永久代是在jvm中)。这样,解决了以前永久代的OOM问题,元数据和class对象存在永久代中,容易出现性能问题和内存溢出,毕竟是和老年代共享堆空间。java8后,永久代升级为元空间独立后,也降低了老年代GC的复杂度。

对象如何晋升到老年代

1、经历一定Minor GC 次数依然存活的对象。
2、Survivor区中存放不下的对象。

堆内存调优

-XX:SurvivorRatio:Eden和Survivor的比值,默认8:1:1。
-XX:NewRatio:老年代和年轻代内存大小的比例。
-XX:MaxTenurningThreshold:对象从年轻代晋升到老年代经过GC次数的最大阈值。

触发Full GC的条件

Full GC相对于Minor GC来说,停止用户线程的STW(stop the world)时间过长,至少慢10倍以上,所以要尽量避免,首先说一下Full GC可能产生的原因,接着给出排查方法以及解决策略。

1、System.gc()方法的调用

在代码中调用System.gc()方法会建议JVM进行Full GC,但是注意这只是建议,JVM执行不执行是另外一回事儿,不过在大多数情况下会增加Full GC的次数,导致系统性能下降,一般建议不要手动进行此方法的调用,可以通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

2、老年代(Tenured Gen)空间不足
在Survivor区域的对象满足晋升到老年代的条件时,晋升进入老年代的对象大小大于老年代的可用内存,这个时候会触发Full GC。

3、Metaspace区内存达到阈值
从JDK8开始,永久代(PermGen)的概念被废弃掉了,取而代之的是一个称为Metaspace的存储空间。Metaspace使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace的大小只与本地内存大小有关。-XX:MetaspaceSize=21810376B(约为20.8MB)超过这个值就会引发Full GC,这个值不是固定的,是会随着JVM的运行进行动态调整的,与此相关的参数还有多个,详细情况请参考这篇文章jdk8 Metaspace 调优

4、统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间
Survivor区域对象晋升到老年代有两种情况:

一种是给每个对象定义一个对象计数器,如果对象在Eden区域出生,并且经过了第一次GC,那么就将他的年龄设置为1,在Survivor区域的对象每熬过一次GC,年龄计数器加一,等到到达默认值15时,就会被移动到老年代中,默认值可以通过-XX:MaxTenuringThreshold来设置。
另外一种情况是如果JVM发现Survivor区域中的相同年龄的对象占到所有对象的一半以上时,就会将大于这个年龄的对象移动到老年代,在这批对象在统计后发现可以晋升到老年代,但是发现老年代没有足够的空间来放置这些对象,这就会引起Full GC。

5、堆中产生大对象超过阈值
这个参数可以通过-XX:PretenureSizeThreshold进行设定,大对象或者长期存活的对象进入老年代,典型的大对象就是很长的字符串或者数组,它们在被创建后会直接进入老年代,虽然可能新生代中的Eden区域可以放置这个对象,在要放置的时候JVM如果发现老年代的空间不足时,会触发GC。

6、老年代连续空间不足

JVM如果判断老年代没有做足够的连续空间来放置大对象,那么就会引起Full GC,例如老年代可用空间大小为200K,但不是连续的,连续内存只要100K,而晋升到老年代的对象大小为120K,由于120>100的连续空间,所以就会触发Full GC。

7、CMS GC时出现promotion failed和concurrent mode failure
这个原因引发的Full GC可以参考这篇文章,下面也摘抄自这篇文章:JVM 调优 —— GC 长时间停顿问题及解决方法

提升失败(promotion failed),在 Minor GC 过程中,Survivor Unused 可能不足以容纳 Eden 和另一个 Survivor 中的存活对象, 那么多余的将被移到老年代, 称为过早提升(Premature Promotion)。 这会导致老年代中短期存活对象的增长, 可能会引发严重的性能问题。 再进一步, 如果老年代满了, Minor GC 后会进行 Full GC, 这将导致遍历整个堆, 称为提升失败(Promotion Failure)。

在 CMS 启动过程中,新生代提升速度过快,老年代收集速度赶不上新生代提升速度。在 CMS 启动过程中,老年代碎片化严重,无法容纳新生代提升上来的大对象,这是因为CMS采用标记清理,会产生连续空间不足的情况,这也是CMS的缺点

总结
可以发现其实堆内存的Full GC一般都是两个原因引起的,要么是老年代内存过小,要么是老年代连续内存过小。无非是这两点,而元数据区Metaspace引发的Full GC可能是阈值引起的,详细原因还是建议参考其他文章。

Stop-the-World与Safepoint

Stop-the-World[GC用]

1、JVM由于要执行GC而停止应用程序的执行

2、任何一种GC算法中都会发生

3、多数GC优化都可以通过减少Stop-the-World发生的时间来提高程序性能,达到高吞吐,低停顿的特点

Safepoint[可达性分析用]

在可达性算法的可达性分析中,分析对象的引用必须在一个快照点进行,在这个点所有的线程都被冻结。不能出现在分析过程中对象的引用关系还产生变化的情况。需要确保程序具有安全性,因此设置了安全点。

一旦GC发生,都让所有的线程跑到最新的安全点,再停顿下来,如果有没到达的安全点的,就恢复线程,等待到达后再进行处理

1、分析过程中对象引用关系不会发生变化的点

2、产生SafaPoint的地方:方法调用,循环跳转,异常跳转等

3、安全点数量适中

垃圾回收器

收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。

说明:如果两个收集器之间存在连线说明他们之间可以搭配使用。

Serial 收集器

复制算法,适用于 年轻代
这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。
对于交互性较强的应用而言,这种垃圾收集器是不能够接受的。
一般在Javaweb应用中是不会采用该收集器的。

在程序运行参数中添加2个参数,如下:

  • -XX:+UseSerialGC:指定年轻代和老年代都使用串行垃圾收集器
  • -XX:+PrintGCDetails:打印垃圾回收的详细信息

ParNew 收集器

复制算法,适用于 年轻代
可以认为是 Serial 收集器的多线程版本。

  • -XX:+UseParallelGC
    年轻代使用ParallelGC垃圾回收器,老年代使用串行回收器。
  • -XX:+UseParallelOldGC
    年轻代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器。
  • -XX:MaxGCPauseMillis
    设置大的垃圾收集时的停顿时间,单位为毫秒 需要注意的时,ParallelGC为了达到设置的停顿时间,可能会调整堆大小或其他的参数,如果堆的大小 设置的较小,就会导致GC工作变得很频繁,反而可能会影响到性能。 该参数使用需谨慎。
  • -XX:GCTimeRatio
    设置垃圾回收时间占程序运行时间的百分比,公式为1/(1+n)。 它的值为0~100之间的数字,默认值为99,也就是垃圾回收时间不能超过1% -XX:UseAdaptiveSizePolicy 自适应GC模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的 平衡。 一般用于,手动调整参数比较困难的场景,让收集器自动进行调整。

Parallel Scavenge 收集器

复制算法,适用于 年轻代
这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。
CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。
作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。
ParallelGC收集器工作机制和ParNewGC收集器一样,只是在此基础之上,新增了两个和系统吞吐量相关的参数, 使得其使用起来更加的灵活和高效。
相关参数如下:

  • -XX:+UseParallelGC
    年轻代使用ParallelGC垃圾回收器,老年代使用串行回收器。
  • -XX:+UseParallelOldGC
    年轻代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器。
  • -XX:MaxGCPauseMillis
    设置大的垃圾收集时的停顿时间,单位为毫秒 需要注意的时,ParallelGC为了达到设置的停顿时间,可能会调整堆大小或其他的参数,如果堆的大小 设置的较小,就会导致GC工作变得很频繁,反而可能会影响到性能。 该参数使用需谨慎。
  • -XX:GCTimeRatio
    设置垃圾回收时间占程序运行时间的百分比,公式为1/(1+n)。 它的值为0~100之间的数字,默认值为99,也就是垃圾回收时间不能超过1%
  • -XX:UseAdaptiveSizePolicy
    自适应GC模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的 平衡。 一般用于,手动调整参数比较困难的场景,让收集器自动进行调整。

Serial Old 收集器

标记 – 整理算法,适用于 老年代
收集器的老年代版本,单线程,使用 标记 – 整理。

在程序运行参数中添加2个参数,如下:

  • -XX:+UseSerialOldGC:指定年轻代和老年代都使用串行垃圾收集器
  • -XX:+PrintGCDetails:打印垃圾回收的详细信息

Parallel Old 收集器

标记 – 整理算法,适用于 老年代
Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记 – 整理算法。

  • -XX:+UseParallelOldGC
    年轻代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器。
  • -XX:MaxGCPauseMillis
    设置大的垃圾收集时的停顿时间,单位为毫秒 需要注意的时,ParallelGC为了达到设置的停顿时间,可能会调整堆大小或其他的参数,如果堆的大小 设置的较小,就会导致GC工作变得很频繁,反而可能会影响到性能。 该参数使用需谨慎。
  • -XX:GCTimeRatio
    设置垃圾回收时间占程序运行时间的百分比,公式为1/(1+n)。 它的值为0~100之间的数字,默认值为99,也就是垃圾回收时间不能超过1%
  • -XX:UseAdaptiveSizePolicy
    自适应GC模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的 平衡。 一般用于,手动调整参数比较困难的场景,让收集器自动进行调整。

CMS 收集器

标记 – 清除算法,适用于 老年代
CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 – 清除 算法实现。
参数
-XX:+UseConcMarkSweepGC进行设置。

运作步骤:

  1. 初始化标记(CMS-initial-mark) ,标记root,会导致stw;
  2. 并发标记(CMS-concurrent-mark),与用户线程同时运行;
  3. 预清理(CMS-concurrent-preclean),与用户线程同时运行;
  4. 重新标记(CMS-remark) ,会导致stw;
  5. 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
  6. 调整堆大小,设置CMS在清理之后进行内存压缩,目的是清理内存中的碎片;
  7. 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;


缺点:对 CPU 资源敏感、无法收集浮动垃圾、标记- 清除 算法带来的空间碎片

G1 收集器

不区分代
G1垃圾收集器是在jdk1.7中正式使用的全新的垃圾收集器,oracle官方计划在jdk9中将G1变成默认的垃圾收集器, 以替代CMS。 G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:

  1. 第一步,开启G1垃圾收集器
  2. 第二步,设置堆的大内存
  3. 第三步,设置大的停顿时间


G1中提供了三种模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,在不同的条件下被触发。
优点:并行与并发、分代收集、空间整合、可预测停顿。
G1垃圾收集器相对比其他收集器而言,大的区别在于它取消了年轻代、老年代的物理划分,取而代之的是将堆 划分为若干个区域(Region),这些区域中包含了有逻辑上的年轻代、老年代区域。

GC面试题

finalize()

简单来说:给对象最后一次重生的机会
finalize()是Object中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它finalize()方法,让此对象处理它生前的最后事情(这个对象可以趁这个时机挣脱死亡的命运)。要明白这个问题,先看一下虚拟机是如何判断一个对象该死的。

判定死亡
“GC ROOTS”定义:GC管理的主要区域是Java堆,一般情况下只针对堆进行垃圾回收。方法区、栈和本地方法区不被GC所管理,因而选择这些区域内的对象作为GC roots,被GC roots引用的对象不被GC回收。

“GC ROOTS”也可以看做是引用链的最顶级。

Java采用可达性分析算法来判定一个对象是否死期已到。Java中以一系列”GC  Roots”对象作为起点,如果一个对象的引用链可以最终追溯到”GC  Roots”对象,那就天下太平。
否则如果只是A对象引用B,B对象又引用A,A,B引用链均为能达到”GC  Roots”的话,那它俩将会被虚拟机宣判符合死亡条件,具有被垃圾回收器回收的资格。

最后的救赎
上面提到了判断死亡的依据,但被判断死亡后,还有生还的机会。
如何自我救赎:
1.对象覆写了finalize()方法(这样在被判死后才会调用此方法,才有机会做最后的救赎);
2.在finalize()方法中重新引用到”GC  Roots”链上(如把当前对象的引用this赋值给某对象的类变量/成员变量,重新建立可达的引用).

需要注意

finalize()只会在对象内存回收前被调用一次(The finalize method is never invoked more than once by a Java virtual machine for any given object. );
finalize()的调用具有不确定行,只保证方法会调用,但不保证方法里的任务会被执行完(比如一个对象手脚不够利索,磨磨叽叽,还在自救的过程中,被杀死回收了)。

finalize()的作用

虽然以上以对象救赎举例,但finalize()的作用往往被认为是用来做最后的资源回收。
基于在自我救赎中的表现来看,此方法有很大的不确定性(不保证方法中的任务执行完)而且运行代价较高。所以用来回收资源也不会有什么好的表现。

综上:finalize()方法并没有什么鸟用。

至于为什么会存在这样一个鸡肋的方法:书中说“它不是C/C++中的析构函数,而是Java刚诞生时为了使C/C++程序员更容易接受它所做出的一个妥协”。

强引用、弱引用、软引用和虚引用有什么用

强引用

  • 最普通的强引用:Object obj = new Object();,Java默认创建的对象都是强引用类型。
  • 抛出OutOfMemoryError终止程序也不会回收具有强引用的对象。
  • 通过将对象设置为null来弱化引用,使其被回收

软引用

  • 对象处在有用但非必须的状态。
  • 只有当内存空间不足时,GC会回收该引用的对象内存。
  • 可以用来实现高速缓存。

弱引用

  • 非必须的对象,比软引用更弱一些。
  • GC时会被回收。
  • 被回收的概率也不大,因为GC线程优先级比较低。
  • 适用于引用偶尔被使用且不影响垃圾收集的对象。

虚引用

  • 不会决定对象的生命周期。
  • 任何时候都可能被GC回收。
  • 跟踪对象被垃圾收集器回收的活动,起哨兵作用。
  • 必须和引用队列ReferenceQueue联合使用。

总结

引用类型 被回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 在内存不足时 对象缓存 内存不足时终止
弱引用 在垃圾回收时 对象缓存 GC运行后终止
虚引用 Unknown 标记、哨兵 Unknown

应用
强引用是Java创建对象的默认引用。

弱引用也可作为缓存。

关于虚引用作为哨兵,我没找到使用方法,可能是Java虚拟机在某些场景下的使用。

软引用实现高速缓存
参考:Java的四种引用类型的回收时机以及使用场景
一般创建的对象都是强引用类型,在被销毁之后就等待被回收,即使是最近多次被访问,也会创建多次,所以可以在另一个地方将这个对象保存下来,从而提高速度。

所以可以使用软引用作为缓存将其保存到一个Map类中(注意,键是一个软引用,而不是值)

这里作为缓存是为了避免反复创建和销毁对象,与Redis这种数据从硬盘到内存的缓存是不一样的


文章 JVM-GC垃圾回收器 转载需要注明出处
喜欢 (0)

您必须 登录 才能发表评论!