当前位置:   article > 正文

JVM的GC机制

jvm的gc机制

一、GC简介

JAVA GC(Garbage Collection,垃圾回收)机制是区别C++的一个重要特征,C++需要开发者自己实现垃圾回收的逻辑,而JAVA开发者则只需要专注于业务开发,因为垃圾回收这件繁琐的事情JVM已经为我们代劳了,从这一点上来说,JAVA还是要做的比较完善一些。但这并不意味着我们不用去理解GC机制的原理,因为如果不了解其原理,可能会引发内存泄漏、频繁GC导致应用卡顿,甚至出现OOM等问题,因此我们需要深入理解其原理,才能编写出高性能的应用程序,解决性能瓶颈。

想要理解GC的原理,我们必须先理解JVM内存管理机制,因为这样我们才能知道回收哪些对象、什么时候回收以及怎么回收。这个内容之前写过一篇博客JVM内存模型和java内存模型

根据JVM规范,JVM把内存划分成了如下几个区域:

1.方法区(Method Area2.堆区(Heap3.虚拟机栈(VM Stack4.本地方法栈(Native Method Stack5.程序计数器(Program Counter Register
  • 1
  • 2
  • 3
  • 4
  • 5

二、GC机制

随着程序的运行,内存中的实例对象、变量等占据的内存越来越多,如果不及时进行回收,会降低程序运行效率,甚至引发系统异常。

在上面介绍的五个内存区域中,有3个是不需要进行垃圾回收的:本地方法栈、程序计数器、虚拟机栈。因为他们的生命周期是和线程同步的,随着线程的销毁,他们占用的内存会自动释放。所以,只有方法区和堆区需要进行垃圾回收,回收的对象就是那些不存在任何引用的对象。

如何分配内存

在这里插入图片描述

堆内存中对象分配的基本策略:
①对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。先把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果ServicorTo不够位置了就放到老年区),同时把这些对象的年龄+1;然后,清空Eden和ServicorFrom中的对象;最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区

Minor GC:新生代GC,指发生在新生代的垃圾收集动作,所有的Minor GC都会触发全世界的暂停(stop-the-world),停止应用程序的线程,不过这个过程非常短暂。
Major GC/Full GC:老年代GC,指发生在老年代的GC。

②大对象直接进入老年代

所谓的大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。可通过配置设置令操作设置值的对象直接在老年代分配。

这样做的目的避免在Eden区以及两个Survivor区之间发生大量的内存复制(新生代采用复制算法收集内存)。

③长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象的年龄设置为 1.对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)就将会被晋升到老年代中。年龄阈值可通过配置调整-XX:MaxTenuringThreshold参数。

④动态年龄判定
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold参数才能晋升到老年代。

如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到配置参数中要求的年龄。

⑤空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。

如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的。如果小于或者设置不允许冒险,这时要改为进行一次Full GC。

如何判断对象是否死亡

堆中⼏乎放着所有的对象实例,对堆垃圾回收前的第⼀步就是要判断哪些对象已经死亡(即不能再被任何途径使⽤的对象)。

①引用计数法
给对象中添加⼀个引⽤计数器,每当有⼀个地⽅引⽤它,计数器就加1;当引⽤失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使⽤的。
(但是,引用计数法很难解决对象之间相互循环引用的问题)

②可达性分析算法
通过一些列的称为GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots之间没有任何引用链相连,则证明这个对象是不可用的。

经典的引用计数算法有个明显的缺陷:当两个对象相互引用,但是二者都已经没有作用时,理应把它们都回收,但是由于它们相互引用,不符合垃圾回收的条件,所以就导致无法处理掉这一块内存区域。因此,Sun的JVM并没有采用这种算法,而是采用一个叫——根搜索算法,也叫可达性分析算法

强,软,弱,虚引用

⽆论是通过引⽤计数法判断对象引⽤数量,还是通过可达性分析法判断对象的引⽤链是否可达,判定对象的存活都与“引⽤”有关。

JDK1.2之前,Java中引⽤的定义很传统:如果reference类型的数据存储的数值代表的是另⼀块内存的起始地址,就称这块内存代表⼀个引⽤。

JDK1.2以后,Java对引⽤的概念进⾏了扩充,将引⽤分为强引⽤、软引⽤、弱引⽤、虚引⽤四种(引⽤强度逐渐减弱)

强引⽤(StrongReference)

强引用就是指类似于“Object obj = new Object()”这类的引用。被强引用关联的对象,垃圾回收器绝不会回收它。当内存空间不⾜时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终⽌,也不会靠随意回收具有强引⽤的对象来解决内存不⾜问题。

软引⽤(SoftReference)

被软引用关联的对象只有在内存空间不足时才会被回收。软引⽤可⽤来实现内存敏感的⾼速缓存。软引⽤可以和⼀个引⽤队列(ReferenceQueue)联合使⽤,如果软引⽤所引⽤的对象被垃圾回收,JAVA虚拟机就会把这个软引⽤加⼊到与之关联的引⽤队列中。在JDK1.2后,提供了SoftReference

弱引⽤(WeakReference)

弱引⽤与软引⽤的区别在于:只具有弱引⽤的对象拥有更短暂的⽣命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,⼀旦发现了只具有弱引⽤的对象,不管当前内存空间⾜够与否,都会回收它的内存。不过,由于垃圾回收器是⼀个优先级很低的线程, 因此不⼀定会很快发现那些只具有弱引⽤的对象。弱引⽤可以和⼀个引⽤队(ReferenceQueue)联合使⽤,如果弱引⽤所引⽤的对象被垃圾回收,Java虚拟机就会把这个弱引⽤加⼊到与之关联的引⽤队列中。

虚引⽤(PhantomReference)

"虚引⽤"顾名思义,就是形同虚设,与其他⼏种引⽤都不同,虚引⽤并不会决定对象的⽣命周期**。如果⼀个对象仅持有虚引⽤,那么它就和没有任何引⽤⼀样,在任何时候都可能被回收。为一个对象设置虚引用的唯一目的就是这个对象被垃圾回收器回收时收到一个系统通知**。在JDK1.2之后,提供了PhantomReference类来实现虚引用。

虚引⽤软引⽤和弱引⽤的⼀个区别在于:虚引⽤必须和引⽤队列(ReferenceQueue)联合使⽤。当垃圾回收器准备回收⼀个对象时,如果发现它还有虚引⽤,就会在回收对象的内存之前,把这个虚引⽤加⼊到与之关联的引⽤队列中。程序可以通过判断引⽤队列中是 否已经加⼊了虚引⽤,来了解被引⽤的对象是否将要被垃圾回收。程序如果发现某个虚引⽤已经被加⼊到引⽤队列,那么就可以在所引⽤的对象的内存被回收之前采取必要的⾏动。
(软引用是在垃圾回收之后把引用加入到引用队列中,虚引用是在垃圾回收之前把引用加入到引用队列中)

特别注意,在程序设计中⼀般很少使⽤弱引⽤与虚引⽤,使⽤软引⽤的情况较多,这是因为软引⽤可以加速JVM对垃圾内存的回收速度,可以维护系统的运⾏安全,防⽌内存溢出(OutOfMemory)等问题的产⽣。

判断不可达一定会被回收吗

即使在可达性分析算法判断不可达的对象,也并非是“非死不可的”。

如果对象在进行可达性分析后发现没有与GC Roots相连的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被调用过,finalize()方法都不会执行,该对象将会被回收。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列中,然后由Finalizer线程去执行。GC将会对F-Queue中的对象进行第二次标记,如果对象在finalize()方法中重新与引用链上的任何一个对象建立关联,那么在第二次标记时将会被移除“即将回收”的集合,否则该对象将会被回收。

GC Roots 有哪些

1.在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。

2.在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。

3.在方法区中常量引用的对象,譬如字符串常量池里的引用。

4.在本地方法栈中JNI(即通常所说的Native方法)引用的对象。

5.Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

6.所有被同步锁(synchronized关键字)持有的对象。

7.反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

GC算法

常见的GC(垃圾回收)算法:标记-清除、标记-复制和标记-整理。

①标记-清除算法
算法分为“标记”和“清除”阶段:⾸先标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象。它是最基础的收集算法,后续的算法都是对其不⾜进⾏改进得到。这种垃圾收集算法带来两个明显的问题:

  1. 效率问题,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量的增长而降低。

  2. 空间问题(标记清除后会产⽣⼤量不连续的碎⽚,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作)

②标记-复制算法
标记-复制算法简称“复制算法
为了解决效率问题,“复制”收集算法出现了。

它可以将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收。

优点:1.解决了标记-清除算法面对大量可回收对象时执行效率低的问题。2.避免了内存碎片。
缺点:将可用空间缩小为原来的一半,空间利用率较低。

新生代中就是采用这种算法,但新生代中的大部分对象“朝生夕灭”,熬不过第一轮收集。因此并不需要按照1:1的比例来划分新生代内存空间。
将新生代划分为一个Eden区和两个Survivor区,大小为8:1:1

每次分配内存时只使用Eden区和其中一个Survivor区。发生垃圾回收时,将Eden和Survior中仍然中存活的对象一次性复制到另外一块Survivor空间中,然后直接清理掉Eden和已经使用过的Survivor区。
在这里插入图片描述
③标记-整理算法
根据⽼年代的特点推出的⼀种标记算法,标记过程仍然与“标记-清除”算法⼀样,但后续步骤不是直接对可回收对象回收,⽽是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存。

(复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法)
在这里插入图片描述
优点:不会产生内存碎片。
缺点:移动存活对象并更新引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行。(即Stop the world)

分代收集算法
当前虚拟机的垃圾收集都采⽤分代收集算法,根据对象存活周期的不同将内存分为⼏块,⼀般将java堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择标记-复制算法,只需要付出少量存活对象的复制成本就可以完成每次垃圾收集。

⽼年代 的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择 “ 标记 - 清除 ” 或 “ 标记 - 整理 ” 算法进⾏垃圾收集。

三、垃圾收集器

在JVM中,GC是由垃圾回收器来执行,所以,在实际应用场景中,我们需要选择合适的垃圾收集器,垃圾收集器主要有:Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old、CMS、G1,下面我们介绍一下垃圾收集器。

①Serial收集器

串行收集器, Serial GC是最古老也是最基本的收集器,但是现在依然广泛使用,JAVA SE5和JAVA SE6中客户端虚拟机采用的默认配置。比较适合于只有一个处理器的系统。在串行处理器中minor和major GC过程都是用一个线程进行回收的。它的最大特点是在进行垃圾回收时,需要对所有正在执行的线程暂停(stop the world),对于有些应用是难以接受的,但是如果应用的实时性要求不是那么高,只要停顿的时间控制在N毫秒之内,大多数应用还是可以接受的,而且事实上,它并没有让我们失望,几十毫秒的停顿,对于我们客户机是完全可以接受的,该收集器适用于单CPU、新生代空间较小且对暂停时间要求不是特别高的应用上,是client级别的默认GC方式。
它是一款一个新生代、单线程的收集器,采用标记-复制算法。它在进行垃圾回收时,必须暂停所有用户线程,直到它收集结束。(stop the world)

②ParNew收集器

ParNew收集器是一个新生代、多线程的收集器,采用标记-复制算法。基本和Serial GC一样,但本质区别是加入了多线程机制,提高了效率,这样它就可以被用于服务端上(server)。只有Serial和ParNew收集器能与CMS配合工作。ParNew收集器是激活CMS后的默认新生代收集器。

③Parallel Scavenge收集器

并行收集器,Parallel Scavenge收集器是一款新生代收集器,基于标记-复制算法实现的。能够进行并行收集的多线程收集器。适用于多CPU、对暂停时间要求较短的应用,是server级别的默认GC方式。

④Serial Old收集器

Serial Old收集器:一个老年代、单线程的收集器,采用标记-整理算法。它在进行垃圾回收时,必须暂停所有用户线程,直到它收集结束。(stop the world)

⑤Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支持多线程并发收集(多个GC线程),基于标记-整理算法实现。

⑥CMS收集器

CMS收集器是老年代收集器,基于标记-清除算法实现的。
该收集器的目标是解决Serial GC停顿的问题,以达到最短回收时间。常见的B/S架构的应用就适合这种收集器,因为其高并发、高响应的特点。

⑦G1收集器

G1收集器:新生代+老年代(将Java堆划分成多个Region),面向全堆的收集器,不需要其他新生代收集器的配合工作。
相比CMS收集器有不少改进,首先,基于标记-压缩算法,不会产生内存碎片,其次可以比较精确的控制停顿。

⑧RTSJ垃圾收集器

RTSJ垃圾收集器,用于Java实时编程。

搭配:
①新生代Serial+老年代Serial Old(是客户端模式下常用的收集器)
②新生代Parallel Scavenge+老年代Parallel Old(适用于后台运算而不需要太多交互的分析任务)
③新生代ParNew+老年代CMS(是服务端模式下常用的收集器,Parallel Scavenge无法与CMS配合)

下面详解介绍一下CMS和G1收集器
CMS收集器
CMS(Concurrent Mark Sweep)收集器是⼀种以获取最短回收停顿时间为⽬标的收集器。它⽽⾮常符合在注重⽤户体验的应⽤上使⽤。

CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第⼀款真正意义上的并发收集器,它第⼀次实现了让垃圾收集线程与⽤户线程(基本上)同时⼯作。

从名字中的Mark Sweep这两个词可以看出,CMS收集器是⼀种 “标记-清除”算法实现的,它的运作过程相⽐于前⾯⼏种垃圾收集器来说更加复杂⼀些。整个过程分为四个步骤:(初始标记和重新标记这两步仍然要stop the world)

stop the world:除垃圾收集器线程之外的线程都被挂起。

①初始标记: 暂停用户线程,并标记下GC Roots能直接关联到的对象,速度很快 ;

②并发标记: 同时开启GC和⽤户线程,⽤⼀个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为⽤户线程可能会不断的更新引⽤域,所以GC线程⽆法保证可达性分析的实时性。所以这个算法⾥会跟踪记录这些发⽣引⽤更新的地⽅。

从GC Root开始对堆中的对象进行可达性分析,找出存活的对象。

(书本:并发标记阶段就是进行GC Roots Tracing的过程)

③重新标记: 暂停用户线程,重新标记阶段就是为了修正并发标记期间因为⽤户程序继续运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,远远⽐并发标记阶段时间短。

④并发清除: 同时开启⽤户线程和GC线程,清理掉在标记阶段判断的已经死亡的对象。

CMS收集器的优缺点:

从它的名字就可以看出它是⼀款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下⾯三个明显的缺点:

①对CPU资源敏感

在并发阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。

②⽆法处理浮动垃圾

CMS在并发清理阶段用户线程还在运行, 还会产生新的垃圾,这一部分垃圾产生在标记过程之后,CMS无法再当次过程中处理,所以只有等到下次gc时候在清理掉,这一部分垃圾就称作“浮动垃圾” 。

③它使⽤的回收算法–“标记-清除”算法会导致收集结束时会有⼤量空间碎⽚产⽣。

空间碎片太多的时候,将会给大对象的分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象的,只能提前触发 full gc。

为了解决这个问题,CMS提供了一个开关参数,用于在CMS顶不住要进行full gc的时候开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片没有了,但是停顿的时间变长了。

G1收集器
G1 (Garbage-First)是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器。以极⾼概率满⾜GC停顿时间要求的同时,还具备⾼吞吐量性能特征。
被视为JDK1.7中HotSpot虚拟机的⼀个重要进化特征。

G1收集器采用“标记-复制”和“标记-整理”。从整体上看是基于“标记-整理”,从局部看,两个region之间是“标记-复制”它具备⼀下特点:

①并⾏与并发:G1能充分利⽤CPU、多核环境下的硬件优势,使⽤多个CPU(CPU或者CPU核⼼)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执⾏的GC动作,G1收集器仍然可以通过并发的⽅式让java程序继续执⾏。

②分代收集:虽然G1可以不需要其他收集器配合就能独⽴管理整个GC堆,但是还是保留了分代的概念。

③空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

④可预测的停顿:这是G1相对于CMS的另⼀个⼤优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建⽴可预测的停顿时间模型,能让使⽤者明确指定在⼀个⻓度为M毫秒的时间⽚段内,消耗在垃圾收集上的时间不得超过N毫秒。

G1收集器的运作⼤致分为以下⼏个步骤:

①初始标记

暂停用户线程,初始标记只是标记下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。

②并发标记

同时开启用户线程和GC线程,并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活对象,这阶段耗时较长,但可与用户程序并发执行。

③最终标记

暂停用户线程,最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,这阶段需要停顿线程,但是可以并行执行。

④筛选回收

暂停用户线程,筛选阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

[Regin:G1收集器将整个Java堆划分为多个大小相等的独立区域(Regin)]

G1收集器在后台维护了⼀个优先列表,每次根据允许的收集时间,优先选择回收价值最⼤的Region(这也就是它的名字Garbage-First的由来)。这种使⽤Region划分内存空间以及有优先级的区域回收⽅式,保证了GF收集器在有限时间内可以尽可能⾼的收集效率(把内存化整为零)。

筛选回收:

对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉个旧的Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

G1收集器只有并发标记不会stop the world

为什么CMS采用“标记-清除”算法而不采用“标记-整理”算法
因为CMS作为第一款实现用户线程和收集线程并发执行的收集器!当时的设计理念是减少停顿时间,最好是能并发执行!但是问题来了,如要用户线程也在执行,那么就不能轻易的改变堆中对象的内存地址!不然会导致用户线程无法定位引用对象,从而无法正常运行!而标记整理算法和标记复制算法都会移动存活的对象,这就与上面的策略不符!因此CMS采用的是标记清理算法!

JDK1.8默认的垃圾回收器
默认:UseParallelGC
UseParallelGC 即 Parallel Scavenge + Parallel Old

UseParallelGC:并行收集器,同时运行在多个cpu之间,物理上的并行收集器,跟上面的收集器一样也是独占式的,但是它最大化的提高程序吞吐量,同时缩短程序停顿时间,另外它不能与CMS收集器配合工作。

为什么新生代和老年代要采用不同的回收算法
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就许选择“标记-复制”算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记–清除”、“标记整理”算法来进行回收。

标记复制算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。

垃圾回收怎么解决跨代引用的问题
记忆集(Remembered Set):一种用于记录从非收集区域指向收集区域指针集合的抽象数据结构,在对象层面来说就是非收集区域对象对收集区域对象的引用的记录。

记忆集存放在收集区域,比如在新生代里面存放着老年代对新生代对象的每一个引用。这样在收集新生代的时候,我们就可以根据记忆集知道哪些对象被老年代对象所引用,不能回收,这就解决了跨代引用的问题。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/喵喵爱编程/article/detail/881396
推荐阅读
相关标签
  

闽ICP备14008679号