【JVM第四章】JVM-垃圾收集和内存分配

1 判断对象是否存活

1.1 引用计数法
  • 引用计数法:在对象中添加一个引用计数器,每个地方引用它时+1,引用失效-1;任何时刻计数器为零的对象就是不可能再使用的。
  • A引用B,B引用A,相互循环引用,如果使用引用计数器算法,这两个对象永远不会被回收,但是极大可能是无效对象,所以在JAVA领域中,主流的虚拟机都没有选用它来管理内存
1.2 可达性分析算法
  • 现在主流的虚拟机都是通过可达性分析算法来判定对象是否存活
  • 如果某个对象到GC Roots间没有任何引用链相连,就是GC Roots到达这个对象不可达,表明这个对象是不可以再被使用的
    在这里插入图片描述
1.2.1 可达性分析算法 - GCRoots

GC Roots的对象可以包括以下几种:(4+1个答案)

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,比如各个线程被调用的方法堆栈中使用的参数、局部变量、临时变量等
  • 在方法区中类静态属性引用的对象,比如java类的引用类型静态变量。(static JvmGC JvmGC)
  • 在方法区中常量引用的对象,比如字符串常量池里面的引用
  • Java虚拟机内部的引用,如基本类型对应的Class对象,一些常驻的异常对象(空指针,OOM)等,还有系统类加载器
  • 所有被同步锁(synchronized关键字)持有的对象

PS:除了这些固定的GC Roots集合以外,还有临时加入的,例如:老年代引用年轻代,这就是第5个根节点,也就是跨代引用,这样才可以可达性分析算法的正确性。(跨代引用(记忆集,卡表))

1.2.1 可达性分析算法 - 引用

引用分为强引用软引用弱引用虚引用4种

1.3 如果对象不可达,一定会被回收吗?

即便在可达性分析算法中判定为不可达的对象,他们也不会立刻死亡。一个对象真正死亡需要进行两次标记过程:

  • 对象在进行可达性分析算法后发现没有与GC Roots相连接的引用链
  • 此对象是否有必要执行finalize()方法。
    PS:如果对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,也就不会触发垃圾收集

如果这个对象被判定为要执行finalize()方法,就会把对象放置到F-Queue的队列中,后面会在虚拟机会自动建立的、低调度优先级的finalizer线程去执行它们的finalize()方法。虚拟机只会触发方法,并不会一定等待它运行结束。原因是finalize()方法执行缓慢,可能会导致F-Queue中的其他对象永久处于等待状态
PS:finalize()方法是对象逃脱死亡命运的最后一次机会,收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。

2 垃圾收集算法

2.1 分带收集理论
  1. 弱分代假说:绝大多数对象都是朝生夕灭的
  2. 强分代假说:熬过越多次垃圾收集过程的对象越难以消亡
  3. 跨代引用假说跨代引用相对于同代引用来说仅占极少数,只需要在新生代上建立一个全局的数据结构(记忆集),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后发生Minor GC,把这一小块内存加入到GC Roots进行扫描。
  4. Java堆划分出不同区域之后,垃圾收集器对此划分了:Minor GC”、“Major GC”、“Full GC”(回收整个堆空间 – 方法区和堆这样的回收类型的划分。
2.2 标记清除算法

标记清除算法分为“标记”和“清除”两个阶段:标记出需要回收的对象,标记完成以后统一回收掉被标记的对象,当然也可以反过来。标记就是对象是否属于垃圾的判定过程

  1. 执行效率不太稳定,如果JAVA堆中包含大量的对象,大部分需要回收,也就有了大量的工作量,这样效率会变低
  2. 内存空间会存在碎片化,标记、清除会产生大量不连续的内存碎片,这样会导致后面有大对象无法分配足够的连续内存空间的时候触发第二次GC
    在这里插入图片描述
2.3 标记复制算法

标记复制也可以看做是半区复制,主要是将内存划分为两块相等的大小,每次只使用其中一块。一块使用完毕,把活着的内存复制到新的上面去,然后直接清空这个内存。这种算法会有复制开销,当有大量的对象存活时候,会造成资源的一个浪费,好处是不会出现空间碎片化。
在这里插入图片描述

2.4 标记整理算法

标记整理算法与标记清除算法相比,前者是非移动式,后者是移动式的。这是一项优缺点并存的风险策略。
当有大量对象存活区域,这样的移动会造成极大的负担,而且这种移动会造成用户应用程序的暂停,也就是“Stop The World”
标记整理是把所有存活对象都向内存空间的一端移动,然后直接清除掉边界以外的内存
在这里插入图片描述

3 垃圾收集

3.1 根节点枚举

所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,那么毫无疑问就会面对“Stop The World”,其中根节点枚举必须在一个能保障一次性的快照中才能进行,因为不能出现在分析过程中,根节点集合的对象引用关系还在不断变化的情况。
这是导致垃圾收集过程必须暂停用户线程的一个重要原因,即使是号称停顿时间可控,或者(几乎)不会发生停顿的CMS、G1、ZGC等收集器,枚举根节点时也是必须要停顿的。

同时会带来一个问题就是,只是方法区的类都难以数清楚,要是逐个检查根节点的引用消耗的时间也不是一个小的数目,有没有更好的一个办法?

在HotSpot的解决方案里,使用了一组称为OopMap的数据结构来解决这个问题,一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样收集器在扫描时就可以直接得知这些信息了

3.2 安全点

在OopMap的协助下,虚拟机可以快速准确的完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系的变化,或者说导致OopMap内容的变化,那么也就意味着如果每一条指令都需要生成对应的OopMap,那么将会需要大量的额外存储空间。

不过实际上,HotSpot没有为每条指令都生成OopMap,只是在“特定的位置记录了这些信息,这些位置被称为安全点。安全点规定代码指令流并非可以随意停止,必须到达安全点才可以暂停开始垃圾收集。

安全点位置额选取基本上是以“是否具有让程序长时间执行的特征”为标准选定的,大概如下才会产生安全点:方法调用、循环跳转、异常跳转

那么现在需要思考的是,如何让线程跑到最近的安全点,然后停止下来。
两种方案:抢先式中断和主动式中断
抢先式中断:不需要代码主动配合,先停止用户线程,发现不在安全点,重新启用线程,让它到达安全点在中断。(基本不会使用)
主动式中断:不直接对线程操作,只是设置一个标志位,线程执行时会不停的轮询这个标志位,一旦标志位为true自己主动挂起。轮询的标志和安全点是重合的,另外还要加上所有创建对象额其他需要在Java堆上分配内存的地方,提前预防垃圾收集,避免没有足够的内存分配新对象
由于轮询的频繁性,HotSpot使用了内存保护陷阱的方式,把轮询操作精简至一条汇编指令的程度。
在这里插入图片描述

3.3 安全区域

安全点看似解决了如何停顿用户线程,让虚拟机进入垃圾回收状态问题,但是如果遇到线程不执行,例如sleep状态或者Blocked状态,这个时候线程是无法响应虚拟机的中断请求,不可能走到安全区中断自己。
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点
当线程进入了安全区域,垃圾收集就不会管进入了安全区域的线程。通时,安全区域的线程也不能主动离开,必须检查虚拟机是否完成了根节点枚举,完成了才可以离开。

3.4 记忆集与卡表

在前面有提到,为了解决对象跨代引用带来的问题,垃圾收集器在新生代中建立了记忆集的数据结构,这样就避免把老年代加进GC Roots扫描范围。所有涉及部分区域收集行为的垃圾收集器,典型的如G1、ZGC,都会出现这个问题,因此我们有必要进一步清理记忆集的原理和实现方式,记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。

延伸:记录精度:(了解)

  • 字长精度:每个记录精确到机器字长,改字包含跨代指针
  • 对象精度:每个记录精确到一个对象,该对象里有字段包含指针
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针
    PS:卡精度又称之为卡表的方式去实现记忆集,这要是目前最常用的一种记忆集实现方式,卡表就是记忆集的一种具体体现,它定义了记忆集的记录精度、与堆内存的映射关系等。

卡表最简单的形式可以只是一个字节数组。

CARD_TABLE[this address >> 9] = 0

字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”一般来说,卡页大小都是以2的N次幂的字节数。
//TODO

3.5 并发的可达性分析 - 三色标记

虚拟机把遇到过的对象,按照“是否访问过”这个条件标记分为三个颜色:

  • 白色:表示对象尚未被垃圾收集器访问过。在可达性分析刚刚开始的阶段,所有的对象都是白色的;在分析结束的阶段,任然还是白色的对象,即代表不可达。
  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色对象表示已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无需重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。(大概率将来也是存活对象)
    在这里插入图片描述
    三色标记算法,有可能把原本存活的对象错误标记为已死亡,这是非常致命的后果
    对象的消失必须要满足两个条件
  1. 赋值器插入了一条或多条从黑色对象到白色对象的新引用
  2. 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用

对象消失的解决方案:增量更新和原始快照

  • 增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
  • 原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索
    PS:以上无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的。在HotSpot虚拟机中,增量更新和原始快照这两种解决方案都有实际应用,譬如,CMS是基于增量更新来做并发标记的,G1则是用原始快照来实现

4 垃圾收集器

成熟的垃圾收集器有7种,我们大致可以分为两大类:新生代、老年代。

  • 新生代:Serial、ParNew、Parallel Scavenge
  • 老年代:Serial Old、Parallel Old、CMS
  • 全能型:G1
    在这里插入图片描述
4.1 年轻代 - Serial收集器

Serial是一个单线程工作的收集器,它只会使用一个处理器或一条收集线程去完成垃圾收集工作,并且它在垃圾收集时,必须暂停其他所有工作线程(用户线程),直到它收集结束。
在这里插入图片描述
它是HotSpot运行在客户端模式下的默认新生代收集器,在新生代采用的是标记-复制算法,单线程也就意味着没有线程切换的开销,额外内存消耗最小的,可以获取到单线程最高的收集效率

4.2 年轻代 - ParNew收集器

ParNew收集器实质上是Serial额多线程并行版本,是除了Serial之外,唯一能够和CMS配合工作的收集器
在这里插入图片描述
ParNew收集器在多核环境中,会随着可以被使用的核心数量的增加,ParNew对于垃圾收集的系统资源的高效利用还是很有好处的。它默认开启的收集线程数与核心处理器数量相同,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数
PS:建议使用默认线程数

4.3 年轻代 - Parallel Scavenge收集器

Parallel Scavenge也是基于标记-复制算法实现的一款新生代收集器,也是能够并行收集的多线程收集器,虽然与parNew非常相似,Parallel Scavenge缺有着自己的独特之处。CMS等垃圾收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge的目标却是达到一个可控制的吞吐量
吞吐量 = 运行用户代码的时间/运行用户代码的时间 + 运行垃圾收集时间
例如:用户代码+垃圾收集总耗时100分钟,其中垃圾收集1分钟,那么吞吐量就是99%

Parallel Scavenge为此提供了两个参数用于精确控制吞吐量,分别是:控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis参数以及直接设置吞吐量大小 -XX:GCTimeRatio参数

  • -XX:MaxGCPauseMillis允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设置的值。通时对于这个值也并不是设置的越小越好,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:当把这个参数调小的时候,新生代中收集300MB的速度肯定比500MB速度快,但是一旦时间不够,那么也会导致垃圾收集发生得更频繁。停顿的时间虽然下降了,但是吞吐量也降下来了。
  • -XX:GCTimeRatio参数应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。比如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19)),默认值是99,即允许最大1%(即1/(1+19))的垃圾收集时间

Parallel Scavenge收集器是一个“吞吐量优先收集器”。除开上面两个参数,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得我们关注。这个参数的主要作用是不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了。主要是给不了解垃圾收集器的使用的。

4.4 老年代 - Serial Old收集器

Serial Old是老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果是服务端模式下:它可以作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用
在这里插入图片描述

4.5 老年代 - Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现
在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合
在这里插入图片描述

4.6 老年代 - CMS

CMS收集器是一种以获取最短回收暂停时间为目标的收集器。CMS收集器**是基于标记-复制算法实现的。他的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为四步:

  • 初始标记:初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并且会暂停所有的用户线程 —“Stop The World”
  • 并发标记:并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时比较长但是不需要暂停用户线程,可以与垃圾收集线程一起运行
  • 重新标记:重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,并且会暂停所有的用户线程 —“Stop The World”
  • 最终标记:最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
    在这里插入图片描述

CMS虽然是集并发收集、低停顿一体的收集器,但是并不完美,至少还有三个明显的缺点:

  • CMS收集器对处理器资源非常敏感,虽然不会导致用户线程停顿,但是会占用了一部分线程导致程序变慢,降低吞吐量。CMS默认启动的回收线程数是(处理器核心线程数+3)/4,也就是说,只要核心线程数在4个或者以上,并发只会占用不超过25%的资源。
  • 无法处理“浮动垃圾”,有可能出现“Con-current Mode Failure”从而导致Full GC的产生。在**CMS的并发标记和清理阶段,用户线程还是在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集处理他们,只好留给下一次垃圾收集时再清理掉。**这一部分垃圾就称为“浮动垃圾”。因为在垃圾收集的过程中用户线程还在运行,所以CMS收集器的启动阈值在JDK6以后设置为92%。但是这个时候又会面临又一个问题,预留的内存无法满足新对象的需求,就会出现一次“并发失败”,这时候虚拟机做了一个备案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了
  • CMS是基于“标记-清除”算法实现的收集器,那么意味着收集结束就会有大量的空间碎片产生。空间碎片过多就会给大对象分配时候造成很大的麻烦,往往老年代还有很多的空间剩余,不得不提前触发Full GC
4.7 老年代 - Garbage Fist(G1)收集器

G1收集器是面向局部收集的设计思路和基于Region的内存布局形式,在JDK9以后成为服务端模式下的默认垃圾收集器,CMS也不再推荐使用
在G1之前,所有的收集器要么是基于新生代要么是基于老年代,要么就是整个堆(full GC)。G1是把真个内存空间划分了无数的小块,可以面向整个堆内存的任何部分来组成回收集(CSet)进行回收,不再是衡量属于哪个分代,而是哪块内存存放的垃圾数量最多,回收效益最大。
在这里插入图片描述
虽然G1是把内存划分成若干个内存块,但是还是遵循分代收集理论设计的,区别在于:

  • 不再坚持固定大小以及固定数量的分代区域划分,而是划分成若干个大小相等的独立区域(Region),每个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间或者老年代空间。这样就可以针对不同角色的Region采用不同的策略去处理,这样无论是新生代或者老年代都可以获取很好的收集效果。
  • Region还有一个特区的Humougous区域,专门用来存储大对象的,G1认为只要超过了一个Region容量一半的对象即可判定为大对象。Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围1MB~32MB,要是2的N次幂。对于那些超级大对象,就会连续存储到N个连续的Humougous Region之中,绝大多数会把Humougous Region作为老年代的一部分看待

前面已经说过G1收集器的特点是把整个堆内存划分为若干个大小相等的内存,在垃圾收集时,会去评估每个Region里面垃圾堆积的价值大小,价值就是回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,尽可能的保证在有限的时间中获取更高的收益

虽然Region把内存划分为了若各个小块,但是还是会有跨代引用问题。
对此,G1使用的还是记忆集的方式,但是要相比CMS还是要复杂许多,每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region指向自己的指针,病标记这些指针分别在哪些卡页的范围之内。本质上是一种哈希表,key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。**这种“双向”的卡表结构(我指向谁,谁指向我)**比原来的卡表更加的复杂,内存占比也会更大。根据经验,大概需要相当于Java堆容量的10%~20%的额外内存来维持收集器工作

前面说了跨代引用,那么G1在并发阶段是如何保证收集线程与用户线程互不干扰运行的?
CMS采集器采用增量更新算法实现,而G1收集器则是通过原始快照算法来实现的。此外垃圾收集对用户线程额影响还体现在回收过程中新对象创建的内存分配上,G1为每个Region设计了两个名为TAMS的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新对象分配的地址偶必须在这两个指针位置以上。G1默认在这个地址以上的对象是隐式标记过的,即默认他们是存活的,不会纳入回收范围。与CMS中的“Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收速度赶不上内存分配速度,G1收集器也要被迫冻结用户线程执行,导致full GC而产生长时间“Stop The World”

G1收集器的运作过程大致可以分为以下四个步骤:

  • 初始标记:仅仅只是标记一下GC Roots能直接关联的对象,并且修改TAMS指针的值,让下一阶段用户并发运行时,能正确地在可用额Region中分配新对象。这个阶段实际并没有额外的停顿。—stop the world
  • 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这个阶段耗时比较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有变动的对象。
  • 最终标记:暂停用户线程,处理并发阶段用户线程留下来的少量垃圾
  • 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成的回收集,然后把决定回收的部分复制到空的Region中,再清除掉整个旧的Region的全部空间。—Stop The World
    在这里插入图片描述
    从上述阶段的描述可以看出,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才能担当起“全功能收集器”的重任与期望

5 内存分配策略

5.1 对象优先在Eden分配,大对象直接进入老年代

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组,HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。
注意 -XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,HotSpot的其他新生代收集器,如Parallel Scavenge并不支持这个参数。如果必须使用此参数进行调优,可考虑ParNew加CMS的收集器组合。

5.2 长期存活的对象将进入老年代

HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头中。对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数**-XX:MaxTenuringThreshold**设置。

(对于这个对象年龄的设置,一般情况下,不要去做任何变更。如果我们的年轻代里边的新生对象大部分都是存活1岁,只有特别小的部分会存活时间很长,并且程序长时间运行的时候,这部分大对象所占用的空间保持不变,或者说,变化很小。可以考虑适当增大这个年龄。)

了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。

5.3 空间分配担保策略及垃圾回收过程
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值